Эх сурвалжийг харах

feat(指纹):
- 新增对串口指纹模块(`FPM`系列)的支持,通过`FingerprintCaptureService`实现。
- 自动探测并适配波特率(57600/115200),并可自动提速至115200以优化性能。
- 实现指纹图像的采集、传输和解码逻辑,包括对半字节顺序和图像尺寸的自适应解码。
- `FingerprintUtil`中集成新的`FingerprintCaptureService`,与原有中控(ZK)指纹模块并行支持,并通过`isZKDevice`字段区分。

feat(用户):
- 在新增/编辑用户弹窗中,增加刷卡录入卡号的功能 (`SwipCardOperationTipDialog`)。
- 通过`InRFIDScanModeEvent`事件控制全局的RFID扫描模式,防止在非输入卡号场景下,刷卡行为被错误响应。

refactor(指纹):
- 在指纹扫描流程中增加`onStart`回调,用于在指纹模块开始采集图像时显示“正在获取指纹”提示,优化用户交互体验。
- 对于中控(ZK)指纹设备,仅在`onScan`回调触发(即获取到图像)时才显示加载提示,避免在`onStart`时过早显示。

refactor(人脸识别):
- `ArcSoftUtil`: 优化相机初始化逻辑,改为动态查找前置或后置摄像头ID,而不是依赖固定的`CAMERA_FACING_BACK`,并根据上下文获取正确的屏幕旋转角度,提高了相机启动的稳定性和预览的正确性。

refactor(登录):
- `LoginActivity`: 优化刷卡登录逻辑,通过`inAccountLogin`标志位判断,仅在非账户登录弹窗激活时才处理刷卡事件,避免在输入账号密码时键盘模拟的刷卡器干扰输入。

fix(UI):
- `fragment_set_face.xml`: 修复人脸录入界面`preview_layout`高度为`0dp`导致预览画面无法显示的问题,调整为`match_parent`。

chore(依赖):
- 升级通信库`sik-comm`版本从`1.0.13`至`1.0.17`。

chore(日志):
- `ISCSApplication`, `ISCSMCApplication`: 调整日志初始化时机,将其提前至`onCreate`方法的开头,确保所有组件都能尽早使用日志。

chore(代码):
- `SwipCardOperationTipDialog`: 修正类注释,从“检查人脸弹窗”更正为“刷卡弹窗”。
- 新增`get_fingerprint`字符串资源。

周文健 3 долоо хоног өмнө
parent
commit
f9e5a1d393
31 өөрчлөгдсөн 1174 нэмэгдсэн , 69 устгасан
  1. 5 0
      data/src/main/java/com/grkj/data/data/EventConstants.kt
  2. 47 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt
  3. 78 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt
  4. 85 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt
  5. 482 0
      data/src/main/java/com/grkj/data/hardware/fingerprint/FingerprintCaptureService.kt
  6. 38 0
      data/src/main/java/com/grkj/data/hardware/fingerprint/FpmFramer.kt
  7. 26 0
      data/src/main/java/com/grkj/data/hardware/fingerprint/FpmGlobals.kt
  8. 130 0
      data/src/main/java/com/grkj/data/hardware/fingerprint/FpmPackets.kt
  9. 1 1
      gradle/libs.versions.toml
  10. 2 2
      iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt
  11. 58 29
      iscs_lock/src/main/java/com/grkj/iscs/features/login/activity/LoginActivity.kt
  12. 12 4
      iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt
  13. 31 13
      iscs_lock/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt
  14. 10 2
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt
  15. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/SwipCardOperationTipDialog.kt
  16. 21 3
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt
  17. 16 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt
  18. 7 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt
  19. 7 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt
  20. 1 1
      iscs_lock/src/main/res/layout/fragment_set_face.xml
  21. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  22. 11 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt
  23. 11 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt
  24. 7 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/data_manage/UserManageFragment.kt
  25. 7 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFingerprintFragment.kt
  26. 33 7
      shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt
  27. 25 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/InRFIDScanModeEvent.kt
  28. 17 0
      ui-base/src/main/java/com/grkj/ui_base/utils/fingerprint/FingerprintUtil.kt
  29. 1 0
      ui-base/src/main/res/values-en/strings.xml
  30. 1 0
      ui-base/src/main/res/values-zh/strings.xml
  31. 1 0
      ui-base/src/main/res/values/strings.xml

+ 5 - 0
data/src/main/java/com/grkj/data/data/EventConstants.kt

