Sfoglia il codice sorgente

对接佳博打印机 SDK

cc12458 3 settimane fa
parent
commit
73a17a4cbd

+ 1 - 1
gradle/libs.versions.toml

@@ -13,7 +13,7 @@ appcompat = "1.7.1"
 
 gson = "2.13.1"
 nanohttpd = "2.3.1"
-navigationCompose = "2.9.0"
+navigationCompose = "2.9.1"
 okhttp = "4.12.0"
 webkit = "1.14.0"
 

+ 1 - 0
library/device/printer/build.gradle.kts

@@ -40,4 +40,5 @@ dependencies {
   implementation(project(":core"))
   implementation(libs.gson)
   implementation(libs.okhttp)
+  implementation("com.gainscha:sdk2:2.0.0")
 }

+ 42 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/JsonUtil.kt

@@ -0,0 +1,42 @@
+package com.hzliuzhi.applet.printer
+
+import android.util.Log
+import com.google.gson.Gson
+import com.google.gson.GsonBuilder
+import com.google.gson.JsonDeserializationContext
+import com.google.gson.JsonDeserializer
+import com.google.gson.JsonElement
+import com.google.gson.JsonParseException
+import com.google.gson.JsonPrimitive
+import com.google.gson.JsonSerializationContext
+import com.google.gson.JsonSerializer
+
+object JsonUtil {
+  val gson: Gson by lazy {
+    GsonBuilder()
+      .registerTypeAdapter(PrintDevice.Type::class.java, PrintDeviceTypeAdapter())
+      .create()
+  }
+}
+
+private class PrintDeviceTypeAdapter : JsonDeserializer<PrintDevice.Type>, JsonSerializer<PrintDevice.Type> {
+  override fun deserialize(json: JsonElement, typeOfT: java.lang.reflect.Type, context: JsonDeserializationContext): PrintDevice.Type {
+    return when {
+      json.isJsonPrimitive && json.asJsonPrimitive.isString -> {
+        val label = json.asString
+        PrintDevice.Type.entries.find { it.label == label } ?: throw JsonParseException("Unknown label: $label")
+      }
+
+      json.isJsonPrimitive && json.asJsonPrimitive.isNumber -> {
+        val value = json.asInt
+        PrintDevice.Type.entries.find { it.value == value } ?: throw JsonParseException("Unknown value: $value")
+      }
+
+      else -> throw JsonParseException("Invalid type for PrintDevice.Type: $json")
+    }
+  }
+
+  override fun serialize(src: PrintDevice.Type, typeOfSrc: java.lang.reflect.Type, context: JsonSerializationContext): JsonElement {
+    return JsonPrimitive(src.label)
+  }
+}

+ 29 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/PrintDevice.kt

@@ -0,0 +1,29 @@
+package com.hzliuzhi.applet.printer
+
+import com.gainscha.sdk2.model.PrinterDevice
+import com.gainscha.sdk2.model.WifiPrinterDevice
+import com.google.gson.JsonElement
+
+data class PrintDevice(
+  val type: Type,
+
+  val ip: String?,
+  val port: Int?,
+) {
+  enum class Type(val value: Int, val label: String) {
+    WIFI(1, "wifi"),
+    ;
+  }
+
+  constructor(ip: String, port: Int) : this(type = Type.WIFI, ip = ip, port = port)
+
+
+  companion object {
+    fun fromJson(json: JsonElement): PrintDevice? = JsonUtil.gson.fromJson(json, PrintDevice::class.java)
+
+    fun fromJson(json: String): PrintDevice? = JsonUtil.gson.fromJson(json, PrintDevice::class.java)
+  }
+
+  fun toJson(): String = JsonUtil.gson.toJson(this)
+}
+

+ 5 - 2
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/PrintParams.kt

@@ -1,12 +1,15 @@
 package com.hzliuzhi.applet.printer
 
-import com.google.gson.Gson
 import com.google.gson.JsonElement
 
 data class PrintParams(
   val url: String?,
+  val device: PrintDevice?,
+  val tspl: String?,
 ) {
   companion object {
-    fun formJson(element: JsonElement): PrintParams? = Gson().fromJson(element, PrintParams::class.java)
+    fun fromJson(element: JsonElement): PrintParams? = JsonUtil.gson.fromJson(element, PrintParams::class.java)
   }
 }
