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

feat(人脸识别):
- `FaceUtil`: 将摄像头预览设置为非镜像模式(`isMirror(false)`),以确保预览画面与实际方向一致。
- `FaceUtil`: 增加对虹软(ArcSoft)引擎和激活状态的重复初始化检查,避免不必要的重复操作。
- `Hlk223Client`: 在HLK人脸模组连续验证(1:N)时,设置为循环模式(`loop = true`),以支持持续识别。

fix(人脸识别):
- `LoginDialog`, `LoginFragment`, `CheckFaceDialog`: 在人脸识别回调中增加对`bitmap`和`userId`的空值检查,防止因识别失败返回`null`导致的程序崩溃。

fix(硬件):
- `HardwareLogic`, `UserLogic`, `RoleLogic`, `UserRepositoryImpl`: 统一了“状态”字段的传参和数据库存储逻辑。将`true`/`false`映射为`1`(启用)/`0`(禁用)或`0`(正常)/`1`(停用),解决了之前部分模块使用`0`/`2`映射导致的状态查询和更新不一致问题。

refactor(人脸识别):
- `Hlk223Client`, `Hlk223Frames`, `Hlk223PhotoEnroll`: 增强了HLK-223模组的通信协议解析能力,通过`autoParse`和`pickBestFrame`机制,实现了对多帧(如REPLY、NOTE帧混合返回)和单帧数据的自适应解析,显著提高了通信的稳定性和对不同固件版本的兼容性。

refactor(指纹):
- `FingerprintUtil`: 优化了指纹模块的初始化逻辑。在原有中控(ZK)指纹模块探测失败时,会尝试注册并使用串口指纹模块(`FingerprintCaptureService`),实现了对两种主流指纹模块的自动降级和兼容支持。

refactor(图片处理):
- `ImageCompress`: 为图片压缩工具新增`rotateDeg`参数,允许在压缩前按指定角度(如270度)旋转图片,以修正图像方向。

fix(UI):
- `item_quick_entrance_*.xml`: 移除了快速入口图标上硬编码的`tint`属性,使其颜色能通过主题或代码动态控制。
- `icon_add.xml`, `icon_remove.xml`: 更新了添加/移除图标的`fillColor`,使其样式更符合新设计。
- `item_locker_group.xml`: 将分组标题的下划线背景从`colorBlack`修正为`colorDivider`,使其与主题风格一致;并在横屏模式下为标题增加了背景样式。
- `dialog_add_key.xml`, `dialog_update_key.xml`: 将钥匙状态字段从“必填”改为“非必填”,增强了表单的灵活性。

fix(功能):
- `WorkstationManageFragment`: 修复了在取消默认工作站后,未正确清除已保存的默认工作站ID的问题。
- `ModBusController`: 修正了解析钥匙RFID时的字节范围,并完善了日志,解决了因数据长度不足导致RFID读取错误的问题。

chore(代码):
- `ISCSApplication`, `ISCSMCApplication`, `LoginActivity`: 注释掉了部分与HLK人脸识别模组相关的测试和初始化代码。
- `BleBusinessManager`, `JobTicketLogic`: 增加了在下发工作票和获取步骤详情时的调试日志,方便问题排查。

周文健 2 долоо хоног өмнө
parent
commit
24a9f243ae
31 өөрчлөгдсөн 212 нэмэгдсэн , 62 устгасан
  1. 4 4
      data/src/main/java/com/grkj/data/domain/logic/impl/HardwareLogic.kt
  2. 2 0
      data/src/main/java/com/grkj/data/domain/logic/impl/JobTicketLogic.kt
  3. 3 3
      data/src/main/java/com/grkj/data/domain/logic/impl/RoleLogic.kt
  4. 2 2
      data/src/main/java/com/grkj/data/domain/logic/impl/UserLogic.kt
  5. 9 3
      data/src/main/java/com/grkj/data/hardware/face/FaceUtil.kt
  6. 53 2
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt
  7. 57 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt
  8. 14 2
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt
  9. 3 3
      data/src/main/java/com/grkj/data/hardware/modbus/ModBusController.kt
  10. 1 1
      data/src/main/java/com/grkj/data/repository/impl/standard/UserRepositoryImpl.kt
  11. 1 9
      iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt
  12. 11 0
      iscs_lock/src/main/java/com/grkj/iscs/features/login/activity/LoginActivity.kt
  13. 3 1
      iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt
  14. 1 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt
  15. 2 0
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/data_manage/WorkstationManageFragment.kt
  16. 1 1
      iscs_lock/src/main/res/drawable/icon_add.xml
  17. 6 2
      iscs_lock/src/main/res/drawable/icon_remove.xml
  18. 2 1
      iscs_lock/src/main/res/layout-land/item_locker_group.xml
  19. 0 2
      iscs_lock/src/main/res/layout-land/item_quick_entrance_config.xml
  20. 0 1
      iscs_lock/src/main/res/layout-land/item_quick_entrance_not_config.xml
  21. 1 1
      iscs_lock/src/main/res/layout/dialog_add_key.xml
  22. 1 1
      iscs_lock/src/main/res/layout/dialog_update_key.xml
  23. 1 1
      iscs_lock/src/main/res/layout/item_locker_group.xml
  24. 0 2
      iscs_lock/src/main/res/layout/item_quick_entrance_config.xml
  25. 0 2
      iscs_lock/src/main/res/layout/item_quick_entrance_not_config.xml
  26. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  27. 1 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt
  28. 1 0
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt
  29. 14 5
      shared/src/main/java/com/grkj/shared/utils/ImageCompress.kt
  30. 1 0
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  31. 15 11
      ui-base/src/main/java/com/grkj/ui_base/utils/fingerprint/FingerprintUtil.kt