@@ -83,6 +83,11 @@ object EventConstants {
      */
     const val EVENT_BOTTOM_TIP: Int = 100_000_016
 
+    /**
+     * 进入RFID扫描模式
+     */
+    const val EVENT_IN_RFID_SCAN_MODE: Int = 100_000_017
+
     //---------------------------作业票------------------------
     const val EVENT_GET_TICKET_STATUS: Int = 100_001_001
 

+ 47 - 0
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt

@@ -0,0 +1,47 @@
+package com.grkj.data.hardware.face.hlk
+
+import com.sik.comm.core.model.CommMessage
+import com.sik.comm.impl_modbus.ModbusProtocol
+
+/**
+ * HLK-FM223 常用命令的“人话”封装:单帧往返。
+ * 传输层复用 ModbusProtocol + PassThroughCodec(直通)。
+ */
+class Hlk223Client(
+    private val protocol: ModbusProtocol,
+    private val deviceId: String
+) {
+    /** 基础收发:req -> rsp(payload 为完整 HLK 帧),再本地解析 */
+    private suspend fun exchange(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = 3000): Pair<Int, ByteArray> {
+        val req: CommMessage = Hlk223.msg(mid, data, timeoutMs)
+        val rsp: CommMessage = protocol.send(deviceId, req)
+        return Hlk223.parse(rsp.payload)
+    }
+
+    suspend fun reset() {
+        val (mid, data) = exchange(Hlk223.MID.RESET)
+        require(mid == Hlk223.MID.REPLY && data.size >= 2)
+        val result = data[1].toInt() and 0xFF
+        require(result == 0) { "RESET failed, result=$result" }
+    }
+
+    suspend fun getStatus(): Int {
+        val (mid, data) = exchange(Hlk223.MID.GET_STATUS)
+        require(mid == Hlk223.MID.REPLY && data.size >= 3)
+        return data[2].toInt() and 0xFF // IDLE/BUSY/ERROR/...
+    }
+
+    suspend fun getVersion(): String {
+        val (mid, data) = exchange(Hlk223.MID.GET_VERSION)
+        require(mid == Hlk223.MID.REPLY && data.size >= 2)
+        return data.copyOfRange(2, data.size).toString(Charsets.US_ASCII)
+    }
+
+    suspend fun getSn(): String {
+        val (mid, data) = exchange(Hlk223.MID.GET_SN)
+        require(mid == Hlk223.MID.REPLY && data.size >= 2)
+        return data.copyOfRange(2, data.size).toString(Charsets.US_ASCII).trim('\u0000')
+    }
+
+    // TODO: 可继续补充 VERIFY/ENROLL/DEL/GET_USER_INFO... 的高层封装
+}

+ 78 - 0
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt

@@ -0,0 +1,78 @@
+package com.grkj.data.hardware.face.hlk
+
+import com.sik.comm.core.model.CommMessage
+
+/**
+ * HLK-FM223 帧工具(组帧/验帧/转 CommMessage)。
+ * 帧: [EF AA][MID][LEN_H][LEN_L][DATA...][XOR]
+ * XOR = 从 MID 到 DATA 末字节的逐字节异或。
+ */
+object Hlk223 {
+    const val SYNC_H: Byte = 0xEF.toByte()
+    const val SYNC_L: Byte = 0xAA.toByte()
+
+    object MID {
+        const val RESET              = 0x10
+        const val GET_STATUS         = 0x11
+        const val VERIFY             = 0x12
+        const val ENROLL             = 0x13
+        const val ENROLL_SINGLE      = 0x1D
+        const val DEL_USER           = 0x20
+        const val DEL_ALL            = 0x21
+        const val GET_USER_INFO      = 0x22
+        const val FACE_RESET         = 0x23
+        const val GET_ALL_USERID     = 0x24
+        const val ENROLL_ITG         = 0x26
+        const val GET_VERSION        = 0x30
+        const val GET_SN             = 0x93
+        const val READ_USB_UVC_PARAM = 0xB0
+        const val SET_USB_UVC_PARAM  = 0xB1
+        const val UPGRADE_FW         = 0xF6
+        const val ENROLL_WITH_PHOTO  = 0xF7
+        const val DEMO_MODE          = 0xFE
+
+        /** 下行常见应答/上报(模组→主机) */
+        const val REPLY              = 0x00
+        const val NOTE               = 0x01
+        const val IMAGE              = 0x02
+    }
+
+    /** 组帧:从 MID + DATA 得到完整二进制帧 */
+    fun build(mid: Int, data: ByteArray = byteArrayOf()): ByteArray {
+        require(mid in 0..0xFF) { "MsgID out of range" }
+        val sizeH = ((data.size ushr 8) and 0xFF).toByte()
+        val sizeL = (data.size and 0xFF).toByte()
+        val head = byteArrayOf(SYNC_H, SYNC_L, mid.toByte(), sizeH, sizeL)
+        val body = head + data
+        val parity = body.drop(2).fold(0) { acc, b -> acc xor (b.toInt() and 0xFF) }.toByte()
+        return body + parity
+    }
+
+    /** 解析 & 校验:返回 (mid, data) */
+    fun parse(frame: ByteArray): Pair<Int, ByteArray> {
+        require(frame.size >= 6) { "Frame too short" }
+        require(frame[0] == SYNC_H && frame[1] == SYNC_L) { "Bad sync" }
+        val mid  = frame[2].toInt() and 0xFF
+        val size = ((frame[3].toInt() and 0xFF) shl 8) or (frame[4].toInt() and 0xFF)
+        require(frame.size == 5 + size + 1) { "Size mismatch: expect=$size actual=${frame.size - 6}" }
+        val calc = frame.copyOf(frame.size - 1).drop(2).fold(0) { acc, b -> acc xor (b.toInt() and 0xFF) } and 0xFF
+        val got  = frame.last().toInt() and 0xFF
+        require(calc == got) { "XOR mismatch: got=0x${got.toString(16)} expect=0x${calc.toString(16)}" }
+        return mid to frame.copyOfRange(5, 5 + size)
+    }
+
+    /** 包装为 CommMessage(交给协议层发送;超时/静默间隔可透传 metadata) */
+    fun msg(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = null, gapMs: Int? = null): CommMessage {
+        val frame = build(mid, data)
+        val meta = buildMap<String, Any> {
+            timeoutMs?.let { put("timeoutMs", it) }   // ModbusProtocol 会读取并覆写本次等待
+            gapMs?.let     { put("silenceGapMs", it) }
+            put("rawFrame", frame)                    // 方便日志/调试
+        }
+        return CommMessage(
+            command = "HLK_MID_${mid.toString(16).uppercase().padStart(2, '0')}",
+            payload = frame,
+            metadata = meta
+        )
+    }
+}

+ 85 - 0
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt

@@ -0,0 +1,85 @@
+package com.grkj.data.hardware.face.hlk
+
+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 com.sik.comm.impl_modbus.ModbusProtocol
+import kotlin.math.min
+
+/**
+ * HLK 照片下发注册(分包,步步等待 REPLY)。
+ * - 首包: Seq=0, data = len(4,BE) + bioType(1) + crc32(4,BE)
+ * - 后续: Seq=1..N, 每包 <= 246B
+ */
+class Hlk223PhotoEnroll(
+    private val protocol: ModbusProtocol,
+    private val deviceId: String
+) {
+    private fun chunk(bytes: ByteArray, mtu: Int = 246): List<ByteArray> {
+        val out = mutableListOf<ByteArray>()
+        var i = 0
+        while (i < bytes.size) {
+            val end = min(i + mtu, bytes.size)
+            out += bytes.copyOfRange(i, end)
+            i = end
+        }
+        return out
+    }
+
+    private fun be32(v: Int) = byteArrayOf(
+        ((v ushr 24) and 0xFF).toByte(),
+        ((v ushr 16) and 0xFF).toByte(),
+        ((v ushr 8) and 0xFF).toByte(),
+        (v and 0xFF).toByte()
+    )
+
+    private fun firstData(photoLen: Int, bioType: Int, crc32: Int): ByteArray {
+        // 文档对首字节是否保留位各版本略有差异,如需对齐可在此调整
+        return byteArrayOf(0x00) + be32(photoLen) + byteArrayOf(bioType.toByte()) + be32(crc32)
+    }
+
+    private fun buildPlan(photo: ByteArray, bioType: Int, crc32: Int): TxPlan {
+        val frames = mutableListOf<CommMessage>()
+        // Seq=0
+        frames += Hlk223.msg(Hlk223.MID.ENROLL_WITH_PHOTO, firstData(photo.size, bioType, crc32))
+        // Seq=1..N
+        val parts = chunk(photo)
+        parts.forEachIndexed { i, part ->
+            val seq = i + 1
+            val seqBytes = byteArrayOf(((seq ushr 8) and 0xFF).toByte(), (seq and 0xFF).toByte())
+            frames += Hlk223.msg(Hlk223.MID.ENROLL_WITH_PHOTO, seqBytes + part)
+        }
+        return TxPlan(frames = frames)
+    }
+
+    private fun policy(): ChainPolicy = object : ChainPolicy {
+        override suspend fun afterSendStep(stepIndex: Int, sent: CommMessage, io: LinkIO): ChainStepResult {
+            val ack = io.readRaw(timeoutMs = 3000, expectedSize = null, silenceGapMs = 30)
+            val (mid, data) = Hlk223.parse(ack.payload)
+            require(mid == Hlk223.MID.REPLY && data.size >= 2) { "Expect REPLY" }
+            val reqMid = data[0].toInt() and 0xFF
+            val result = data[1].toInt() and 0xFF
+            require(reqMid == Hlk223.MID.ENROLL_WITH_PHOTO) { "Ack MID mismatch" }
+            require(result == 0) { "Step=$stepIndex failed, result=$result" }
+            // 如需处理 NOTE,可在这里循环 readRaw 两次:先 NOTE 再 REPLY。
+            return ChainStepResult(received = listOf(ack), continueNext = true, interFrameDelayMs = 10)
+        }
+    }
+
+    /** 执行注册,返回 userId(末包 REPLY 携带) */
+    suspend fun enroll(photoJpg: ByteArray, bioType: Int = 1, crc32: Int): Int {
+        val rsps = protocol.sendChain(deviceId, buildPlan(photoJpg, bioType, crc32), policy())
+        val last = rsps.lastOrNull() ?: error("No reply")
+        val (_, data) = Hlk223.parse(last.payload)
+        // 按常见格式:REPLY data 尾部 2 字节为 userId(BE),若文档版本不同请对齐偏移
+        val uid = if (data.size >= 2) {
+            val hi = data[data.size - 2].toInt() and 0xFF
+            val lo = data[data.size - 1].toInt() and 0xFF
+            (hi shl 8) or lo
+        } else 0
+        require(uid > 0) { "Enroll success but userId missing" }
+        return uid
+    }
+}

+ 482 - 0
data/src/main/java/com/grkj/data/hardware/fingerprint/FingerprintCaptureService.kt

@@ -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
+}

