|
|
@@ -0,0 +1,482 @@
|
|
|
+package com.grkj.data.hardware.fingerprint
|
|
|
+
|
|
|
+import android.util.Base64
|
|
|
+import com.grkj.shared.utils.extension.toHexStrings
|
|
|
+import org.slf4j.LoggerFactory
|
|
|
+import com.sik.comm.codec.PassThroughCodec
|
|
|
+import com.sik.comm.impl_modbus.ModbusConfig
|
|
|
+import com.sik.comm.impl_modbus.ModbusProtocol
|
|
|
+import com.sik.comm.core.model.CommMessage
|
|
|
+import com.sik.comm.core.model.TxPlan
|
|
|
+import com.sik.comm.core.model.ChainStepResult
|
|
|
+import com.sik.comm.core.policy.ChainPolicy
|
|
|
+import com.sik.comm.core.protocol.LinkIO
|
|
|
+import kotlinx.coroutines.*
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean
|
|
|
+import kotlin.math.abs
|
|
|
+
|
|
|
+object FingerprintCaptureService {
|
|
|
+ private val log = LoggerFactory.getLogger("FPM.Capture")
|
|
|
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
|
|
+ private val running = AtomicBoolean(false)
|
|
|
+
|
|
|
+ // 设备 & 通信
|
|
|
+ private var protocol: ModbusProtocol? = null
|
|
|
+ private var deviceId: String = "FPM-01"
|
|
|
+ private var address: Int = FpmPackets.DEFAULT_ADDR
|
|
|
+
|
|
|
+ // 持久化 framer(粘包/半包友好)
|
|
|
+ private val framer = FpmFramer()
|
|
|
+
|
|
|
+ // —— 文档常量 —— //
|
|
|
+ private const val INS_WRITE_REG: Byte = 0x0E // PS_WriteReg(写系统寄存器)
|
|
|
+
|
|
|
+ // 表3-31:寄存器号映射
|
|
|
+ private const val REG_DELAY_TIME: Int = 0x00 // 0: 串口延时 0~255ms
|
|
|
+ private const val REG_BAUD_RATE: Int = 0x04 // 4: 波特率控制 N (波特率 = N*9600, 1..12)
|
|
|
+ private const val REG_PKT_SIZE: Int = 0x06 // 6: 包大小 0:32 / 1:64 / 2:128 / 3:256
|
|
|
+
|
|
|
+ // 写“1字节内容”的挂起版本(PS_WriteReg: [INS][reg(1)][value(1)])
|
|
|
+ private suspend fun writeReg8Suspend(
|
|
|
+ proto: ModbusProtocol,
|
|
|
+ tag: String,
|
|
|
+ reg: Int,
|
|
|
+ value: Int
|
|
|
+ ) {
|
|
|
+ val body = byteArrayOf(reg.toByte(), (value and 0xFF).toByte())
|
|
|
+ val frame = FpmPackets.cmd(address, INS_WRITE_REG, body)
|
|
|
+ log.info("指令:{}", frame.toHexStrings())
|
|
|
+ val rsp: CommMessage =
|
|
|
+ proto.send(deviceId, FpmPackets.toMessage(tag, frame, timeoutMs = 2000, gapMs = 30))
|
|
|
+ log.info("指令返回:{}", rsp.payload.toHexStrings())
|
|
|
+ val (ack, _) = FpmPackets.parseAck(rsp.payload, address)
|
|
|
+ require(ack == 0x00) { "$tag failed, ack=0x${ack.toString(16)}" }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun register(
|
|
|
+ port: String,
|
|
|
+ baud: Int = 57_600,
|
|
|
+ deviceId: String = "FPM-01",
|
|
|
+ address: Int = FpmPackets.DEFAULT_ADDR,
|
|
|
+ boostTo115200: Boolean = true, // 初始化后如非115200则提速到115200
|
|
|
+ setZeroDelay: Boolean = false, // 不改延时
|
|
|
+ setPktMax: Boolean = true // 包大小拉满(256B)
|
|
|
+ ): FingerprintCaptureService {
|
|
|
+ this.deviceId = deviceId
|
|
|
+ this.address = address
|
|
|
+
|
|
|
+ log.info("=== FPM.register() 启动 ===")
|
|
|
+ log.info(
|
|
|
+ "目标串口={}, 默认波特率={}, 优先尝试115200={}, 设置包长={}, 设置延时={}",
|
|
|
+ port, baud, boostTo115200, setPktMax, setZeroDelay
|
|
|
+ )
|
|
|
+
|
|
|
+ // —— 1) 优先尝试 115200,其次回退到入参 baud —— //
|
|
|
+ val tryOrders = buildList {
|
|
|
+ if (baud != 115_200) add(115_200)
|
|
|
+ add(baud)
|
|
|
+ }
|
|
|
+
|
|
|
+ var connectedProto: ModbusProtocol? = null
|
|
|
+ var connectedBaud: Int? = null
|
|
|
+
|
|
|
+ fun buildConfig(br: Int) = ModbusConfig(
|
|
|
+ deviceId = deviceId,
|
|
|
+ portName = port,
|
|
|
+ baudRate = br,
|
|
|
+ dataBits = 8,
|
|
|
+ stopBits = 1,
|
|
|
+ parity = 'N',
|
|
|
+ defaultUnitId = null,
|
|
|
+ codec = PassThroughCodec(),
|
|
|
+ requestTimeoutMs = 4000,
|
|
|
+ responseGapMs = 30
|
|
|
+ )
|
|
|
+
|
|
|
+ suspend fun warmUp(proto: ModbusProtocol) {
|
|
|
+ // 让设备喘口气:上电/切参/写寄存器后给点时间
|
|
|
+ delay(200)
|
|
|
+
|
|
|
+ // 尝试清空输入缓冲,通过一次“空计划”触发 LinkIO 读
|
|
|
+ runCatching {
|
|
|
+ val plan = TxPlan(emptyList()) // 空发送计划
|
|
|
+ val policy = object : ChainPolicy {
|
|
|
+ override suspend fun afterSendStep(
|
|
|
+ stepIndex: Int,
|
|
|
+ sent: CommMessage,
|
|
|
+ io: LinkIO
|
|
|
+ ): ChainStepResult {
|
|
|
+ // 直接调用 LinkIO.readRaw 来把残留数据读出来扔掉
|
|
|
+ runCatching {
|
|
|
+ io.readRaw(timeoutMs = 80, expectedSize = null, silenceGapMs = 8)
|
|
|
+ }
|
|
|
+ return ChainStepResult(emptyList(), false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ proto.sendChain(deviceId, plan, policy)
|
|
|
+ }.onFailure {
|
|
|
+ log.debug("warmUp(): skip clear buffer, {}", it.message)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 探活:GET_IMAGE 成功(0x00)或无手指(0x02)都算链路OK
|
|
|
+ suspend fun linkAlive(proto: ModbusProtocol): Boolean {
|
|
|
+ // 连接/切波特/写寄存器之后先暖机
|
|
|
+ warmUp(proto)
|
|
|
+
|
|
|
+ repeat(3) { i ->
|
|
|
+ val preDelay = listOf(60L, 120L, 240L)[i]
|
|
|
+ delay(preDelay) // 给模块采样硬件/镜头稳定时间
|
|
|
+ val ok = runCatching {
|
|
|
+ val ack = getImageOnce(proto) // 0x00 成功 / 0x02 无手指
|
|
|
+ (ack == 0x00 || ack == 0x02)
|
|
|
+ }.onFailure { e ->
|
|
|
+ log.warn("探活异常[{}]: {}", i, e.message)
|
|
|
+ }.getOrDefault(false)
|
|
|
+ if (ok) return true
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ // —— 尝试不同波特率连接 —— //
|
|
|
+ runBlocking {
|
|
|
+ for (br in tryOrders) {
|
|
|
+ log.info("尝试波特率 {} 连接设备 {}", br, deviceId)
|
|
|
+ val cfg = buildConfig(br)
|
|
|
+ val p = ModbusProtocol().apply { registerConfig(cfg) }
|
|
|
+
|
|
|
+ val connOk = runCatching {
|
|
|
+ p.connect(cfg.deviceId)
|
|
|
+ true
|
|
|
+ }.onFailure {
|
|
|
+ log.warn("波特率 {} 连接异常: {}", br, it.message)
|
|
|
+ }.getOrDefault(false)
|
|
|
+
|
|
|
+ if (!connOk) {
|
|
|
+ runCatching { p.disconnect(cfg.deviceId) }
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ val ok = linkAlive(p)
|
|
|
+ log.info("波特率 {} 探活结果 = {}", br, ok)
|
|
|
+ if (ok) {
|
|
|
+ connectedProto = p
|
|
|
+ connectedBaud = br
|
|
|
+ protocol = p
|
|
|
+ log.info("波特率 {} 链接成功并探活通过", br)
|
|
|
+ break
|
|
|
+ } else {
|
|
|
+ log.warn("波特率 {} 探活失败,准备断开重试", br)
|
|
|
+ runCatching { p.disconnect(cfg.deviceId) }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ val proto = connectedProto
|
|
|
+ ?: run {
|
|
|
+ log.error("全部波特率尝试失败: {}", tryOrders)
|
|
|
+ throw IllegalStateException("无法在 ${tryOrders.joinToString()} 波特率下建立可用链路")
|
|
|
+ }
|
|
|
+
|
|
|
+ // —— 2) 已连上:设置包大小(不改延时) —— //
|
|
|
+ runBlocking {
|
|
|
+ if (setPktMax) {
|
|
|
+ log.info("设置包长=256B (寄存器6 写 0x03)")
|
|
|
+ writeReg8Suspend(proto, "FPM_SET_PKT", REG_PKT_SIZE, 3)
|
|
|
+ }
|
|
|
+ if (setZeroDelay) {
|
|
|
+ log.info("设置DelayTime=0 (寄存器0 写 0x00)")
|
|
|
+ writeReg8Suspend(proto, "FPM_SET_DELAY", REG_DELAY_TIME, 0)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // —— 3) 如果当前波特率不是115200,且允许提速 —— //
|
|
|
+ if (boostTo115200 && connectedBaud != 115_200) {
|
|
|
+ log.info("当前波特率 {},准备提速到 115200", connectedBaud)
|
|
|
+ runBlocking {
|
|
|
+ val coeff = 115_200 / 9_600 // =12
|
|
|
+ writeReg8Suspend(proto, "FPM_SET_BAUD", REG_BAUD_RATE, coeff)
|
|
|
+ }
|
|
|
+ log.info("波特率寄存器已写入=12 (115200),准备断开重连")
|
|
|
+
|
|
|
+ runCatching { proto.disconnect(deviceId) }
|
|
|
+ Thread.sleep(50)
|
|
|
+
|
|
|
+ val hi = buildConfig(115_200)
|
|
|
+ val protoHi = ModbusProtocol().apply { registerConfig(hi) }
|
|
|
+ protoHi.connect(hi.deviceId)
|
|
|
+ log.info("已切换主机串口参数到115200,开始探活验证")
|
|
|
+
|
|
|
+ runBlocking {
|
|
|
+ val ok = linkAlive(protoHi)
|
|
|
+ log.info("115200 探活结果 = {}", ok)
|
|
|
+ require(ok) { "115200切参后设备未应答,请检查连线/保存的波特率。" }
|
|
|
+ }
|
|
|
+
|
|
|
+ protocol = protoHi
|
|
|
+ log.info("波特率 115200 链接成功,register() 结束")
|
|
|
+ } else {
|
|
|
+ log.info("register() 完成,无需切波特率,当前={}", connectedBaud)
|
|
|
+ }
|
|
|
+
|
|
|
+ return this
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ fun stop() {
|
|
|
+ if (running.getAndSet(false)) {
|
|
|
+ scope.coroutineContext.cancelChildren()
|
|
|
+ runCatching { protocol?.disconnect(deviceId) }.onFailure { /*ignore*/ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun listen(
|
|
|
+ onStart: () -> Unit,
|
|
|
+ onImage: (String) -> Unit,
|
|
|
+ onError: (Throwable) -> Unit,
|
|
|
+ noFingerSleepMs: Long = 200,
|
|
|
+ betweenShotsSleepMs: Long = 50
|
|
|
+ ) {
|
|
|
+ if (!running.compareAndSet(false, true)) return
|
|
|
+ val proto = protocol
|
|
|
+ ?: run { running.set(false); onError(IllegalStateException("not registered")); return }
|
|
|
+ scope.launch {
|
|
|
+ try {
|
|
|
+ loop@ while (running.get()) {
|
|
|
+ val ack = try {
|
|
|
+ getImageOnce(proto)
|
|
|
+ } catch (e: Throwable) {
|
|
|
+ onError(e); delay(300); continue@loop
|
|
|
+ }
|
|
|
+ when (ack) {
|
|
|
+ 0x00 -> {
|
|
|
+ onStart()
|
|
|
+ val raw4b = try {
|
|
|
+ upImageOnce(proto)
|
|
|
+ } catch (e: Throwable) {
|
|
|
+ onError(e); delay(300); continue@loop
|
|
|
+ }
|
|
|
+ // —— 自适应解码:半字节顺序 + 分辨率 —— //
|
|
|
+ val (gray8, w, h) = decodeAuto(raw4b)
|
|
|
+ val b64 = gray8ToPngBase64(gray8, w, h)
|
|
|
+ try {
|
|
|
+ withContext(Dispatchers.Main) { onImage(b64) }
|
|
|
+ } catch (_: Throwable) {
|
|
|
+ }
|
|
|
+ delay(betweenShotsSleepMs)
|
|
|
+ }
|
|
|
+
|
|
|
+ 0x02 -> {
|
|
|
+ delay(noFingerSleepMs)
|
|
|
+ } // 无手指
|
|
|
+ else -> {
|
|
|
+ delay(200)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (t: Throwable) {
|
|
|
+ onError(t)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- 内部 --- //
|
|
|
+
|
|
|
+ private suspend fun getImageOnce(proto: ModbusProtocol): Int {
|
|
|
+ val frame = FpmPackets.cmd(address, FpmIns.GET_IMAGE)
|
|
|
+ val msg: CommMessage =
|
|
|
+ FpmPackets.toMessage("FPM_GET_IMAGE", frame, timeoutMs = 3000, gapMs = 30)
|
|
|
+ val rsp: CommMessage = proto.send(deviceId, msg)
|
|
|
+ val (ack, _) = FpmPackets.parseAck(rsp.payload, address)
|
|
|
+ return ack
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 发 UP_IMAGE,一直读到 PID=0x08(LAST)为止 */
|
|
|
+ private suspend fun upImageOnce(proto: ModbusProtocol): ByteArray {
|
|
|
+ framer.feed(ByteArray(0)) // 确保内部状态干净(可选)
|
|
|
+ val start = FpmPackets.cmd(address, FpmIns.UP_IMAGE)
|
|
|
+ val plan = TxPlan(
|
|
|
+ listOf(
|
|
|
+ FpmPackets.toMessage(
|
|
|
+ "FPM_UP_IMAGE",
|
|
|
+ start,
|
|
|
+ timeoutMs = 15000,
|
|
|
+ gapMs = 60
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+
|
|
|
+ val chunks = ArrayList<ByteArray>(256)
|
|
|
+ var isLast = false
|
|
|
+ val policy = object : ChainPolicy {
|
|
|
+ override suspend fun afterSendStep(
|
|
|
+ stepIndex: Int,
|
|
|
+ sent: CommMessage,
|
|
|
+ io: LinkIO
|
|
|
+ ): ChainStepResult {
|
|
|
+ val deadlineNs = System.nanoTime() + 15_000_000_000L
|
|
|
+ while (System.nanoTime() < deadlineNs) {
|
|
|
+ val rsp = try {
|
|
|
+ io.readRaw(timeoutMs = 600, expectedSize = null, silenceGapMs = 8)
|
|
|
+ } catch (e: Exception) {
|
|
|
+ if (isLast) return ChainStepResult(emptyList(), false)
|
|
|
+ delay(10)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ log.debug("readRaw(): got {}B", rsp.payload.size)
|
|
|
+ val frames = framer.feed(rsp.payload)
|
|
|
+ for (f in frames) {
|
|
|
+ when (f.getOrNull(6)) {
|
|
|
+ FpmPackets.PID_ACK -> { /* 可读确认码;不强制 */
|
|
|
+ }
|
|
|
+
|
|
|
+ FpmPackets.PID_DATA -> {
|
|
|
+ chunks += FpmPackets.parseDataLike(f)
|
|
|
+ }
|
|
|
+
|
|
|
+ FpmPackets.PID_LAST -> {
|
|
|
+ chunks += FpmPackets.parseDataLike(f)
|
|
|
+ isLast = true
|
|
|
+ return ChainStepResult(emptyList(), false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (!isLast) error("UP_IMAGE not finished (no LAST) within deadline")
|
|
|
+ return ChainStepResult(emptyList(), false)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ proto.sendChain(deviceId, plan, policy)
|
|
|
+
|
|
|
+ val total = chunks.sumOf { it.size }
|
|
|
+ val out = ByteArray(total)
|
|
|
+ var pos = 0
|
|
|
+ for (c in chunks) {
|
|
|
+ System.arraycopy(c, 0, out, pos, c.size); pos += c.size
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ }
|
|
|
+
|
|
|
+ // === 解码自适应 ===
|
|
|
+
|
|
|
+ private enum class NibbleOrder { HI_LO, LO_HI }
|
|
|
+
|
|
|
+ // 选顺序 + 分辨率时多打一些 info 日志
|
|
|
+ private fun decodeAuto(raw4b: ByteArray): Triple<ByteArray, Int, Int> {
|
|
|
+ log.info("decodeAuto(): raw4b={}B", raw4b.size)
|
|
|
+
|
|
|
+ // HI→LO
|
|
|
+ val g1 = unpack4bitTo8bit(raw4b, NibbleOrder.HI_LO)
|
|
|
+ val (w1, h1) = pickSize(g1.size, preferW = 256)
|
|
|
+ val s1 = rowEnergy(g1, w1, h1)
|
|
|
+ log.info(
|
|
|
+ "decodeAuto(): optionA HI→LO gray8={}px size={}x{} energy={}",
|
|
|
+ g1.size,
|
|
|
+ w1,
|
|
|
+ h1,
|
|
|
+ s1
|
|
|
+ )
|
|
|
+
|
|
|
+ // LO→HI
|
|
|
+ val g2 = unpack4bitTo8bit(raw4b, NibbleOrder.LO_HI)
|
|
|
+ val (w2, h2) = pickSize(g2.size, preferW = 256)
|
|
|
+ val s2 = rowEnergy(g2, w2, h2)
|
|
|
+ log.info(
|
|
|
+ "decodeAuto(): optionB LO→HI gray8={}px size={}x{} energy={}",
|
|
|
+ g2.size,
|
|
|
+ w2,
|
|
|
+ h2,
|
|
|
+ s2
|
|
|
+ )
|
|
|
+
|
|
|
+ val takeB = s2 > s1
|
|
|
+ log.info(
|
|
|
+ "decodeAuto(): choose {} based on higher row energy",
|
|
|
+ if (takeB) "LO→HI" else "HI→LO"
|
|
|
+ )
|
|
|
+
|
|
|
+ return if (takeB) Triple(g2, w2, h2) else Triple(g1, w1, h1)
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 行能量:从第二行开始算,避免 prev 为负下标 */
|
|
|
+ private fun rowEnergy(gray: ByteArray, w: Int, h: Int): Long {
|
|
|
+ if (w <= 0 || h <= 0 || gray.size < w * h) return 0L
|
|
|
+ var e = 0L
|
|
|
+ var base = w // 第一行的起始下标是 0,这里从“第二行”开始
|
|
|
+ for (y in 1 until h) {
|
|
|
+ val prev = base - w // 上一行的起始
|
|
|
+ var x = 0
|
|
|
+ while (x < w) {
|
|
|
+ val a = gray[base + x].toInt() and 0xFF
|
|
|
+ val b = gray[prev + x].toInt() and 0xFF
|
|
|
+ e += kotlin.math.abs(a - b)
|
|
|
+ x++
|
|
|
+ }
|
|
|
+ base += w
|
|
|
+ }
|
|
|
+ return e
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 依据像素数选择尺寸:优先常见表,再退 256×H */
|
|
|
+ private fun pickSize(pixels: Int, preferW: Int = 256): Pair<Int, Int> {
|
|
|
+ val common = listOf(256 to 288, 256 to 282, 240 to 320, 192 to 192, 208 to 208, 128 to 128)
|
|
|
+ common.firstOrNull { it.first * it.second == pixels }?.let { return it }
|
|
|
+ if (pixels % preferW == 0) return preferW to (pixels / preferW)
|
|
|
+ // 最后兜底:尽量找一个接近的 H
|
|
|
+ for (w in listOf(256, 240, 192, 208, 128)) {
|
|
|
+ if (pixels % w == 0) return w to (pixels / w)
|
|
|
+ }
|
|
|
+ error("无法匹配图像尺寸:像素=$pixels")
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4bit → 8bit(支持两种半字节顺序)
|
|
|
+ private fun unpack4bitTo8bit(raw: ByteArray, order: NibbleOrder): ByteArray {
|
|
|
+ val out = ByteArray(raw.size * 2)
|
|
|
+ var j = 0
|
|
|
+ when (order) {
|
|
|
+ NibbleOrder.HI_LO -> {
|
|
|
+ for (b in raw) {
|
|
|
+ val hi = (b.toInt() ushr 4) and 0x0F
|
|
|
+ val lo = b.toInt() and 0x0F
|
|
|
+ out[j++] = (hi * 17).toByte()
|
|
|
+ out[j++] = (lo * 17).toByte()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ NibbleOrder.LO_HI -> {
|
|
|
+ for (b in raw) {
|
|
|
+ val lo = b.toInt() and 0x0F
|
|
|
+ val hi = (b.toInt() ushr 4) and 0x0F
|
|
|
+ out[j++] = (lo * 17).toByte()
|
|
|
+ out[j++] = (hi * 17).toByte()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return out
|
|
|
+ }
|
|
|
+
|
|
|
+ // 8bit 灰度 → PNG(Base64)
|
|
|
+ private fun gray8ToPngBase64(gray: ByteArray, width: Int, height: Int): String {
|
|
|
+ val bmp = android.graphics.Bitmap.createBitmap(
|
|
|
+ width,
|
|
|
+ height,
|
|
|
+ android.graphics.Bitmap.Config.ARGB_8888
|
|
|
+ )
|
|
|
+ val colors = IntArray(width * height)
|
|
|
+ for (i in colors.indices) {
|
|
|
+ val g = gray[i].toInt() and 0xFF
|
|
|
+ colors[i] = (0xFF shl 24) or (g shl 16) or (g shl 8) or g
|
|
|
+ }
|
|
|
+ bmp.setPixels(colors, 0, width, 0, 0, width, height)
|
|
|
+ val baos = java.io.ByteArrayOutputStream()
|
|
|
+ bmp.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, baos)
|
|
|
+ val png = baos.toByteArray()
|
|
|
+ return Base64.encodeToString(png, Base64.NO_WRAP)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/** 指令码 */
|
|
|
+object FpmIns {
|
|
|
+ const val GET_IMAGE: Byte = 0x01
|
|
|
+ const val UP_IMAGE: Byte = 0x0A
|
|
|
+}
|