+
+

+ 52 - 8
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/Printer.kt

@@ -5,18 +5,22 @@ import android.content.Context
 import android.print.PrintAttributes
 import android.print.PrintDocumentAdapter
 import android.print.PrintManager
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
 import com.google.gson.JsonElement
 import com.hzliuzhi.applet.core.shared.Payload
 import com.hzliuzhi.applet.core.shared.SharedFlowHub
 import com.hzliuzhi.applet.core.shared.SharedFlowHub.callbackAs
 import com.hzliuzhi.applet.core.shared.SharedFlowHub.cast
+import com.hzliuzhi.applet.printer.impl.GPrinter
 import com.hzliuzhi.applet.printer.impl.PrintPdfDocumentAdapter
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.filter
 import kotlinx.coroutines.flow.launchIn
 import kotlinx.coroutines.flow.onEach
 
-class Printer(context: Context) {
+class Printer(context: Context) : DefaultLifecycleObserver {
   private val applicationContext = context.applicationContext
 
   private val manager by lazy {
@@ -32,11 +36,19 @@ class Printer(context: Context) {
     private var instance: Printer? = null
     fun getInstance(context: Context): Printer {
       return instance ?: synchronized(this) {
-        Printer(context).also { instance = it }
+        Printer(context).also {
+          instance = it
+          (context as? ComponentActivity)?.lifecycle?.addObserver(it)
+        }
       }
     }
   }
 
+  override fun onDestroy(owner: LifecycleOwner) {
+    super.onDestroy(owner)
+    GPrinter.onDestroy()
+  }
+
   fun eventHandle(scope: CoroutineScope) {
     SharedFlowHub.events
       .filter { it.type.contains("print") }
@@ -46,18 +58,46 @@ class Printer(context: Context) {
             event.cast<JsonElement, JsonElement>()?.also { event ->
               callback = event.callback
               event.payload
-                ?.let { PrintParams.formJson(it) }
+                ?.let { PrintParams.fromJson(it) }
                 ?.also { print(it) }
                 ?: invoke(Payload.error<Unit>(message = "[print] 参数解析错误"))
             }
           }
 
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:print:connect" -> run {
+            event.cast<JsonElement, JsonElement>()?.also { event ->
+              callback = event.callback
+              event.payload
+                ?.let { PrintDevice.fromJson(it) }
+                ?.also { device ->
+                  GPrinter.connect(device) {
+                    val payload = if (it.second) Payload.data(device, message = "连接成功") else Payload.error<Unit>(message = "连接失败")
+                    invoke(payload)
+                  }
+                }
+                ?: invoke(Payload.error<Unit>(message = "[print:connect] 参数解析错误"))
+            }
+          }
+
+          "${SharedFlowHub.WEBVIEW_BRIDGE_EVENT}:print:disconnect" -> run {
+            event.cast<JsonElement, JsonElement>()?.also { event ->
+              callback = event.callback
+              event.payload
+                ?.let { PrintDevice.fromJson(it) }
+                ?.also { device ->
+                  GPrinter.disconnect(device) {
+                    val payload = if (!it.second) Payload.data(device, message = "断开成功") else Payload.error<Unit>(message = "断开失败")
+                    invoke(payload)
+                  }
+                }
+                ?: invoke(Payload.error<Unit>(message = "[print:disconnect] 参数解析错误"))
+            }
+          }
+
           "print" -> run {
             event.cast<PrintDocumentAdapter, JsonElement>()?.also { event ->
               callback = event.callback
-              event.payload?.also {
-                print(adapter = it)
-              } ?: invoke(Payload.error<Unit>(message = "[print] 参数解析错误"))
+              event.payload?.also { print(adapter = it) } ?: invoke(Payload.error<Unit>(message = "[print] 参数解析错误"))
             }
           }
 
@@ -71,9 +111,13 @@ class Printer(context: Context) {
     if (!params.url.isNullOrEmpty()) {
       PrintPdfDocumentAdapter.download(
         applicationContext, params.url,
-        { file -> print(file.name, PrintPdfDocumentAdapter(file, { invoke(Payload.data(true));true }, { invoke(Payload.error<Unit>(message = it)); true })) },
+        { file -> print(file.name, PrintPdfDocumentAdapter(file, { invoke(Payload.data(true)); true }, { invoke(Payload.error<Unit>(message = it)); true })) },
         { message -> Payload.error<Unit>(message = message) }
       )
+    } else if (!params.tspl.isNullOrEmpty()) {
+      runCatching { GPrinter.print(device = params.device, text = params.tspl) }
+        .onSuccess { invoke(Payload.data(true)) }
+        .onFailure { invoke(Payload.error<Unit>(message = it.message)) }
     }
   }
 
@@ -85,7 +129,7 @@ class Printer(context: Context) {
   }
 
   private fun invoke(payload: Payload<*>) {
-    payload.toJson()?.also { callback?.invoke(it) }
+    JsonUtil.gson.toJsonTree(payload, Payload::class.java).also { callback?.invoke(it) }
     callback = null
   }
 }

+ 108 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/impl/GPrinter.kt

@@ -0,0 +1,108 @@
+package com.hzliuzhi.applet.printer.impl
+
+import com.gainscha.sdk2.ConnectionListener
+import com.gainscha.sdk2.Printer
+import com.gainscha.sdk2.command.Tspl
+import com.gainscha.sdk2.model.PrinterDevice
+import com.gainscha.sdk2.model.WifiPrinterDevice
+import com.hzliuzhi.applet.core.shared.Message
+import com.hzliuzhi.applet.core.shared.Payload
+import com.hzliuzhi.applet.core.shared.SharedFlowHub
+import com.hzliuzhi.applet.printer.JsonUtil
+import com.hzliuzhi.applet.printer.PrintDevice
+
+object GPrinter {
+  init {
+    object : ConnectionListener {
+      override fun onPrinterConnected(printer: Printer?) {
+        printer?.printerDevice?.to().also {
+          callback[it]?.invoke(Pair(printer, true))
+          callback.remove(it)
+        } ?: emit("print:connect", Payload.data(printer?.printerDevice?.to() ?: true, message = "自动连接成功 (${Printer.getConnectedPrinters().size})"))
+      }
+
+      override fun onPrinterConnectFail(printer: Printer?) {
+        printer?.printerDevice?.to().also {
+          callback[it]?.invoke(Pair(printer, false))
+          callback.remove(it)
+        }
+      }
+
+      override fun onPrinterDisconnect(printer: Printer?) {
+        printer?.printerDevice?.to()?.takeIf { callback.containsKey(it) }?.also {
+          callback[it]?.invoke(Pair(printer, false))
+          callback.remove(it)
+        } ?: emit("print:disconnect", Payload.data(printer?.printerDevice?.to() ?: true, message = "自动断开成功 (${Printer.getConnectedPrinters().size})"))
+      }
+    }.also { Printer.addConnectionListener(it) }
+  }
+
+  fun onDestroy() {
+    Printer.removeAllConnectionListener()
+    Printer.getConnectedPrinters().onEach { it.disconnect() }
+  }
+
+  private val callback = mutableMapOf<PrintDevice, (Pair<Printer?, Boolean>) -> Unit>()
+
+  fun connect(device: PrintDevice, onResult: (Pair<Printer?, Boolean>) -> Unit) {
+    device.getConnected()?.takeIf { it.isConnected }?.also { onResult(Pair(it, true)) } ?: run {
+      callback[device] = onResult
+      Printer.connect(device.toGPrinter())
+    }
+  }
+
+  fun disconnect(device: PrintDevice, onResult: (Pair<Printer?, Boolean>) -> Unit) {
+    callback[device] = onResult
+    device.getConnected()?.also { printer ->
+      printer.disconnect()
+    }
+  }
+
+  private fun emit(type: String, payload: Payload<*>) {
+    Message(
+      type = type,
+      payload = JsonUtil.gson.toJsonTree(payload, Payload::class.java)
+    ).toWebEvent().also { SharedFlowHub.emit(it) }
+  }
+
+  fun print(text: String, device: PrintDevice?) {
+    fun doPrint(printer: Printer?) {
+      if (printer == null) throw IllegalArgumentException("打印机(${device ?: ""})未连接")
+
+      val tspl = Tspl().fromString(text)
+      printer.print(tspl.bytes)
+    }
+
+    val printer = if (device == null) {
+      Printer.getConnectedPrinters().firstOrNull { it.isConnected }
+    } else {
+      device.getConnected()?.takeIf { it.isConnected }
+    }
+
+    if (printer != null) {
+      doPrint(printer)
+    } else if (device != null) {
+      connect(device) { (printer) -> doPrint(printer) }
+    } else {
+      throw IllegalArgumentException("未指定打印机且无已连接打印机")
+    }
+  }
+}
+
+private fun PrintDevice.getConnected(): Printer? {
+  return Printer.getConnectedPrinters().firstOrNull { printer -> printer.printerDevice.to() == this }
+}
+
+private fun PrintDevice.toGPrinter() = when (type) {
+  PrintDevice.Type.WIFI -> WifiPrinterDevice().apply {
+    ip = this@toGPrinter.ip
+    port = this@toGPrinter.port!!
+  }
+}
+
+private fun PrinterDevice.to(): PrintDevice? {
+  return when (this) {
+    is WifiPrinterDevice -> PrintDevice(ip = ip, port = port)
+    else -> null
+  }
+}

+ 60 - 0
library/device/printer/src/main/java/com/hzliuzhi/applet/printer/impl/Tspl.kt

@@ -0,0 +1,60 @@
+package com.hzliuzhi.applet.printer.impl
+
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.util.Base64
+import android.util.Log
+import com.gainscha.sdk2.command.Tspl
+import com.gainscha.sdk2.command.Tspl.BITMAP_MODE
+
+fun Tspl.fromString(value: String?) = apply {
+  value.takeUnless { it.isNullOrEmpty() }?.trimIndent()?.lines()?.forEach {
+    val line = it.trim()
+    // Log.d("log:printer", "G line:-> $line")
+    when {
+      line.startsWith("BITMAP", true) -> runCatching {
+        /**
+         * BITMAP x,y,width,height,mode,base64_data
+         *  - x,y : 坐标,表示图片左上角在标签纸上的起始位置(单位:点,1mm ≈ 8点,具体看打印机DPI)。
+         *  - width : 图片的宽度(单位:字节,以 8 点为单位,1字节=8点),描述每一行有多少个字节(每个字节8点)
+         *  - height : 图片的高度(单位:点),描述总共有多少行。
+         *  - mode,位图模式:
+         *    - 0:OVERWRITE (覆盖,即直接覆盖原有内容)。
+         *    - 1:OR(按位或,叠加)。
+         *    - 2:XOR(按位异或)。
+         *    - 3:OVERWRITE_COMPRESS
+         *  - bitmap_data : 图片数据,需要转换成 bitmap_data
+         */
+        Regex("""BITMAP\s+(\d+),(\d+),(\d+),(\d+),(\d+),(.+)""").find(line)?.also { match ->
+          val (x, y, width, height, mode, data) = match.destructured
+          addBitmap(
+            x.toInt(),
+            y.toInt(),
+            getBitmapFromString(data),
+            width.toInt() * 8, // width 参数为字节数,实际像素宽度=width*8
+            getBitmapModeFromInt(mode.toInt()),
+            false
+          )
+        } ?: throw IllegalArgumentException("BITMAP 指令格式错误 (BITMAP x,y,width,height,mode,base64_data)")
+      }.onFailure { Log.e("TSPL", "BITMAP 指令解析异常: $line, error: ${it.message}") }
+
+      line.isNotBlank() -> addData("$line\r\n")
+    }
+  }
+}
+
+private fun getBitmapFromString(value: String): Bitmap {
+  val base64 = value.substringAfterLast("base64,", value).trim()
+  val bytes = Base64.decode(base64, Base64.DEFAULT)
+  return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+}
+
+private fun getBitmapModeFromInt(value: Int): BITMAP_MODE {
+  return when (value) {
+    0 -> BITMAP_MODE.OVERWRITE
+    1 -> BITMAP_MODE.OR
+    2 -> BITMAP_MODE.XOR
+    3 -> BITMAP_MODE.OVERWRITE_COMPRESS
+    else -> BITMAP_MODE.OVERWRITE
+  }
+}

+ 4 - 0
settings.gradle.kts

@@ -16,6 +16,10 @@ dependencyResolutionManagement {
   repositories {
     google()
     mavenCentral()
+    maven {
+      url = uri("http://118.31.6.84:8081/repository/maven-public/")
+      isAllowInsecureProtocol = true
+    }
     maven { url = uri("https://jitpack.io") }
     maven { url = uri("${rootProject.projectDir}/local-repo") }
   }