+ 38 - 0
data/src/main/java/com/grkj/data/hardware/fingerprint/FpmFramer.kt

@@ -0,0 +1,38 @@
+package com.grkj.data.hardware.fingerprint
+
+/**
+ * EF01 协议增量式帧解包器:
+ * - 可处理:多帧粘连 / 半帧残留 / 噪声丢弃
+ * - 帧: EF 01 | addr(4) | pid(1) | len(2) | body(len-2) | sum(2)
+ */
+class FpmFramer {
+    private var buf = ByteArray(0)
+
+    /** 喂入一段原始串口数据,返回尽可能多的完整帧(每帧含 EF01 头) */
+    fun feed(chunk: ByteArray): List<ByteArray> {
+        if (chunk.isEmpty()) return emptyList()
+        buf += chunk
+        val out = mutableListOf<ByteArray>()
+        var i = 0
+        while (true) {
+            // 找同步字
+            while (i + 2 <= buf.size && (buf[i] != 0xEF.toByte() || buf[i + 1] != 0x01.toByte())) i++
+            if (i + 9 > buf.size) break // 不足以读 pid+len
+            val pid = buf[i + 6]
+            val len = ((buf[i + 7].toInt() and 0xFF) shl 8) or (buf[i + 8].toInt() and 0xFF)
+            val frameLen = 9 + len
+            if (len <= 0 || frameLen <= 0) { i += 2; continue } // 防御
+            if (i + frameLen > buf.size) break // 半帧:等下次喂
+
+            val frame = buf.copyOfRange(i, i + frameLen)
+            // 仅接受我们关心的三类帧
+            if (pid == FpmPackets.PID_ACK || pid == FpmPackets.PID_DATA || pid == FpmPackets.PID_LAST) {
+                out += frame
+            }
+            i += frameLen
+        }
+        // 压缩缓存,只保留未完成的尾巴
+        buf = if (i >= buf.size) ByteArray(0) else buf.copyOfRange(i, buf.size)
+        return out
+    }
+}

+ 26 - 0
data/src/main/java/com/grkj/data/hardware/fingerprint/FpmGlobals.kt

@@ -0,0 +1,26 @@
+package com.grkj.data.hardware.fingerprint
+
+/** 运行期全局缓存(线程安全简单场景可用) */
+object FpmGlobals {
+    data class LinkParams(
+        var baud: Int? = null,      // 当前有效波特率(如 115200)
+        var pktSizeBytes: Int? = null, // 实际包字节(32/64/128/256)
+        var delayTimeMs: Int? = null   // 包间隙 (0–255ms)
+    )
+    data class ImageInfo(
+        var width: Int? = null,
+        var height: Int? = null,
+        var imageFormat: Int? = null  // 文档 ImageFormat:0 原始/1 预处理(如后续需要可扩)
+    )
+    @Volatile var link = LinkParams()
+    @Volatile var image = ImageInfo()
+
+    fun rememberLink(baud: Int? = null, pktBytes: Int? = null, delayMs: Int? = null) {
+        baud?.let { link.baud = it }
+        pktBytes?.let { link.pktSizeBytes = it }
+        delayMs?.let { link.delayTimeMs = it }
+    }
+    fun rememberImage(w: Int? = null, h: Int? = null, fmt: Int? = null) {
+        w?.let { image.width = it }; h?.let { image.height = it }; fmt?.let { image.imageFormat = it }
+    }
+}

+ 130 - 0
data/src/main/java/com/grkj/data/hardware/fingerprint/FpmPackets.kt