+ 4 - 4
data/src/main/java/com/grkj/data/domain/logic/impl/HardwareLogic.kt

@@ -460,7 +460,7 @@ class HardwareLogic @Inject constructor(
             filterVo?.keyCode,
             filterVo?.keyNfc,
             filterVo?.macAddress,
-            filterVo?.status?.let { if (it) 0 else 2 } ?: null,
+            filterVo?.status?.let { if (it) 1 else 0 },
             size,
             offset
         )
@@ -478,7 +478,7 @@ class HardwareLogic @Inject constructor(
         return hardwareRepository.getLockInfoPage(
             filterVo?.lockCode,
             filterVo?.lockNfc,
-            filterVo?.status?.let { if (it) 0 else 2 } ?: null,
+            filterVo?.status?.let { if (it) 1 else 0 },
             size,
             offset
         )
@@ -492,7 +492,7 @@ class HardwareLogic @Inject constructor(
         return hardwareRepository.getCardInfoPage(
             filterVo?.cardNfc,
             filterVo?.username,
-            filterVo?.status?.let { if (it) 0 else 2 } ?: null,
+            filterVo?.status?.let { if (it) 1 else 0 },
             size,
             offset
         )
@@ -506,7 +506,7 @@ class HardwareLogic @Inject constructor(
         return hardwareRepository.getRfidTokenInfoPage(
             filterVo?.rfidCode,
             filterVo?.rfid,
-            filterVo?.status?.let { if (it) 0 else 2 } ?: null,
+            filterVo?.status?.let { if (it) 1 else 0 },
             size,
             offset
         )

+ 2 - 0
data/src/main/java/com/grkj/data/domain/logic/impl/JobTicketLogic.kt

@@ -44,6 +44,7 @@ import com.grkj.data.repository.WorkstationRepository
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.data.BeanUtils
 import com.sik.sikcore.date.TimeUtils
+import com.sik.sikcore.extension.toJson
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -723,6 +724,7 @@ class JobTicketLogic @Inject constructor(
             jobTicketKeyDataList as MutableList<TicketDetailRes.JobTicketKeyVO>?
         //填充挂锁数据
         val ticketLockData = getJobTicketLockDataByTicketId(ticketId)
+        logger.info("步骤详情-挂锁数据:${ticketLockData.toJson()}")
         val jobTicketLockDataList =
             BeanUtils.copyList(ticketLockData, TicketDetailRes.JobTicketLockVO::class.java)
         ticketDetailRes?.ticketLockVOList =

+ 3 - 3
data/src/main/java/com/grkj/data/domain/logic/impl/RoleLogic.kt

@@ -88,7 +88,7 @@ class RoleLogic @Inject constructor(
         return roleRepository.getRoleManageData(
             roleManageFilterVo?.roleName,
             roleManageFilterVo?.permissionCharacters,
-            roleManageFilterVo?.status?.let { if (it == true) "0" else "1" },
+            roleManageFilterVo?.status?.let { if (it) "0" else "1" },
             size,
             current * size
         )
@@ -98,7 +98,7 @@ class RoleLogic @Inject constructor(
         val sysRole = SysRole()
         sysRole.roleName = addRoleDo.roleName
         sysRole.roleKey = addRoleDo.roleKey
-        sysRole.status = if (addRoleDo.status == true) "0" else "2"
+        sysRole.status = if (addRoleDo.status == true) "0" else "1"
         sysRole.dataScope = "1"
         val roleId = roleRepository.insertRole(sysRole)
         val sysRoleMenu = mutableListOf<SysRoleMenu>().apply {
@@ -117,7 +117,7 @@ class RoleLogic @Inject constructor(
         sysRole.roleId = updateRoleDo.roleId
         sysRole.roleName = updateRoleDo.roleName
         sysRole.roleKey = updateRoleDo.roleKey
-        sysRole.status = if (updateRoleDo.status == true) "0" else "2"
+        sysRole.status = if (updateRoleDo.status == true) "0" else "1"
         sysRole.dataScope = "1"
         val roleId = roleRepository.insertRole(sysRole)
         sysMenuRepository.deleteByRoleId(updateRoleDo.roleId)

+ 2 - 2
data/src/main/java/com/grkj/data/domain/logic/impl/UserLogic.kt

@@ -332,7 +332,7 @@ class UserLogic @Inject constructor(
         sysUserDo.userName = addUserDataVo.username
         sysUserDo.password = addUserDataVo.password
         sysUserDo.nickName = addUserDataVo.nickname
-        sysUserDo.status = if (addUserDataVo.status) "0" else "2"
+        sysUserDo.status = if (addUserDataVo.status) "0" else "1"
         sysUserDo.delFlag = "0"
         return userRepository.insert(sysUserDo)
     }
@@ -347,7 +347,7 @@ class UserLogic @Inject constructor(
         sysUserDo.userName = userDataVo.username
         sysUserDo.password = userDataVo.password
         sysUserDo.nickName = userDataVo.nickname
-        sysUserDo.status = if (userDataVo.status) "0" else "2"
+        sysUserDo.status = if (userDataVo.status) "0" else "1"
         sysUserDo.delFlag = "0"
         userRepository.updateUserData(sysUserDo)
     }

+ 9 - 3
data/src/main/java/com/grkj/data/hardware/face/FaceUtil.kt

@@ -209,6 +209,9 @@ object FaceUtil {
         if (backend == FaceBackend.HLK) {
             isActivated = true; return
         }
+        if (isActivated) {
+            return
+        }
         val cfg = readOrInitConfig(context)
         val code = try {
             if (cfg.activeOnline) FaceEngine.activeOnline(
@@ -237,6 +240,9 @@ object FaceUtil {
 
     fun initEngine(context: Context) {
         if (backend == FaceBackend.HLK) return
+        if (faceEngine != null) {
+            return
+        }
         faceEngine = FaceEngine()
         afCode = faceEngine!!.init(
             context,
@@ -399,7 +405,7 @@ object FaceUtil {
             .previewViewSize(Point(cameraWidth, cameraHeight))
             .rotation(rotation)
             .specificCameraId(camId)
-            .isMirror(true)
+            .isMirror(false)
             .previewOn(preview)
             .cameraListener(listener)
             .build()
@@ -433,7 +439,7 @@ object FaceUtil {
                     hlkVerifyJob = ioScope.launch {
                         try {
                             hlkClient?.startVerifyWithNotes(
-                                timeoutSec = 15, loop = false,
+                                timeoutSec = 15, loop = true,
                                 onFaceState = { rect, state, yaw, pitch, roll ->
                                     logger.info("onFaceState: $rect, $state, $yaw, $pitch, $roll")
                                     lastFaceRectByHlk = rect; lastAliveByHlk = (state == 0)
@@ -509,7 +515,7 @@ object FaceUtil {
             .previewViewSize(Point(cameraWidth, cameraHeight))
             .rotation(rotation)
             .specificCameraId(camId)
-            .isMirror(true)
+            .isMirror(false)
             .previewOn(preview)
             .cameraListener(listener)
             .build()

+ 53 - 2
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt

@@ -32,11 +32,59 @@ class Hlk223Client(
     ): Pair<Int, ByteArray> {
         val req: CommMessage = Hlk223.msg(mid, data, timeoutMs)
         logger.info("请求:${req.payload.toHexStrings()}")
+
+        val rsp: CommMessage = protocol.send(deviceId, req)
+        logger.info("返回:${rsp.payload.toHexStrings()}")
+
+        // 🔽 关键:自动拆帧(单帧 / 多帧 / 含 NOTE 都能兜)
+        val parsed: List<Pair<Int, ByteArray>> =
+            try {
+                Hlk223.autoParse(rsp.payload)  // 你前面那版我已经给了;没有就搬运进去
+            } catch (t: Throwable) {
+                // 兜底:老固件返回干净单帧时,保持兼容
+                logger.warn("autoParse 失败,尝试单帧解析: ${t.message}")
+                listOf(Hlk223.parseOne(rsp.payload))
+            }
+
+        if (parsed.isEmpty()) error("No valid frame found in response")
+
+        if (parsed.size > 1) {
+            // 把多出来的帧打日志,帮你定位设备“爱发 NOTE/IMAGE”的毛病
+            val mids = parsed.joinToString(",") { "0x${it.first.toString(16)}" }
+            logger.info("多帧返回,MIDs=[$mids]")
+        }
+
+        return pickBestFrame(parsed)
+    }
+
+    @Suppress("unused")
+    private suspend fun exchangeAll(
+        mid: Int,
+        data: ByteArray = byteArrayOf(),
+        timeoutMs: Int? = 3000
+    ): List<Pair<Int, ByteArray>> {
+        val req: CommMessage = Hlk223.msg(mid, data, timeoutMs)
+        logger.info("请求:${req.payload.toHexStrings()}")
         val rsp: CommMessage = protocol.send(deviceId, req)
         logger.info("返回:${rsp.payload.toHexStrings()}")
-        return Hlk223.parseOne(rsp.payload)
+
+        return try {
+            Hlk223.autoParse(rsp.payload).also {
+                if (it.isEmpty()) error("No valid frames")
+            }
+        } catch (t: Throwable) {
+            listOf(Hlk223.parseOne(rsp.payload))
+        }
     }
 
+    private fun pickBestFrame(frames: List<Pair<Int, ByteArray>>): Pair<Int, ByteArray> {
+        // 优先选 REPLY
+        frames.firstOrNull { it.first == Hlk223.MID.REPLY }?.let { return it }
+        // 没有 REPLY:如果只有一帧就用那一帧,否则退回第一帧
+        return frames.singleOrNull() ?: frames.first()
+    }
+
+
     private fun u8(b: Byte) = b.toInt() and 0xFF
     private fun s16(hi: Int, lo: Int): Int = ((hi shl 8) or lo).toShort().toInt()
 
@@ -161,7 +209,8 @@ class Hlk223Client(
                 val deadline = System.currentTimeMillis() + (timeoutSec + 2) * 1000L
                 while (System.currentTimeMillis() < deadline &&
                     !verifyStop.get() &&
-                    currentCoroutineContext().isActive) {
+                    currentCoroutineContext().isActive
+                ) {
                     val ack: CommMessage = try {
                         io.readRaw(
                             timeoutMs = singleReadTimeoutMs,
@@ -193,6 +242,7 @@ class Hlk223Client(
                                 val nid = data.getOrNull(0)?.let { u8(it) } ?: continue
                                 when (nid) {
                                     NID_FACE_STATE -> if (data.size >= 1 + 16) {
+                                        logger.info("人脸数据:${data.toHexStrings()}")
                                         fun s(i: Int) = s16(u8(data[i + 1]), u8(data[i]))
                                         val st = s(1)
                                         val left = s(3);
@@ -208,6 +258,7 @@ class Hlk223Client(
                                             right,
                                             bottom
                                         ) else null
+                                        logger.info("人脸位置:${left},${top},${right},${bottom}")
                                         onFaceState(rect, st, yaw, pitch, roll)
                                     }
 

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

@@ -61,6 +61,63 @@ object Hlk223 {
         return mid to frame.copyOfRange(5, 5 + size)
     }
 
+    /**
+     * 从一次性 buffer 中尽可能提取“完整帧”(含 EF AA 头)。
+     * 无状态,不保留残包;半帧会被丢到下一次由上层自己拼(或用 Framer 方案)。
+     */
+    private fun extractFramesOnce(buffer: ByteArray): List<ByteArray> {
+        if (buffer.isEmpty()) return emptyList()
+        val out = ArrayList<ByteArray>()
+        var i = 0
+        while (true) {
+            // 找同步头
+            while (i + 1 < buffer.size && !(buffer[i] == SYNC_H && buffer[i + 1] == SYNC_L)) i++
+            if (i + 5 > buffer.size) break // 不够读 mid+len
+
+            val size = ((buffer[i + 3].toInt() and 0xFF) shl 8) or (buffer[i + 4].toInt() and 0xFF)
+            // 合理性检查:防止畸形长度拉爆
+            if (size < 0 || size > 0xFFFF) { i++ ; continue }
+
+            val frameLen = 5 + size + 1
+            if (frameLen < 6) { i++ ; continue }
+            if (i + frameLen > buffer.size) break // 半帧,留给上层下次拼
+
+            // 校验 XOR
+            var xor = 0
+            for (k in i + 2 until i + 5 + size) xor = xor xor (buffer[k].toInt() and 0xFF)
+            val got = buffer[i + 5 + size].toInt() and 0xFF
+            if (xor == got) {
+                out += buffer.copyOfRange(i, i + frameLen)
+                i += frameLen
+            } else {
+                i++ // 假头,细粒度推进
+            }
+        }
+        return out
+    }
+
+    /**
+     * 自适应解析:你把 exchange 返回的 payload 丢进来,
+     * 我返回“拆好的 (mid, data) 列表”。单帧也会返回 size=1。
+     */
+    fun autoParse(payload: ByteArray): List<Pair<Int, ByteArray>> {
+        if (payload.isEmpty()) return emptyList()
+
+        // Fast path:看起来像“完整单帧”,直接走 parseOne,零拷贝少一次扫描
+        if (payload.size >= 6 && payload[0] == SYNC_H && payload[1] == SYNC_L) {
+            val size = ((payload[3].toInt() and 0xFF) shl 8) or (payload[4].toInt() and 0xFF)
+            val frameLen = 5 + size + 1
+            if (frameLen == payload.size) {
+                return listOf(parseOne(payload))
+            }
+        }
+
+        // 多帧 / 带噪声:切帧→逐帧 parseOne
+        val frames = extractFramesOnce(payload)
+        if (frames.isEmpty()) return emptyList()
+        return frames.map { f -> parseOne(f) }
+    }
+
     /** 兼容旧名:但注意它假设传入就是“单帧”;多帧/半帧请用 Framer */
     fun parse(frame: ByteArray): Pair<Int, ByteArray> = parseOne(frame)
 

+ 14 - 2
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt

@@ -1,11 +1,13 @@
 package com.grkj.data.hardware.face.hlk
 
+import coil.util.Logger
 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 org.slf4j.LoggerFactory
 import kotlin.math.min
 
 /**
@@ -18,6 +20,7 @@ class Hlk223PhotoEnroll(
     private val protocol: ModbusProtocol,
     private val deviceId: String
 ) {
+    private val logger: org.slf4j.Logger = LoggerFactory.getLogger(Hlk223PhotoEnroll::class.java)
     private fun chunk(bytes: ByteArray, mtu: Int = 246): List<ByteArray> {
         val out = mutableListOf<ByteArray>()
         var i = 0
@@ -56,11 +59,19 @@ class Hlk223PhotoEnroll(
         return TxPlan(frames = frames)
     }
 
+    private fun pickBestFrame(frames: List<Pair<Int, ByteArray>>): Pair<Int, ByteArray> {
+        // 优先选 REPLY
+        frames.firstOrNull { it.first == Hlk223.MID.REPLY }?.let { return it }
+        // 没有 REPLY:如果只有一帧就用那一帧,否则退回第一帧
+        return frames.singleOrNull() ?: frames.first()
+    }
+
     private fun policy(): ChainPolicy = object : ChainPolicy {
         override suspend fun afterSendStep(stepIndex: Int, sent: CommMessage, io: LinkIO): ChainStepResult {
             // 每发一包就等一次 REPLY(如需处理 NOTE,可在此多读一次 NOTE)
             val ack = io.readRaw(timeoutMs = 3000, expectedSize = null, silenceGapMs = 30)
-            val (mid, data) = Hlk223.parse(ack.payload)
+            logger.info("返回数据:${ack.payload}")
+            val (mid, data) = pickBestFrame(Hlk223.autoParse(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
@@ -74,7 +85,8 @@ class Hlk223PhotoEnroll(
     suspend fun enroll(payload: ByteArray, bioType: Int = 0, crc32: Int): Int {
         val rsps = protocol.sendChain(deviceId, buildPlan(payload, bioType, crc32), policy())
         val last = rsps.lastOrNull() ?: error("No reply")
-        val (_, data) = Hlk223.parse(last.payload)
+        logger.info("返回数据:${last.payload}")
+        val (_, data) = pickBestFrame(Hlk223.autoParse(last.payload))
         // 常见格式:REPLY data 尾部 2 字节为 userId(BE),若固件不同需对齐偏移
         val uid = if (data.size >= 4) {
             val hi = data[data.size - 2].toInt() and 0xFF

+ 3 - 3
data/src/main/java/com/grkj/data/hardware/modbus/ModBusController.kt

@@ -221,11 +221,11 @@ object ModBusController {
                         if (key.isExist) {
                             logger.info("initKey : ${dockBean.addr} : ${key.idx == 0}")
                             readKeyRfid(dockBean.addr, key.idx) { idx, res ->
-                                if (res.size < 4) {
-                                    logger.error("Key rfid error")
+                                if (res.size < 11) {
+                                    logger.error("Lock rfid error")
                                     return@readKeyRfid
                                 }
-                                val rfid = res.toHexFromLe()
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
                                 logger.info("初始化钥匙 RFID : $rfid")
                                 // 更新rfid
                                 updateKeyRfid(dockBean.addr, key.idx, rfid)

+ 1 - 1
data/src/main/java/com/grkj/data/repository/impl/standard/UserRepositoryImpl.kt

@@ -132,7 +132,7 @@ class UserRepositoryImpl @Inject constructor(
         size: Int
     ): List<UserManageVo> {
         val nickname        = filter?.nickname?.takeIf { it.isNotEmpty() }
-        val statusStr       = when (filter?.status) { true -> "1"; false -> "0"; null -> null }
+        val statusStr       = when (filter?.status) { true -> "0"; false -> "1"; null -> null }
         val cardNfc         = filter?.cardNfc?.takeIf { it.isNotEmpty() }
         val workstationName = filter?.workstationName?.takeIf { !it.isNullOrEmpty() }
         val offset          = current * size

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

@@ -64,7 +64,7 @@ class ISCSApplication : Application() {
      */
     override fun onCreate() {
         super.onCreate()
-        logLastExitReason(this)
+//        logLastExitReason(this)
         initLogger()
         DialogX.init(this)
         SIKCore.init(this)
@@ -95,14 +95,6 @@ class ISCSApplication : Application() {
         if (ISCSConfig.isInit) {
             BleUtil.instance?.initBle(this)
         }
-
-        // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
-//        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
-        // ③ 想切到 HLK 路线(但保持对外 API 不变)
-//        FaceUtil.enableHlkBackend(hlk)
-        //todo 模拟器不支持 测试用,直接创建管理员账号
-        FaceUtil.checkActiveStatus(SIKCore.getApplication())
-        FaceUtil.initEngine(SIKCore.getApplication())
         AutoSizeConfig.getInstance().isCustomFragment = false
         StateConfig.emptyLayout = com.grkj.ui_base.R.layout.layout_empty
         ThreadUtils.runOnIO {

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

@@ -22,6 +22,9 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.local.database.PresetData
 import com.grkj.data.enums.LoginResultEnum
 import com.grkj.data.entity.local.LoginMenuEntity
+import com.grkj.data.hardware.face.FaceUtil
+import com.grkj.data.hardware.face.hlk.Hlk223Client
+import com.grkj.data.hardware.face.hlk.Hlk223Config
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.ActivityLoginBinding
 import com.grkj.iscs.databinding.ItemLoginMethodBinding
@@ -48,6 +51,7 @@ 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.SIKCore
 import com.sik.sikcore.extension.setDebouncedClickListener
 import com.sik.sikcore.shell.ShellUtils
 import com.sik.sikimage.ImageConvertUtils
@@ -71,6 +75,13 @@ class LoginActivity : BaseActivity<ActivityLoginBinding>() {
     }
 
     override fun initView() {
+        // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
+//        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
+        // ③ 想切到 HLK 路线(但保持对外 API 不变)
+//        FaceUtil.enableHlkBackend(hlk)
+        //todo 模拟器不支持 测试用,直接创建管理员账号
+        FaceUtil.checkActiveStatus(SIKCore.getApplication())
+        FaceUtil.initEngine(SIKCore.getApplication())
         binding.chipLang.text = I18nManager.locale.value.toLanguageTag()
         binding.chipLang.setOnClickListener {
             val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发

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

@@ -130,8 +130,9 @@ class LoginDialog(
                                 true
                             )
                         }
+
                         override fun onScan(bitmap: Bitmap) {
-                            if (FingerprintUtil.isZKDevice){
+                            if (FingerprintUtil.isZKDevice) {
                                 LoadingEvent.sendLoadingEvent(
                                     CommonUtils.getStr("doing_login"),
                                     true
@@ -183,6 +184,7 @@ class LoginDialog(
             FaceUtil.checkCamera(
                 mBinding.preview!!
             ) { bitmap, userId ->
+                if (bitmap == null || userId == null) return@checkCamera
                 viewModel.loginWithUserId(userId).observe(lifecycleOwner) {
                     if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
                         viewModel.loginWithFace(ImageConvertUtils.bitmapToBase64(bitmap).toString())

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

@@ -161,6 +161,7 @@ class CheckFaceDialog(
             FaceUtil.checkCamera(
                 mBinding.preview!!,
             ) { bitmap, userId ->
+                if (bitmap == null || userId == null) return@checkCamera
                 bitmap?.let { itBitmap ->
                     if (userId == null) {
                         FaceUtil.inDetecting = false

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

@@ -55,6 +55,8 @@ class WorkstationManageFragment : BaseFragment<FragmentWorkstationManageBinding>
                         if (it) {
                             if (setDefault) {
                                 MainDomainData.defaultWorkstationId.saveMMKVData(updateWorkstation.workstationId)
+                            } else {
+                                MainDomainData.defaultWorkstationId.saveMMKVData(0L)
                             }
                             TipDialog.show(
                                 title = CommonUtils.getStr("action_succeed"),

+ 1 - 1
iscs_lock/src/main/res/drawable/icon_add.xml

@@ -6,7 +6,7 @@
     android:viewportHeight="1024">
 
     <path
-        android:fillColor="#0078E8"
+        android:fillColor="#16C07A"
         android:pathData="M512,51.2
         c-254,0 -460.8,206.8 -460.8,460.8
         s206.8,460.8 460.8,460.8

+ 6 - 2
iscs_lock/src/main/res/drawable/icon_remove.xml

@@ -11,8 +11,12 @@
         android:viewportHeight="1024">
 
         <path
-            android:fillColor="#8a8a8a"
-            android:pathData="M512,1024C229.2,1024 0,794.8 0,512S229.2,0 512,0s512,229.2 512,512 -229.2,512 -512,512zM825.1,546.6a34.6,34.6 0,0 0,0 -69.1L198.8,477.4a34.6,34.6 0,0 0,0 69.1h626.3z" />
+            android:fillColor="#FF5A5F"
+            android:pathData="M512,1024C229.2,1024 0,794.8 0,512S229.2,0 512,0s512,229.2 512,512 -229.2,512 -512,512z" />
+
+        <path
+            android:fillColor="#FFFFFF"
+            android:pathData="M825.1,546.6a34.6,34.6 0,0 0,0 -69.1L198.8,477.4a34.6,34.6 0,0 0,0 69.1h626.3z" />
 
     </vector>
 </inset>

+ 2 - 1
iscs_lock/src/main/res/layout-land/item_locker_group.xml

@@ -23,6 +23,7 @@
                 android:layout_height="wrap_content"
                 android:gravity="center"
                 android:minWidth="@dimen/item_locker_group_min_width"
+                android:background="@drawable/bg_item_group_title_layout"
                 android:orientation="horizontal"
                 android:paddingVertical="@dimen/iscs_space_1">
 
@@ -45,7 +46,7 @@
             <View
                 android:layout_width="match_parent"
                 android:layout_height="@dimen/divider_line_space"
-                android:background="?attr/colorBlack" />
+                android:background="?attr/colorDivider" />
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/group_locker_rv"

+ 0 - 2
iscs_lock/src/main/res/layout-land/item_quick_entrance_config.xml

@@ -31,7 +31,6 @@
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
                 android:src="@drawable/icon_add"
-                android:tint="?attr/colorStatusGreen"
                 android:visibility="gone" />
 
             <ImageView
@@ -40,7 +39,6 @@
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
                 android:src="@drawable/icon_remove"
-                android:tint="?attr/colorStatusRed"
                 android:visibility="gone" />
         </FrameLayout>
 

+ 0 - 1
iscs_lock/src/main/res/layout-land/item_quick_entrance_not_config.xml

@@ -30,7 +30,6 @@
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
                 android:src="@drawable/icon_add"
-                android:tint="?attr/colorStatusGreen"
                 android:visibility="gone" />
 
             <ImageView

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

@@ -139,7 +139,7 @@
                 android:textSize="@dimen/iscs_text_md"
                 app:formRole="label"
                 app:markPosition="start"
-                app:required="true"
+                app:required="false"
                 app:i18nKey='@{"manage_filter_status"}' />
 
             <RadioGroup

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

@@ -139,7 +139,7 @@
                 android:textSize="@dimen/iscs_text_md"
                 app:formRole="label"
                 app:markPosition="start"
-                app:required="true"
+                app:required="false"
                 app:i18nKey='@{"manage_filter_status"}' />
 
             <RadioGroup

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

@@ -42,7 +42,7 @@
             <View
                 android:layout_width="match_parent"
                 android:layout_height="@dimen/divider_line_space"
-                android:background="?attr/colorBlack" />
+                android:background="?attr/colorDivider" />
 
             <androidx.recyclerview.widget.RecyclerView
                 android:id="@+id/group_locker_rv"

+ 0 - 2
iscs_lock/src/main/res/layout/item_quick_entrance_config.xml

@@ -31,7 +31,6 @@
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
                 android:src="@drawable/icon_add"
-                android:tint="?attr/colorStatusGreen"
                 android:visibility="gone" />
 
             <ImageView
@@ -40,7 +39,6 @@
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
                 android:src="@drawable/icon_remove"
-                android:tint="?attr/colorStatusRed"
                 android:visibility="gone" />
         </FrameLayout>
 

+ 0 - 2
iscs_lock/src/main/res/layout/item_quick_entrance_not_config.xml

@@ -29,7 +29,6 @@
                 android:layout_width="@dimen/common_badge_icon_size"
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
-                android:tint="?attr/colorStatusGreen"
                 android:src="@drawable/icon_add"
                 android:visibility="gone" />
 
@@ -38,7 +37,6 @@
                 android:layout_width="@dimen/common_badge_icon_size"
                 android:layout_height="@dimen/common_badge_icon_size"
                 android:layout_gravity="right|top"
-                android:tint="?attr/colorStatusRed"
                 android:src="@drawable/icon_remove"
                 android:visibility="gone" />
         </FrameLayout>

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

@@ -93,9 +93,9 @@ class ISCSMCApplication : Application() {
         }
 
         // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
-        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
+//        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
         // ③ 想切到 HLK 路线(但保持对外 API 不变)
-        FaceUtil.enableHlkBackend(hlk)
+//        FaceUtil.enableHlkBackend(hlk)
         //todo 模拟器不支持 测试用,直接创建管理员账号
         FaceUtil.checkActiveStatus(SIKCore.getApplication())
         FaceUtil.initEngine(SIKCore.getApplication())

+ 1 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt

@@ -174,6 +174,7 @@ class LoginDialog(
         ActivityTracker.getCurrentActivity()?.let { context ->
             FaceUtil.checkCamera(mBinding.preview!!
             ) { bitmap, userId ->
+                if (bitmap == null || userId == null) return@checkCamera
                 viewModel.loginWithUserId(userId).observe(lifecycleOwner) {
                     if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
                         viewModel.loginWithFace(ImageConvertUtils.bitmapToBase64(bitmap).toString())

+ 1 - 0
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt

@@ -308,6 +308,7 @@ class LoginFragment : BaseFragment<FragmentLoginBinding>() {
         ActivityTracker.getCurrentActivity()?.let { context ->
             FaceUtil.checkCamera(binding.preview
             ) { bitmap, userId ->
+                if (bitmap == null || userId == null) return@checkCamera
                 viewModel.loginWithUserId(userId).observe(this) {
                     if (it == LoginResultEnum.FACE_VERIFY_FAILED) {
                         viewModel.loginWithFace(ImageConvertUtils.bitmapToBase64(bitmap).toString())

+ 14 - 5
shared/src/main/java/com/grkj/shared/utils/ImageCompress.kt

@@ -10,20 +10,20 @@ object ImageCompress {
         base64: String,
         targetBytes: Int,
         minQuality: Int = 0,         // 质量下限
-        startQuality: Int = 92,       // 初始质量
-        minSide: Int = 50            // 缩放触底,避免过小
+        startQuality: Int = 92,      // 初始质量
+        minSide: Int = 50,           // 缩放触底,避免过小
+        rotateDeg: Int = 270           // 新增:旋转角度(0、90、180、270等)
     ): ByteArray {
         require(targetBytes > 0) { "targetBytes must > 0" }
 
         val clean = base64.substringAfter(",") // 去 data uri 前缀
         val raw = android.util.Base64.decode(clean, android.util.Base64.DEFAULT)
 
-        // 任意格式解码成 Bitmap
         val src = android.graphics.BitmapFactory.decodeByteArray(raw, 0, raw.size)
             ?: error("Base64 不是有效图片")
 
         // PNG 可能带透明通道,JPEG 不支持:铺白底去 alpha
-        val bmp = if (src.hasAlpha()) {
+        val withBg = if (src.hasAlpha()) {
             val out = android.graphics.Bitmap.createBitmap(src.width, src.height, android.graphics.Bitmap.Config.ARGB_8888)
             val c = android.graphics.Canvas(out)
             c.drawColor(android.graphics.Color.WHITE)
@@ -31,6 +31,16 @@ object ImageCompress {
             out
         } else src
 
+        // 新增:旋转角度(以中心为轴)
+        val bmp = if (rotateDeg % 360 != 0) {
+            val matrix = android.graphics.Matrix().apply { postRotate(rotateDeg.toFloat()) }
+            val rotated = android.graphics.Bitmap.createBitmap(
+                withBg, 0, 0, withBg.width, withBg.height, matrix, true
+            )
+            if (rotated != withBg) withBg.recycle()
+            rotated
+        } else withBg
+
         try {
             // 先只做质量二分
             var jpeg = compressWithQualityBinarySearch(bmp, targetBytes, startQuality, minQuality)
@@ -39,7 +49,6 @@ object ImageCompress {
             // 还超:按比例缩放后再二分压缩,循环直到满足或触底
             var cur = bmp
             while (jpeg.size > targetBytes) {
-                // 估算缩放比例(乘个 0.95 留余量)
                 val ratio = kotlin.math.sqrt(targetBytes.toDouble() / jpeg.size.toDouble()) * 0.95
                 if (ratio >= 0.999) break
                 val newW = (cur.width * ratio).toInt().coerceAtLeast(minSide)

+ 1 - 0
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -475,6 +475,7 @@ object BleBusinessManager {
                         return@getTicketDetail
                     }
                     if (step?.enableLock == true) {    // 上锁工作票
+                        logger.info("当前下发分组:${MainDomainData.deviceTakeTicketGroupBound[itBO.ticketId]}")
                         sendTicketBusiness(
                             true,
                             currentModeEvent.bleBean.bleDevice.mac,

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

@@ -82,18 +82,22 @@ 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!")
+            logger.info("Device not found!,尝试更换模组")
+            try {
+                FingerprintCaptureService.register("/dev/ttyS0").also { fingerprintCaptureService = it }
+                    .listen({
+                        onScanListener?.onStart()
+                    }, {
+                        ImageConvertUtils.base64ToBitmap(it)?.let { bmp ->
+                            onScanListener?.onScan(bmp)
+                        }
+                    }, {
+                        logger.info("指纹异常:${it}")
+                    })
+            } catch (e: Exception) {
+                logger.info("指纹模组不支持")
+            }
             return
         }
         isZKDevice = true