@@ -0,0 +1,130 @@
+package com.grkj.data.hardware.fingerprint
+
+import org.slf4j.LoggerFactory
+import com.sik.comm.core.model.CommMessage
+
+object FpmPackets {
+    private val log = LoggerFactory.getLogger("FPM.Packets")
+
+    const val HDR_H: Byte = 0xEF.toByte()
+    const val HDR_L: Byte = 0x01.toByte()
+    const val PID_CMD: Byte = 0x01
+    const val PID_DATA: Byte = 0x02
+    const val PID_LAST: Byte = 0x08
+    const val PID_ACK:  Byte = 0x07
+    const val DEFAULT_ADDR: Int = 0xFFFF_FFFF.toInt()
+
+    fun be16(x: Int) = byteArrayOf(((x ushr 8) and 0xFF).toByte(), (x and 0xFF).toByte())
+
+    private fun addrBytes(addr: Int) = byteArrayOf(
+        ((addr ushr 24) and 0xFF).toByte(),
+        ((addr ushr 16) and 0xFF).toByte(),
+        ((addr ushr 8) and 0xFF).toByte(),
+        (addr and 0xFF).toByte()
+    )
+
+    /** 计算校验和:从 pid 开始,**包含 LEN_H、LEN_L**,再到数据末尾;取低 2 字节(大端放入) */
+    private fun checksum(pid: Byte, lenHi: Byte, lenLo: Byte, body: ByteArray): ByteArray {
+        var sum = 0
+        sum = (sum + (pid.toInt() and 0xFF)) and 0xFFFF
+        sum = (sum + (lenHi.toInt() and 0xFF)) and 0xFFFF
+        sum = (sum + (lenLo.toInt() and 0xFF)) and 0xFFFF
+        for (b in body) sum = (sum + (b.toInt() and 0xFF)) and 0xFFFF
+        return be16(sum)
+    }
+
+    /** 封帧:EF01 | 地址4B | pid | LEN(2B) | body | SUM(2B),其中 LEN = body.size + 2 */
+    private fun frame(addr: Int, pid: Byte, body: ByteArray): ByteArray {
+        val len = body.size + 2
+        val lenHi = ((len ushr 8) and 0xFF).toByte()
+        val lenLo = (len and 0xFF).toByte()
+        val cks = checksum(pid, lenHi, lenLo, body)
+        val out = byteArrayOf(HDR_H, HDR_L) + addrBytes(addr) + byteArrayOf(pid, lenHi, lenLo) + body + cks
+        if (log.isTraceEnabled) {
+            log.trace(
+                "build frame pid=0x{} len={} body={} sum={}",
+                String.format("%02X", pid),
+                out.size,
+                body.hex(max = 64),
+                String.format("%02X%02X", cks[0], cks[1])
+            )
+        }
+        return out
+    }
+
+    /** 命令帧:body = [INS][PARAMS] */
+    fun cmd(addr: Int = DEFAULT_ADDR, ins: Byte, params: ByteArray = byteArrayOf()): ByteArray {
+        val body = byteArrayOf(ins) + params
+        return frame(addr, PID_CMD, body)
+    }
+
+    fun data(addr: Int = DEFAULT_ADDR, chunk: ByteArray): ByteArray =
+        frame(addr, PID_DATA, chunk)
+
+    fun last(addr: Int = DEFAULT_ADDR, chunk: ByteArray): ByteArray =
+        frame(addr, PID_LAST, chunk)
+
+    /** 解析 ACK:校验和按(pid+LEN_H+LEN_L+payload)验证 */
+    fun parseAck(payload: ByteArray, expectAddr: Int = DEFAULT_ADDR): Pair<Int, ByteArray> {
+        require(payload.size >= 9) { "ACK too short" }
+        require(payload[0] == HDR_H && payload[1] == HDR_L) { "Bad header" }
+        // 可选校验地址(如需严格一致可比对 expectAddr)
+        val pid = payload[6]; require(pid == PID_ACK) { "Not ACK" }
+        val lenHi = payload[7]; val lenLo = payload[8]
+        val len = ((lenHi.toInt() and 0xFF) shl 8) or (lenLo.toInt() and 0xFF)
+        require(payload.size == 9 + len) { "Len mismatch" }
+
+        val contentWithSum = payload.copyOfRange(9, payload.size) // [ack | ret... | sum(2)]
+        require(contentWithSum.size >= 3) { "ACK content short" }
+        val body = contentWithSum.copyOfRange(0, contentWithSum.size - 2) // [ack | ret...]
+        val sum = contentWithSum.copyOfRange(contentWithSum.size - 2, contentWithSum.size)
+
+        val calc = checksum(pid, lenHi, lenLo, body)
+        require(calc[0] == sum[0] && calc[1] == sum[1]) { "Checksum mismatch" }
+
+        val ackCode = body[0].toInt() and 0xFF
+        val ret = body.copyOfRange(1, body.size)
+        if (log.isDebugEnabled) log.debug("ACK: code=0x{} ret={}", String.format("%02X", ackCode), ret.hex(max = 64))
+        return ackCode to ret
+    }
+
+    /** 解析数据/最后包:同样按(pid+LEN_H+LEN_L+data)验和,返回纯 data */
+    fun parseDataLike(payload: ByteArray): ByteArray {
+        require(payload.size >= 9) { "DATA too short" }
+        require(payload[0] == HDR_H && payload[1] == HDR_L) { "Bad header" }
+        val pid = payload[6]
+        require(pid == PID_DATA || pid == PID_LAST) { "Not DATA/LAST" }
+        val lenHi = payload[7]; val lenLo = payload[8]
+        val len = ((lenHi.toInt() and 0xFF) shl 8) or (lenLo.toInt() and 0xFF)
+        require(payload.size == 9 + len) { "Len mismatch" }
+
+        val bodyWithSum = payload.copyOfRange(9, payload.size) // [data... | sum(2)]
+        require(bodyWithSum.size >= 2) { "DATA content short" }
+        val body = bodyWithSum.copyOfRange(0, bodyWithSum.size - 2)
+        val sum  = bodyWithSum.copyOfRange(bodyWithSum.size - 2, bodyWithSum.size)
+
+        val calc = checksum(pid, lenHi, lenLo, body)
+        require(calc[0] == sum[0] && calc[1] == sum[1]) { "Checksum mismatch" }
+
+        if (log.isTraceEnabled) log.trace("DATA chunk len={} pid=0x{}", body.size, String.format("%02X", pid))
+        return body
+    }
+
+    fun toMessage(tag: String, frame: ByteArray, timeoutMs: Int? = 3000, gapMs: Int? = 30): CommMessage {
+        val meta = buildMap<String, Any> {
+            timeoutMs?.let { put("timeoutMs", it) }
+            gapMs?.let     { put("silenceGapMs", it) }
+            put("rawFrame", frame)
+        }
+        return CommMessage(tag, frame, meta)
+    }
+}
+
+/** 简易十六进制输出(限长) */
+internal fun ByteArray.hex(max: Int = 256): String {
+    val n = kotlin.math.min(max, this.size)
+    val sb = StringBuilder(n * 2)
+    for (i in 0 until n) sb.append(String.format("%02X", this[i])).append(' ')
+    if (n < this.size) sb.append("…(+").append(this.size - n).append(')')
+    return sb.toString().trim()
+}

+ 1 - 1
gradle/libs.versions.toml

@@ -24,7 +24,7 @@ ksp = "2.1.10-1.0.31"
 nav_version = "2.9.0"
 kotlin_serialization_json = "1.7.3"
 fastble = "1.4.2"
-sikcomm = "1.0.13"
+sikcomm = "1.0.17"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

+ 2 - 2
iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -52,13 +52,14 @@ import java.io.File
  */
 @HiltAndroidApp
 class ISCSApplication : Application() {
-    private val logger: Logger = LoggerFactory.getLogger(ISCSApplication::class.java)
+    private val logger: Logger by lazy { LoggerFactory.getLogger(ISCSApplication::class.java) }
 
     /**
      * 程序创建
      */
     override fun onCreate() {
         super.onCreate()
+        initLogger()
         DialogX.init(this)
         SIKCore.init(this)
         if (!EventBus.getDefault().isRegistered(this)) {
@@ -68,7 +69,6 @@ class ISCSApplication : Application() {
             logger.error("异常发生", it)
             true
         }
-        initLogger()
         System.loadLibrary("sqlcipher")   // 新库必须手动加载
         MMKV.initialize(this)
         I18nManager.attach(this)

+ 58 - 29
iscs_lock/src/main/java/com/grkj/iscs/features/login/activity/LoginActivity.kt

@@ -46,6 +46,8 @@ import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.ui_base.utils.extension.getAppVersionName
 import com.grkj.ui_base.utils.extension.serialNo
 import com.grkj.ui_base.utils.fingerprint.FingerprintUtil
+import com.kongzue.dialogx.dialogs.CustomDialog
+import com.kongzue.dialogx.interfaces.DialogLifecycleCallback
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.shell.ShellUtils
 import com.sik.sikimage.ImageConvertUtils
@@ -58,6 +60,7 @@ import java.util.Locale
 @AndroidEntryPoint
 class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     private val viewModel: LoginViewModel by viewModels()
+    private var inAccountLogin = false
 
     //登录方式列表
     private val loginMenuList: MutableList<LoginMenuEntity> = mutableListOf()
@@ -129,6 +132,9 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
         )
         itemBinding.loginContainer.changeBgTint(item.menuBg)
         holder.itemView.setDebouncedClickListener {
+            if (item.loginType == 3) {
+                inAccountLogin = true
+            }
             LoginDialog.show(this@LoginActivity, viewModel, item.loginType) {
                 LoadingEvent.sendLoadingEvent()
                 when (it) {
@@ -178,7 +184,14 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
                         CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_login_failed)
                     )
                 }
-            }
+            }.setDialogLifecycleCallback(object : DialogLifecycleCallback<CustomDialog>() {
+                override fun onDismiss(dialog: CustomDialog?) {
+                    super.onDismiss(dialog)
+                    if (item.loginType == 3) {
+                        inAccountLogin = false
+                    }
+                }
+            })
         }
     }
 
@@ -249,11 +262,19 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
         FingerprintUtil.init(this)
         FingerprintUtil.start()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
-            override fun onScan(bitmap: Bitmap) {
+            override fun onStart() {
                 LoadingEvent.sendLoadingEvent(
                     CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
                     true
                 )
+            }
+            override fun onScan(bitmap: Bitmap) {
+                if (FingerprintUtil.isZKDevice){
+                    LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
+                        true
+                    )
+                }
                 viewModel.loginWithFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap).toString()
                 ).observe(this@LoginActivity) { isSuccess ->
@@ -281,38 +302,46 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
         if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
             // 检测到回车开始处理
             if (event.keyCode == 66) {
-                logger.info("Swipe card login Origin: $cardNo")
-                try {
-                    cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
-                    logger.info("Swipe card login: $cardNo")
-                    LoadingEvent.sendLoadingEvent(
-                        CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
-                        true
-                    )
-                    viewModel.loginWithCard(cardNo).observe(this) { isSuccess ->
-                        LoadingEvent.sendLoadingEvent()
-                        if (isSuccess == LoginResultEnum.JOB_CARD_LOGIN_SUCCESS) {
-                            showToast(
-                                CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_login_success)
-                            )
-                            startActivity(Intent(this@LoginActivity, MainActivity::class.java))
-                            finish()
-                        } else {
-                            showToast(
-                                CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_login_failed)
-                            )
+                return if (inAccountLogin) {
+                    super.dispatchKeyEvent(event)
+                } else {
+                    logger.info("Swipe card login Origin: $cardNo")
+                    try {
+                        cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
+                        logger.info("Swipe card login: $cardNo")
+                        LoadingEvent.sendLoadingEvent(
+                            CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
+                            true
+                        )
+                        viewModel.loginWithCard(cardNo).observe(this) { isSuccess ->
+                            LoadingEvent.sendLoadingEvent()
+                            if (isSuccess == LoginResultEnum.JOB_CARD_LOGIN_SUCCESS) {
+                                showToast(
+                                    CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_login_success)
+                                )
+                                startActivity(Intent(this@LoginActivity, MainActivity::class.java))
+                                finish()
+                            } else {
+                                showToast(
+                                    CommonUtils.getStr(com.grkj.ui_base.R.string.job_card_login_failed)
+                                )
+                            }
                         }
+                        // 重置cardNo
+                        cardNo = ""
+                    } catch (e: Exception) {
+                        cardNo = ""
+                        logger.info("读卡失败: ${e.toString()}")
                     }
-                    // 重置cardNo
-                    cardNo = ""
-                } catch (e: Exception) {
-                    cardNo = ""
-                    logger.info("读卡失败: ${e.toString()}")
+                    true
                 }
-                return super.dispatchKeyEvent(event)
             }
             cardNo += event.keyCharacterMap.getDisplayLabel(event.keyCode)
         }
-        return super.dispatchKeyEvent(event)
+        return if (inAccountLogin) {
+            super.dispatchKeyEvent(event)
+        } else {
+            true
+        }
     }
 }

+ 12 - 4
iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt

@@ -126,11 +126,19 @@ class LoginDialog(
                     FingerprintUtil.init(SIKCore.getApplication())
                     FingerprintUtil.start()
                     FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
-                        override fun onScan(bitmap: Bitmap) {
+                        override fun onStart() {
                             LoadingEvent.sendLoadingEvent(
-                                CommonUtils.getStr("doing_login"),
+                                CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
                                 true
                             )
+                        }
+                        override fun onScan(bitmap: Bitmap) {
+                            if (FingerprintUtil.isZKDevice){
+                                LoadingEvent.sendLoadingEvent(
+                                    CommonUtils.getStr("doing_login"),
+                                    true
+                                )
+                            }
                             viewModel.loginWithFingerprint(
                                 ImageConvertUtils.bitmapToBase64(
                                     bitmap
@@ -163,8 +171,8 @@ class LoginDialog(
             viewModel: LoginViewModel,
             loginType: Int,
             callBack: ((LoginResultEnum) -> Unit)?
-        ) {
-            CustomDialog.show(LoginDialog(lifecycleOwner, viewModel, callBack).apply {
+        ): CustomDialog {
+            return CustomDialog.show(LoginDialog(lifecycleOwner, viewModel, callBack).apply {
                 showByType(loginType)
             })
         }

+ 31 - 13
iscs_lock/src/main/java/com/grkj/iscs/features/main/activity/MainActivity.kt

@@ -30,6 +30,7 @@ import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.data.hardware.ble.BleSendDispatcher
 import com.grkj.ui_base.utils.event.FlashTipEvent
+import com.grkj.ui_base.utils.event.InRFIDScanModeEvent
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.extension.removeTint
 import com.sik.sikcore.extension.file
@@ -46,6 +47,11 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
     private val viewModel: MainViewModel by viewModels()
     private var cardNo: String = ""
     private var isFirstEnter: Boolean = true
+
+    /**
+     * RFID扫描模式
+     */
+    private var inRFIDScanMode = false
     private val tabConfigs = listOf(
         TabConfig(
             View.generateViewId(),
@@ -147,7 +153,7 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
             }
             binding.navBar.isVisible = bottomNavDestinations.contains(destination.id)
         }
-        viewModel.checkReturnKey().observe(this){}
+        viewModel.checkReturnKey().observe(this) {}
     }
 
     override fun onEvent(event: EventBean<Any>) {
@@ -165,6 +171,10 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
                 loadAvatar()
             }
 
+            EventConstants.EVENT_IN_RFID_SCAN_MODE -> {
+                inRFIDScanMode = (event.data as InRFIDScanModeEvent).inRfidScanMode
+            }
+
             EventConstants.EVENT_FLASH_TIP_CODE -> {
                 (event.data as FlashTipEvent).apply {
                     if (isShow) {
@@ -217,22 +227,30 @@ class MainActivity() : BaseActivity<ActivityMainBinding>() {
         if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
             // 检测到回车开始处理
             if (event.keyCode == 66) {
-                logger.info("Swipe card login Origin: $cardNo")
-                try {
-                    cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
-                    logger.info("Swipe card login: $cardNo")
-                    RFIDCardReadEvent.sendRFIDCardReadEvent(cardNo)
-                    // 重置cardNo
-                    cardNo = ""
-                } catch (e: Exception) {
-                    cardNo = ""
-                    logger.info("读卡失败: ${e.toString()}")
+                return if (inRFIDScanMode) {
+                    logger.info("Swipe card login Origin: $cardNo")
+                    try {
+                        cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
+                        logger.info("Swipe card login: $cardNo")
+                        RFIDCardReadEvent.sendRFIDCardReadEvent(cardNo)
+                        // 重置cardNo
+                        cardNo = ""
+                    } catch (e: Exception) {
+                        cardNo = ""
+                        logger.info("读卡失败: ${e.toString()}")
+                    }
+                    true
+                } else {
+                    super.dispatchKeyEvent(event)
                 }
-                return super.dispatchKeyEvent(event)
             }
             cardNo += event.keyCharacterMap.getDisplayLabel(event.keyCode)
         }
-        return super.dispatchKeyEvent(event)
+        return if (inRFIDScanMode) {
+            true
+        } else {
+            super.dispatchKeyEvent(event)
+        }
     }
 
     /**

+ 10 - 2
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt

@@ -99,10 +99,18 @@ class CheckFaceDialog(
                 FingerprintUtil.init(SIKCore.getApplication())
                 FingerprintUtil.start()
                 FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
-                    override fun onScan(bitmap: Bitmap) {
+                    override fun onStart() {
                         LoadingEvent.sendLoadingEvent(
-                            CommonUtils.getStr("doing_checking"), true
+                            CommonUtils.getStr(com.grkj.ui_base.R.string.doing_checking),
+                            true
                         )
+                    }
+                    override fun onScan(bitmap: Bitmap) {
+                        if (FingerprintUtil.isZKDevice){
+                            LoadingEvent.sendLoadingEvent(
+                                CommonUtils.getStr("doing_checking"), true
+                            )
+                        }
                         viewModel.checkFinger(
                             ImageConvertUtils.bitmapToBase64(bitmap).toString()
                         ).observe(lifecycleOwner) {

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/SwipCardOperationTipDialog.kt

@@ -10,7 +10,7 @@ import com.kongzue.dialogx.interfaces.OnBindView
 import com.sik.sikcore.extension.setDebouncedClickListener
 
 /**
- * 检查人脸弹窗
+ * 刷卡弹窗
  */
 class SwipCardOperationTipDialog() :
     OnBindView<CustomDialog>(R.layout.dialog_swip_card_operation_tip) {

+ 21 - 3
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/AddUserDialog.kt

@@ -15,12 +15,15 @@ import com.grkj.data.domain.vo.AddUserDataVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogAddUserBinding
 import com.grkj.iscs.databinding.ItemDeleteBtnBinding
+import com.grkj.iscs.features.main.dialog.SwipCardOperationTipDialog
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.event.InRFIDScanModeEvent
 import com.grkj.ui_base.utils.extension.tip
 import com.kongzue.dialogx.dialogs.CustomDialog
 import com.kongzue.dialogx.dialogs.PopTip
+import com.kongzue.dialogx.interfaces.DialogLifecycleCallback
 import com.kongzue.dialogx.interfaces.OnBindView
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.extension.deleteIfExists
@@ -47,6 +50,7 @@ class AddUserDialog(
     private lateinit var binding: DialogAddUserBinding
     private var selectedRoles: List<TextDropDownDialog.TextDropDownEntity> = emptyList()
     private var selectedWorkstations: List<TextDropDownDialog.TextDropDownEntity> = emptyList()
+    private var swipeCardDialog: CustomDialog? = null
     private var faceData: MutableList<String> =
         mutableListOf(CommonUtils.getStr(com.grkj.ui_base.R.string.register))
     private var fingerprintGroupData: MutableList<String> =
@@ -139,7 +143,8 @@ class AddUserDialog(
                         notifyDataSetChanged()
                     }
                 }
-                itemBinding.delete.isVisible = item != CommonUtils.getStr(com.grkj.ui_base.R.string.register)
+                itemBinding.delete.isVisible =
+                    item != CommonUtils.getStr(com.grkj.ui_base.R.string.register)
                 itemBinding.delete.setDebouncedClickListener {
                     item.deleteIfExists()
                     faceData.remove(item)
@@ -176,7 +181,8 @@ class AddUserDialog(
                         }
                     }
                 }
-                itemBinding.delete.isVisible = item != CommonUtils.getStr(com.grkj.ui_base.R.string.register)
+                itemBinding.delete.isVisible =
+                    item != CommonUtils.getStr(com.grkj.ui_base.R.string.register)
                 itemBinding.delete.setDebouncedClickListener {
                     fingerprintGroupData.removeIf { it == item }
                     if (fingerprintGroupData.size - 1 < MMKVConstants.KEY_MAX_FINGERPRINT_INSERT.getMMKVData(
@@ -189,6 +195,17 @@ class AddUserDialog(
                 }
             }
         }.models = fingerprintGroupData
+        binding.swipCardClick.setDebouncedClickListener {
+            InRFIDScanModeEvent.sendInRFIDScanModeEvent(true)
+            SwipCardOperationTipDialog.show().also { swipeCardDialog = it }
+                .setDialogLifecycleCallback(object :
+                    DialogLifecycleCallback<CustomDialog>() {
+                    override fun onDismiss(dialog: CustomDialog) {
+                        InRFIDScanModeEvent.sendInRFIDScanModeEvent(false)
+                        super.onDismiss(dialog)
+                    }
+                })
+        }
         apply()
     }
 
@@ -199,6 +216,7 @@ class AddUserDialog(
         rfid: String
     ) {
         binding.cardcodeEt.setText(rfid)
+        swipeCardDialog?.dismiss()
     }
 
     private fun checkData(): Boolean {
@@ -250,7 +268,7 @@ class AddUserDialog(
         ): CustomDialog {
             return CustomDialog.show(
                 addUserDialogView ?: AddUserDialog(
-                    roleData, workstationData, apply, onConfirm, registerFingerPrint,clearData
+                    roleData, workstationData, apply, onConfirm, registerFingerPrint, clearData
                 ).also { addUserDialogView = it }, CustomDialog.ALIGN.CENTER
             )
         }

+ 16 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/UpdateUserDialog.kt

@@ -15,12 +15,15 @@ import com.grkj.data.domain.vo.UserManageVo
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogUpdateUserBinding
 import com.grkj.iscs.databinding.ItemDeleteBtnBinding
+import com.grkj.iscs.features.main.dialog.SwipCardOperationTipDialog
 import com.grkj.iscs.features.main.dialog.TextDropDownDialog
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.event.InRFIDScanModeEvent
 import com.grkj.ui_base.utils.extension.tip
 import com.kongzue.dialogx.dialogs.CustomDialog
 import com.kongzue.dialogx.dialogs.PopTip
+import com.kongzue.dialogx.interfaces.DialogLifecycleCallback
 import com.kongzue.dialogx.interfaces.OnBindView
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.extension.deleteIfExists
@@ -47,6 +50,7 @@ class UpdateUserDialog(
     private lateinit var binding: DialogUpdateUserBinding
     private var selectedRoles = mutableListOf<TextDropDownDialog.TextDropDownEntity>()
     private var selectedWorkstations = mutableListOf<TextDropDownDialog.TextDropDownEntity>()
+    private var swipeCardDialog: CustomDialog? = null
     private var faceData: MutableList<String> =
         mutableListOf(CommonUtils.getStr(com.grkj.ui_base.R.string.register))
     private var fingerprintGroupData: MutableList<String> =
@@ -253,6 +257,17 @@ class UpdateUserDialog(
                 }
             }
         }.models = fingerprintGroupData
+        binding.swipCardClick.setDebouncedClickListener {
+            InRFIDScanModeEvent.sendInRFIDScanModeEvent(true)
+            SwipCardOperationTipDialog.show().also { swipeCardDialog = it }
+                .setDialogLifecycleCallback(object :
+                    DialogLifecycleCallback<CustomDialog>() {
+                    override fun onDismiss(dialog: CustomDialog) {
+                        InRFIDScanModeEvent.sendInRFIDScanModeEvent(false)
+                        super.onDismiss(dialog)
+                    }
+                })
+        }
         apply()
     }
 
@@ -263,6 +278,7 @@ class UpdateUserDialog(
         rfid: String
     ) {
         binding.cardcodeEt.setText(rfid)
+        swipeCardDialog?.dismiss()
     }
 
     companion object {

+ 7 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/UserManageFragment.kt

@@ -14,6 +14,7 @@ import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.EventConstants
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.domain.vo.UserManageVo
+import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentUserManageBinding
 import com.grkj.iscs.databinding.ItemUserManageUserBinding
@@ -359,6 +360,12 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
         FingerprintUtil.start()
         mFingerprintGroupName = UUID.randomUUID().toString()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
+            override fun onStart() {
+                LoadingEvent.sendLoadingEvent(
+                    CommonUtils.getStr(com.grkj.ui_base.R.string.get_fingerprint),
+                    true
+                )
+            }
             override fun onScan(bitmap: Bitmap) {
                 viewModel.saveUserFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap) ?: "",

+ 7 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -12,6 +12,7 @@ import com.drake.brv.utils.setup
 import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.domain.vo.FingerprintDataVo
+import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentSetFingerprintBinding
 import com.grkj.iscs.databinding.ItemSetFingerprintBinding
@@ -153,6 +154,12 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
         FingerprintUtil.start()
         mFingerprintGroupName = UUID.randomUUID().toString()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
+            override fun onStart() {
+                LoadingEvent.sendLoadingEvent(
+                    CommonUtils.getStr(com.grkj.ui_base.R.string.get_fingerprint),
+                    true
+                )
+            }
             override fun onScan(bitmap: Bitmap) {
                 viewModel.saveUserFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap) ?: "",

+ 1 - 1
iscs_lock/src/main/res/layout/fragment_set_face.xml

@@ -122,7 +122,7 @@
                     <FrameLayout
                         android:id="@+id/preview_layout"
                         android:layout_width="match_parent"
-                        android:layout_height="0dp"
+                        android:layout_height="match_parent"
                         android:visibility="visible">
 
                         <TextureView

+ 2 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt

@@ -55,13 +55,14 @@ import java.io.File
  */
 @HiltAndroidApp
 class ISCSMCApplication : Application() {
-    private val logger: Logger = LoggerFactory.getLogger(ISCSMCApplication::class.java)
+    private val logger: Logger by lazy { LoggerFactory.getLogger(ISCSMCApplication::class.java) }
 
     /**
      * 程序创建
      */
     override fun onCreate() {
         super.onCreate()
+        initLogger()
         ISCSDatabase.DB_FOLDER = "ISCS_MC/database/"
         MMKV.initialize(this)
         DialogX.init(this)
@@ -73,7 +74,6 @@ class ISCSMCApplication : Application() {
             logger.error("异常发生", it)
             true
         }
-        initLogger()
         System.loadLibrary("sqlcipher")   // 新库必须手动加载
         I18nManager.attach(this)
         I18nManager.init(

+ 11 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt

@@ -117,10 +117,19 @@ class LoginDialog(
                     FingerprintUtil.init(SIKCore.getApplication())
                     FingerprintUtil.start()
                     FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
-                        override fun onScan(bitmap: Bitmap) {
+                        override fun onStart() {
                             LoadingEvent.sendLoadingEvent(
-                                CommonUtils.getStr("doing_login"), true
+                                CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
+                                true
                             )
+                        }
+                        override fun onScan(bitmap: Bitmap) {
+                            if (FingerprintUtil.isZKDevice){
+                                LoadingEvent.sendLoadingEvent(
+                                    CommonUtils.getStr("doing_login"),
+                                    true
+                                )
+                            }
                             viewModel.loginWithFingerprint(
                                 ImageConvertUtils.bitmapToBase64(
                                     bitmap

+ 11 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt

@@ -394,10 +394,19 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
         FingerprintUtil.init(requireContext())
         FingerprintUtil.start()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
-            override fun onScan(bitmap: Bitmap) {
+            override fun onStart() {
                 LoadingEvent.sendLoadingEvent(
-                    CommonUtils.getStr("doing_login"), true
+                    CommonUtils.getStr(com.grkj.ui_base.R.string.doing_login),
+                    true
                 )
+            }
+            override fun onScan(bitmap: Bitmap) {
+                if (FingerprintUtil.isZKDevice){
+                    LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr("doing_login"),
+                        true
+                    )
+                }
                 viewModel.loginWithFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap).toString()
                 ).observe(this@LoginFragment) { isSuccess ->

+ 7 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/data_manage/UserManageFragment.kt

@@ -14,6 +14,7 @@ import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.EventConstants
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.domain.vo.UserManageVo
+import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentUserManageBinding
 import com.grkj.iscs_mc.databinding.ItemUserManageUserBinding
@@ -359,6 +360,12 @@ class UserManageFragment : BaseFragment<FragmentUserManageBinding>() {
         FingerprintUtil.start()
         mFingerprintGroupName = UUID.randomUUID().toString()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
+            override fun onStart() {
+                LoadingEvent.sendLoadingEvent(
+                    CommonUtils.getStr(com.grkj.ui_base.R.string.get_fingerprint),
+                    true
+                )
+            }
             override fun onScan(bitmap: Bitmap) {
                 viewModel.saveUserFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap) ?: "",

+ 7 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFingerprintFragment.kt

@@ -12,6 +12,7 @@ import com.drake.brv.utils.setup
 import com.grkj.data.data.CommonConstants
 import com.grkj.data.data.MMKVConstants
 import com.grkj.data.domain.vo.FingerprintDataVo
+import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentSetFingerprintBinding
 import com.grkj.iscs_mc.databinding.ItemSetFingerprintBinding
@@ -155,6 +156,12 @@ class SetFingerprintFragment : BaseFragment<FragmentSetFingerprintBinding>() {
         FingerprintUtil.start()
         mFingerprintGroupName = UUID.randomUUID().toString()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {
+            override fun onStart() {
+                LoadingEvent.sendLoadingEvent(
+                    CommonUtils.getStr(com.grkj.ui_base.R.string.get_fingerprint),
+                    true
+                )
+            }
             override fun onScan(bitmap: Bitmap) {
                 viewModel.saveUserFingerprint(
                     ImageConvertUtils.bitmapToBase64(bitmap) ?: "",

+ 33 - 7
shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt

@@ -96,7 +96,7 @@ object ArcSoftUtil {
     // ---- 旧有字段(保持) ----
     private var cameraHelper: CameraHelper? = null
     private var previewSize: Camera.Size? = null
-    private val rgbCameraId = Camera.CameraInfo.CAMERA_FACING_BACK
+    private var rgbCameraId: Int? = null
     private var faceEngine: FaceEngine? = null
     private val cameraWidth: Int = 640
     private val cameraHeight: Int = 480
@@ -120,6 +120,16 @@ object ArcSoftUtil {
         }
     }
 
+    private fun findCameraIdByFacing(facing: Int): Int? {
+        val count = Camera.getNumberOfCameras()
+        val info = Camera.CameraInfo()
+        for (id in 0 until count) {
+            Camera.getCameraInfo(id, info)
+            if (info.facing == facing) return id
+        }
+        return null
+    }
+
     /**
      * 所需的所有权限信息
      */
@@ -194,6 +204,13 @@ object ArcSoftUtil {
         needCheckCenter: Boolean = false,
         callBack: (Bitmap?, Int, Boolean) -> Unit
     ) {
+        if (rgbCameraId == null) {
+            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
+                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK) // 兜底
+        }
+        val camId = rgbCameraId ?: run {
+            logger.error("找不到可用相机ID"); return
+        }
         val cameraListener: CameraListener = object : CameraListener {
             override fun onCameraOpened(
                 camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean
@@ -277,11 +294,12 @@ object ArcSoftUtil {
             }
         }
 
+        val rotation = DisplayUtils.getRotation(preview.context) // 别用 Application 拿 rotation
         cameraHelper = CameraHelper.Builder()
             .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(DisplayUtils.getRotation(SIKCore.getApplication()))
-            .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT)
-            .isMirror(false)
+            .rotation(rotation)
+            .specificCameraId(camId)
+            .isMirror(true) // 前摄预览一般要镜像,非必需但更自然
             .previewOn(preview)
             .cameraListener(cameraListener)
             .build()
@@ -297,6 +315,13 @@ object ArcSoftUtil {
         preview: View,
         callBack: (Bitmap?, Long?) -> Unit
     ) {
+        if (rgbCameraId == null) {
+            rgbCameraId = findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_FRONT)
+                ?: findCameraIdByFacing(Camera.CameraInfo.CAMERA_FACING_BACK) // 兜底
+        }
+        val camId = rgbCameraId ?: run {
+            logger.error("找不到可用相机ID"); return
+        }
         val metrics = DisplayMetrics()
         windowManager.defaultDisplay.getMetrics(metrics)
 
@@ -359,11 +384,12 @@ object ArcSoftUtil {
             }
         }
 
+        val rotation = DisplayUtils.getRotation(preview.context) // 别用 Application 拿 rotation
         cameraHelper = CameraHelper.Builder()
             .previewViewSize(Point(cameraWidth, cameraHeight))
-            .rotation(windowManager.defaultDisplay.rotation)
-            .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT)
-            .isMirror(false)
+            .rotation(rotation)
+            .specificCameraId(camId)
+            .isMirror(true) // 前摄预览一般要镜像,非必需但更自然
             .previewOn(preview)
             .cameraListener(cameraListener)
             .build()

+ 25 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/InRFIDScanModeEvent.kt

@@ -0,0 +1,25 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.data.data.EventConstants
+import com.grkj.shared.utils.event.EventHelper
+
+/**
+ * 进入RFID扫描模式
+ */
+class InRFIDScanModeEvent(val inRfidScanMode: Boolean = true) {
+
+    companion object {
+        /**
+         * 发送是否进入扫描模式事件
+         */
+        @JvmStatic
+        fun sendInRFIDScanModeEvent(show: Boolean = true) {
+            val inRfidScanModeEvent = InRFIDScanModeEvent(show)
+            val bottomNavVisibilityEventBean = EventBean<InRFIDScanModeEvent>(
+                EventConstants.EVENT_IN_RFID_SCAN_MODE, inRfidScanModeEvent
+            )
+            EventHelper.sendEvent(bottomNavVisibilityEventBean)
+        }
+    }
+}

+ 17 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/fingerprint/FingerprintUtil.kt

@@ -7,8 +7,10 @@ import android.hardware.usb.UsbManager
 import android.util.Base64
 import android.util.Log
 import androidx.appcompat.app.AppCompatActivity.USB_SERVICE
+import com.grkj.data.hardware.fingerprint.FingerprintCaptureService
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.thread.ThreadUtils
+import com.sik.sikimage.ImageConvertUtils
 import com.zkteco.android.biometric.FingerprintExceptionListener
 import com.zkteco.android.biometric.core.device.ParameterHelper
 import com.zkteco.android.biometric.core.device.TransportType
@@ -36,8 +38,10 @@ object FingerprintUtil {
     private var bStarted = false
     private var usb_pid = 0
     private var zkusbManager: ZKUSBManager? = null
+    var isZKDevice: Boolean = false
     private val usb_vid: Int = ZKTECO_VID
     private var fingerprintSensor: FingerprintSensor? = null
+    private var fingerprintCaptureService: FingerprintCaptureService? = null
     private var bRegister = false
     private var enroll_index = 0
     private var isReseted = false
@@ -78,14 +82,26 @@ object FingerprintUtil {
             logger.info("Device already connected!")
             return
         }
+        FingerprintCaptureService.register("/dev/ttyS0").also { fingerprintCaptureService = it }
+            .listen({
+                onScanListener?.onStart()
+            }, {
+                ImageConvertUtils.base64ToBitmap(it)?.let { bmp ->
+                    onScanListener?.onScan(bmp)
+                }
+            }, {
+                logger.info("指纹异常:${it}")
+            })
         if (!enumSensor()) {
             logger.info("Device not found!")
             return
         }
+        isZKDevice = true
         tryGetUSBPermission()
     }
 
     fun stop() {
+        fingerprintCaptureService?.stop()
         if (!bStarted) {
             logger.info("Device not connected!")
             return
@@ -315,6 +331,7 @@ object FingerprintUtil {
     }
 
     interface OnScanListener {
+        fun onStart()
         fun onScan(bitmap: Bitmap)
     }
 }

+ 1 - 0
ui-base/src/main/res/values-en/strings.xml

@@ -853,4 +853,5 @@
     <string name="account_login_tip">请输入您的账号和密码,然后点击登录。</string>
     <string name="face_login_tip">请将面部对准摄像头,完成人脸认证后,将自动登录。</string>
     <string name="face_not_activated">人脸引擎激活失败,识别暂不可用</string>
+    <string name="get_fingerprint">Getting Fingerprint</string>
 </resources>

+ 1 - 0
ui-base/src/main/res/values-zh/strings.xml

@@ -853,4 +853,5 @@
     <string name="account_login_tip">请输入您的账号和密码,然后点击登录。</string>
     <string name="face_login_tip">请将面部对准摄像头,完成人脸认证后,将自动登录。</string>
     <string name="face_not_activated">人脸引擎激活失败,识别暂不可用</string>
+    <string name="get_fingerprint">正在获取指纹</string>
 </resources>

+ 1 - 0
ui-base/src/main/res/values/strings.xml

@@ -853,4 +853,5 @@
     <string name="account_login_tip">请输入您的账号和密码,然后点击登录。</string>
     <string name="face_login_tip">请将面部对准摄像头,完成人脸认证后,将自动登录。</string>
     <string name="face_not_activated">人脸引擎激活失败,识别暂不可用</string>
+    <string name="get_fingerprint">正在获取指纹</string>
 </resources>