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

feat(硬件):
- `DeviceModel`: 为CAN设备模型增加`isException`(是否异常)状态字段。
- `DeviceParseStatus`: 在解析钥匙仓和锁仓状态时,增加对`isException`状态的解析和设置。
- `CanHelper`: 在过滤可用钥匙和锁列表时,排除`isException`为`true`的设备。
- `HardwareBusinessManager`: 在处理钥匙/锁的归还逻辑前,增加对设备和仓位自身`isException`状态的检查,若异常则弹窗提示并中止操作。

refactor(硬件):
- `DeviceParseStatus`: 引入`ExistDebouncer`对CAN设备(钥匙仓、锁仓)的`isExist`状态变化进行防抖处理,防止因信号抖动导致的重复或错误触发。
- `CanReadyPlugin`, `ModBusController`: 优化了从CAN/Modbus总线读取RFID的逻辑,统一使用`toHexFromLe()`进行小端序转换,并简化了数据长度校验。
- `CanHardwareHelper`: 修正`controlKeyByKey`逻辑,确保在开/关锁后同步控制充电状态。
- `HardwareBusinessManager`: 优化了`canDeviceLockHandler`和`canDeviceKeyHandler`中的异步回调和状态管理,将`deviceChange`标志位的重置操作移至`finally`块中,确保状态无论成功失败都能被清理,避免逻辑卡死。
- `MainViewModel`: 引入`HandlerGate`并发控制机制,防止因CAN设备状态的快速、重复上报而导致对同一把钥匙的蓝牙连接和作业票读取任务被多次触发。

refactor(日志):
- `ISCSApplication`, `ISCSMCApplication`: 重构Logback初始化逻辑,改为在应用启动时动态创建日志目录,并将目录路径注入`logback.xml`,移除了XML配置文件中硬编码的路径和自动创建目录的`contextListener`。

refactor(启动):
- `SplashActivity`: 简化数据库初始化`initDatabase`的调用逻辑,确保在不同权限场景下都能正确执行。
- `BootAndUnlockReceiver`: 优化开机自启动逻辑,将延迟增加到4秒,并改用`am start`命令来启动应用,以提高启动成功率。

refactor(UI):
- `SkinManager`: 优化了`restartApp`逻辑,替换原先的`killProcess`方式为更平滑的`finishAndRemoveTask`/`finishAffinity`,以无缝重启方式应用新皮肤,改善用户体验。
- `activity_login.xml`: 为登录页的日期时间文本控件新增`colorLoginHeaderText`颜色属性,以支持主题换肤。
- `SlotsManageFragment`, `InitDeviceRegistrationKeyAndLockFragment`: 移除根据硬件模式动态设置布局的代码,统一使用`linear()`布局。

fix(UI):
- `fragment_home.xml`: 调整首页统计卡片的布局方向为`vertical`,修复文本显示不全问题。
- `activity_main.xml`: 增大了顶部用户名显示区域的`maxLength`,以容纳更长的文本。

chore:
- 将所有模块的`minSdk`从24提升至26。
- 预设数据: 将角色名称“作业负责人”更新为“上锁人”, “作业参与人”更新为“共锁人”。
- 字符串: 新增`material_manage`和`statistical_analysis`等中英文字符串资源。

周文健 3 долоо хоног өмнө
parent
commit
890f6c1aaa
43 өөрчлөгдсөн 523 нэмэгдсэн , 401 устгасан
  1. 1 1
      data/build.gradle.kts
  2. 1 1
      data/src/main/java/com/grkj/data/domain/logic/impl/HardwareLogic.kt
  3. 2 3
      data/src/main/java/com/grkj/data/hardware/can/CanCommand.kt
  4. 32 15
      data/src/main/java/com/grkj/data/hardware/can/CanHardwareHelper.kt
  5. 4 3
      data/src/main/java/com/grkj/data/hardware/can/CanHelper.kt
  6. 3 2
      data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt
  7. 5 0
      data/src/main/java/com/grkj/data/hardware/can/DeviceModel.kt
  8. 75 82
      data/src/main/java/com/grkj/data/hardware/can/DeviceParseStatus.kt
  9. 37 0
      data/src/main/java/com/grkj/data/hardware/can/ExistDebouncer.kt
  10. 17 0
      data/src/main/java/com/grkj/data/hardware/can/HandlerGate.kt
  11. 3 3
      data/src/main/java/com/grkj/data/hardware/modbus/ModBusController.kt
  12. 1 1
      iscs_lock/build.gradle.kts
  13. 8 18
      iscs_lock/src/main/assets/logback.xml
  14. 2 2
      iscs_lock/src/main/assets/preset/preset_sys_role.json
  15. 17 5
      iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt
  16. 2 7
      iscs_lock/src/main/java/com/grkj/iscs/features/init/fragment/InitDeviceRegistrationKeyAndLockFragment.kt
  17. 1 0
      iscs_lock/src/main/java/com/grkj/iscs/features/init/viewmodel/InitDeviceRegistrationKeyAndLockViewModel.kt
  18. 1 5
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/hardware_manage/SlotsManageFragment.kt
  19. 51 36
      iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt
  20. 30 24
      iscs_lock/src/main/java/com/grkj/iscs/features/splash/activity/SplashActivity.kt
  21. 26 8
      iscs_lock/src/main/java/com/grkj/iscs/receivers/BootAndUnlockReceiver.kt
  22. 1 1
      iscs_lock/src/main/res/layout-land/activity_login.xml
  23. 1 1
      iscs_lock/src/main/res/layout/activity_login.xml
  24. 2 0
      iscs_lock/src/main/res/layout/activity_splash.xml
  25. 12 12
      iscs_lock/src/main/res/layout/fragment_home.xml
  26. 8 18
      iscs_mc/src/main/assets/logback.xml
  27. 2 2
      iscs_mc/src/main/assets/preset/preset_sys_role.json
  28. 17 5
      iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  29. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/material_manage/MaterialExchangeViewModel.kt
  30. 11 5
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/splash/activity/SplashActivity.kt
  31. 1 1
      iscs_mc/src/main/res/layout-land/activity_login.xml
  32. 1 1
      iscs_mc/src/main/res/layout/activity_login.xml
  33. 1 1
      iscs_mc/src/main/res/layout/activity_main.xml
  34. 2 0
      iscs_mc/src/main/res/values-en/strings.xml
  35. 2 0
      iscs_mc/src/main/res/values-zh/strings.xml
  36. 2 0
      iscs_mc/src/main/res/values/strings.xml
  37. 1 1
      shared/build.gradle.kts
  38. 1 0
      skin/src/main/res/values/attrs.xml
  39. 2 0
      skin/src/main/res/values/theme.xml
  40. 1 1
      sync/build.gradle.kts
  41. 1 1
      ui-base/build.gradle.kts
  42. 101 121
      ui-base/src/main/java/com/grkj/ui_base/business/HardwareBusinessManager.kt
  43. 33 13
      ui-base/src/main/java/com/grkj/ui_base/skin/SkinManager.kt

+ 1 - 1
data/build.gradle.kts

@@ -10,7 +10,7 @@ android {
     compileSdk = 35
 
     defaultConfig {
-        minSdk = 24
+        minSdk = 26
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles("consumer-rules.pro")

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

@@ -96,7 +96,7 @@ class HardwareLogic @Inject constructor(
      */
     override fun getKeyInfo(rfid: String): KeyInfoRes? {
         val isKey = hardwareRepository.getKeyInfoByRfid(rfid)
-        var keyInfoRes = BeanUtils.copyProperties(isKey, KeyInfoRes::class.java)
+        val keyInfoRes = BeanUtils.copyProperties(isKey, KeyInfoRes::class.java)
         logger.info("keyInfo:${isKey},${keyInfoRes}")
         return keyInfoRes
     }

+ 2 - 3
data/src/main/java/com/grkj/data/hardware/can/CanCommand.kt

@@ -217,13 +217,12 @@ object CanCommands {
         /**
          * 批量写入 5 位控制(低5位有效),适配 5路/柜体同构
          */
-        fun setLatchBits_1to5(target: Int, isOpen: Boolean): SdoRequest.Write {
-            val v = if (isOpen) 0b0_0000 else 0b1_1111
+        fun setLatchBits_1to5(target: Int, control: Int): SdoRequest.Write {
             return SdoRequest.Write(
                 nodeId,
                 Command.CONTROL_REG,
                 0x00,
-                shortLE(v, target),
+                shortLE(control, target),
                 2,
                 timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
             )

+ 32 - 15
data/src/main/java/com/grkj/data/hardware/can/CanHardwareHelper.kt

@@ -88,7 +88,7 @@ class CanHardwareHelper : IHardwareHelper {
     override fun controlAllKeyBuckleClose(complete: () -> Unit) {
         getKeyDockData().forEach {
             val req = CanCommands.forDevice(it.addr).setLatch(true, true)
-            CanHelper.writeTo(req){
+            CanHelper.writeTo(req) {
                 complete()
             }
         }
@@ -137,13 +137,16 @@ class CanHardwareHelper : IHardwareHelper {
     override suspend fun openDoor(left: Boolean?, right: Boolean?): Boolean {
         val materialCabinets =
             CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD)
-        return if (materialCabinets.isNotEmpty()) {
-            val req = CanCommands.forDevice(materialCabinets.map { it.key }[0])
-                .controlDoorOpen(left, right)
-            CanHelper.writeTo(req)?.op != SdoOp.ERROR
-        } else {
-            false
-        }
+        val req = CanCommands.forDevice(3)
+            .controlDoorOpen(left, right)
+        return CanHelper.writeTo(req)?.op != SdoOp.ERROR
+//        return if (materialCabinets.isNotEmpty()) {
+//            val req = CanCommands.forDevice(materialCabinets.map { it.key }[0])
+//                .controlDoorOpen(left, right)
+//            CanHelper.writeTo(req)?.op != SdoOp.ERROR
+//        } else {
+//            false
+//        }
     }
 
     override fun getNewLockRFID(): List<String> {
@@ -167,7 +170,15 @@ class CanHardwareHelper : IHardwareHelper {
             val req = CanCommands.forDevice(keyDevice.nodeId)
                 .controlLatch(keyDevice.id, if (isOpen) 0 else 1)
             CanHelper.writeTo(req) {
-                done?.invoke(it.toCommMessage().payload)
+                val leftOn =
+                    if (keyDevice.id == 0 && isOpen) true else if (keyDevice.id == 0) false else null
+                val rightOn =
+                    if (keyDevice.id == 1 && isOpen) true else if (keyDevice.id == 1) false else null
+                CanCommands.forDevice(keyDevice.nodeId).setCharge(leftOn, rightOn).let {
+                    CanHelper.writeTo(it) {
+                        done?.invoke(it.toCommMessage().payload)
+                    }
+                }
             }
         }
     }
@@ -180,9 +191,12 @@ class CanHardwareHelper : IHardwareHelper {
     ) {
         slaveAddress?.let {
             val target = lockIdxList.fold(0) { acc, i ->
-                acc or (1 shl (i-1))
+                acc or (1 shl (i - 1))
             }
-            val req = CanCommands.forDevice(slaveAddress).setLatchBits_1to5(target, isOpen)
+            val v = lockIdxList.fold(0) { acc, i ->
+                acc or ((if (isOpen) 0 else 1) shl (i - 1))
+            }
+            val req = CanCommands.forDevice(slaveAddress).setLatchBits_1to5(target, v)
             CanHelper.writeTo(req) {
                 done?.invoke(it.toCommMessage().payload)
             }
@@ -212,6 +226,7 @@ class CanHardwareHelper : IHardwareHelper {
         done: ((ByteArray) -> Unit)?
     ) {
         val keyDevice = CanHelper.getKeyDeviceByMac(mac)
+        logger.info("钥匙硬件信息:${keyDevice}")
         keyDevice?.let {
             val leftOn = if (it.id == 0 && isOpen) true else if (it.id == 0) false else null
             val rightOn = if (it.id == 1 && isOpen) true else if (it.id == 1) false else null
@@ -227,7 +242,7 @@ class CanHardwareHelper : IHardwareHelper {
         val keys = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_KEY_DOCK)
         keys.forEach {
             val req = CanCommands.forDevice(it.key).setLatch(false, false)
-            CanHelper.writeTo(req){
+            CanHelper.writeTo(req) {
                 complete()
             }
         }
@@ -245,7 +260,7 @@ class CanHardwareHelper : IHardwareHelper {
         val keys = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_KEY_DOCK)
         keys.forEach {
             val req = CanCommands.forDevice(it.key).setCharge(false, false)
-            CanHelper.writeTo(req){}
+            CanHelper.writeTo(req) {}
         }
     }
 
@@ -377,8 +392,10 @@ class CanHardwareHelper : IHardwareHelper {
     ) {
         slaveAddress?.let {
             val req = CanCommands.forDevice(slaveAddress).controlLatch(idx, if (isOpen) 0 else 1)
-            CanHelper.writeTo(req) {
-                done?.invoke(it.toCommMessage().payload)
+            CanHelper.writeTo(req) {res->
+                controlKeyCharge(!isOpen, idx, slaveAddress) {
+                    done?.invoke(res.toCommMessage().payload)
+                }
             }
         }
     }

+ 4 - 3
data/src/main/java/com/grkj/data/hardware/can/CanHelper.kt

@@ -84,7 +84,7 @@ object CanHelper {
                 nodeMap[nodeId]?.let {
                     logger.debug(
                         "硬件状态:{},{},{}",
-                        index,statusData.toBinaryString(), getDeviceByDeviceType(it)
+                        index, statusData.toBinaryString(), getDeviceByDeviceType(it)
                     )
                 }
             }
@@ -134,7 +134,6 @@ object CanHelper {
         deviceChangeListeners.forEach {
             it.value.invoke(deviceData)
         }
-        deviceData.forEach { it.deviceChange = false }
     }
 
     /**
@@ -253,7 +252,8 @@ object CanHelper {
         // 1) 拿数据 + 排序(要接收返回值)
         val lockDockList = deviceData.values
             .flatten()
-            .filterIsInstance<DeviceModel.DeviceKey>()
+            .filterIsInstance<DeviceModel.CommonDevice>()
+            .filter { !it.isException }
             .sortedBy { it.nodeId }
 
         logger.info("锁具基座列表: $lockDockList")
@@ -309,6 +309,7 @@ object CanHelper {
             BleManager.disconnect(it)
         }
         var keyList = deviceData.flatMap { it.value }.filterIsInstance<DeviceModel.DeviceKey>()
+            .filter { !it.isException }
             //RFID不为空,RFID不在异常列表,mac不在异常列表,mac不为空,存在,不在归还连接列表,不在归还连接中列表
             .filterIndexed { idx, _ -> (idx + 1) !in slotCols }.filter { kb ->
                 kb.rfid !in exceptionKeysRfid && kb.mac !in exceptionKeysMac && kb.isExist && !BleReturnDispatcher.isConnected(

+ 3 - 2
data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt

@@ -9,6 +9,7 @@ import com.grkj.data.hardware.modbus.ModBusController.controlKeyBuckle
 import com.grkj.data.utils.event.ModbusInitCompleteEvent
 import com.grkj.data.utils.event.ToastEvent
 import com.grkj.shared.utils.extension.removeLeadingZeros
+import com.grkj.shared.utils.extension.toHexFromLe
 import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.comm.core.model.ProtocolState
@@ -175,11 +176,11 @@ class CanReadyPlugin : CommPlugin {
                     val readRfidReq = CanCommands.forDevice(dockBean.addr).getSlotRfid_1to5(idx)
                     CanHelper.readFrom(readRfidReq) { result ->
                         val res = result?.payload ?: return@readFrom
-                        if (res.size < 11) {
+                        if (res.size < 4) {
                             logger.error("Lock rfid error")
                             return@readFrom
                         }
-                        val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
+                        val rfid = res.toHexFromLe()
                         logger.info("初始化锁具 RFID : $rfid")
                         HardwareMode.getCurrentHardwareMode()
                             .updateLockRfid(dockBean.addr, idx, rfid)

+ 5 - 0
data/src/main/java/com/grkj/data/hardware/can/DeviceModel.kt

@@ -40,6 +40,11 @@ sealed class DeviceModel {
      */
     var isExist: Boolean = false
 
+    /**
+     * 是否异常
+     */
+    var isException: Boolean = false
+
     /**
      * 是否就绪
      */

+ 75 - 82
data/src/main/java/com/grkj/data/hardware/can/DeviceParseStatus.kt

@@ -1,139 +1,139 @@
 package com.grkj.data.hardware.can
 
-import com.sik.sikcore.bit.BitTypeUtils
-
 /**
  * 设备状态转换
  */
 object DeviceParseStatus {
+
+    private fun key(nodeId: Int, deviceType: Int, id: Int) =
+        "$nodeId-$deviceType-$id"
+
     /**
      * 钥匙仓位状态转换
      */
     fun parseKeyDockStatus(nodeId: Int, index: Int, statusData: ByteArray) {
         val deviceModel = CanHelper.getDeviceByNodeId(nodeId)
-        val leftKeyModel: DeviceModel.DeviceKey =
-            (deviceModel.getOrNull(0) ?: DeviceModel.DeviceKey().apply {
-                this.nodeId = nodeId
-                this.deviceType = CanDeviceConst.DEVICE_KEY_DOCK
-                this.id = 0
-                this.deviceChange = true
-            }) as DeviceModel.DeviceKey
-        val rightKeyModel: DeviceModel.DeviceKey =
-            (deviceModel.getOrNull(1) ?: DeviceModel.DeviceKey().apply {
-                this.nodeId = nodeId
-                this.deviceType = CanDeviceConst.DEVICE_KEY_DOCK
-                this.id = 1
-                this.deviceChange = true
-            }) as DeviceModel.DeviceKey
+        val leftKeyModel = (deviceModel.getOrNull(0) ?: DeviceModel.DeviceKey().apply {
+            this.nodeId = nodeId; this.deviceType = CanDeviceConst.DEVICE_KEY_DOCK; this.id = 0
+        }) as DeviceModel.DeviceKey
+        val rightKeyModel = (deviceModel.getOrNull(1) ?: DeviceModel.DeviceKey().apply {
+            this.nodeId = nodeId; this.deviceType = CanDeviceConst.DEVICE_KEY_DOCK; this.id = 1
+        }) as DeviceModel.DeviceKey
+
         when (index) {
             CanCommands.Command.STATUS -> {
                 require(statusData.size == 2) { "Status payload size must is 2" }
                 val leftKeyData = statusData[0]
                 val rightKeyData = statusData[1]
-                val leftKeyExists = ((leftKeyData.toInt() shr 0) and 1) == 1
-                if (leftKeyModel.isExist != leftKeyExists) {
-                    leftKeyModel.deviceChange = true
-                }
-                leftKeyModel.isExist = leftKeyExists
+
+                val leftExists = ((leftKeyData.toInt() shr 0) and 1) == 1
+                val rightExists = ((rightKeyData.toInt() shr 0) and 1) == 1
+
+                // 充电位只读,不参与 deviceChange 判定
                 leftKeyModel.isCharging = ((leftKeyData.toInt() shr 1) and 1) == 1
-                val rightKeyExists = ((rightKeyData.toInt() shr 0) and 1) == 1
-                if (rightKeyModel.isExist != rightKeyExists) {
-                    rightKeyModel.deviceChange = true
-                }
-                rightKeyModel.isExist = rightKeyExists
                 rightKeyModel.isCharging = ((rightKeyData.toInt() shr 1) and 1) == 1
+
+                var changed = false
+                ExistDebouncer.submit(key(nodeId, CanDeviceConst.DEVICE_KEY_DOCK, 0), leftExists) { stable ->
+                    if (leftKeyModel.isExist != stable) {
+                        leftKeyModel.isExist = stable
+                        leftKeyModel.deviceChange = true
+                        changed = true
+                    }
+                }
+                ExistDebouncer.submit(key(nodeId, CanDeviceConst.DEVICE_KEY_DOCK, 1), rightExists) { stable ->
+                    if (rightKeyModel.isExist != stable) {
+                        rightKeyModel.isExist = stable
+                        rightKeyModel.deviceChange = true
+                        changed = true
+                    }
+                }
+
+                // 注意:上面回调在协程里跑,这里不能立刻 update。
+                // 简单做法:把 update 挪到回调里各自执行;更优做法:聚合后再刷。
+                // 这里选“各自回调里刷”,以减少结构改动:
+                ExistDebouncer.submit(key(nodeId, CanDeviceConst.DEVICE_KEY_DOCK, 0), leftExists) { _ ->
+                    CanHelper.updateDeviceData(nodeId, listOf(leftKeyModel, rightKeyModel))
+                }
+                ExistDebouncer.submit(key(nodeId, CanDeviceConst.DEVICE_KEY_DOCK, 1), rightExists) { _ ->
+                    CanHelper.updateDeviceData(nodeId, listOf(leftKeyModel, rightKeyModel))
+                }
+                return // 避免后面再刷一次
             }
 
             CanCommands.Command.CONTROL_REG -> {
                 val keyData = statusData[0]
-                val leftKeyLocked = ((keyData.toInt() shr 0) and 1) == 1
-                if (leftKeyModel.locked != leftKeyLocked) {
-                    leftKeyModel.deviceChange = true
-                }
-                leftKeyModel.locked = leftKeyLocked
-                val rightKeyLocked = ((keyData.toInt() shr 4) and 1) == 1
-                if (rightKeyModel.locked != rightKeyLocked) {
-                    rightKeyModel.deviceChange = true
-                }
-                rightKeyModel.locked = rightKeyLocked
+                val keyExceptionData = statusData[1]
+                leftKeyModel.isException = ((keyExceptionData.toInt() shr 0) and 1) == 1
+                leftKeyModel.locked = ((keyData.toInt() shr 0) and 1) == 1
+                rightKeyModel.isException = ((keyExceptionData.toInt() shr 4) and 1) == 1
+                rightKeyModel.locked = ((keyData.toInt() shr 4) and 1) == 1
             }
         }
         CanHelper.updateDeviceData(nodeId, listOf(leftKeyModel, rightKeyModel))
     }
 
     /**
-     * 锁仓状态转换
+     * 钥匙柜控制板状态转换
      */
     fun parseLockDockStatus(nodeId: Int, index: Int, statusData: ByteArray) {
         var deviceModel = CanHelper.getDeviceByNodeId(nodeId)
         if (deviceModel.isEmpty()) {
-            deviceModel = mutableListOf<DeviceModel.CommonDevice>()
-            for (i in 0 until 5) {
-                deviceModel.add(DeviceModel.CommonDevice().apply {
-                    this.nodeId = nodeId
-                    this.deviceType = CanDeviceConst.DEVICE_LOCK_DOCK
-                    this.id = i + 1
-                    this.deviceChange = true
+            deviceModel = mutableListOf<DeviceModel.CommonDevice>().apply {
+                for (i in 0 until 5) add(DeviceModel.CommonDevice().apply {
+                    this.nodeId = nodeId; this.deviceType = CanDeviceConst.DEVICE_LOCK_DOCK; this.id = i + 1
                 })
             }
         }
         when (index) {
             CanCommands.Command.STATUS -> {
-                deviceModel.forEach {
-                    val deviceExists = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
-                    if (it.isExist != deviceExists) {
-                        it.deviceChange = true
+                deviceModel.forEach { dev ->
+                    val exists = ((statusData[0].toInt() shr (dev.id - 1)) and 1) == 1
+                    ExistDebouncer.submit("$nodeId-${CanDeviceConst.DEVICE_LOCK_DOCK}-${dev.id}", exists) { stable ->
+                        if (dev.isExist != stable) {
+                            dev.isExist = stable
+                            dev.deviceChange = true
+                            CanHelper.updateDeviceData(nodeId, deviceModel)
+                        }
                     }
-                    it.isExist = deviceExists
                 }
+                return
             }
-
             CanCommands.Command.CONTROL_REG -> {
                 deviceModel.forEach {
-                    val deviceLocked = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
-                    if (it.locked != deviceLocked) {
-                        it.deviceChange = true
-                    }
-                    it.locked = deviceLocked
+                    it.locked = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
+                    it.isException = ((statusData[1].toInt() shr (it.id - 1) + 8) and 1) == 1
                 }
             }
         }
         CanHelper.updateDeviceData(nodeId, deviceModel)
     }
 
-    /**
-     * 钥匙柜控制板状态转换
-     */
     fun parseKeyCabinetControlBoardStatus(nodeId: Int, index: Int, statusData: ByteArray) {
         var deviceModel = CanHelper.getDeviceByNodeId(nodeId)
         if (deviceModel.isEmpty()) {
-            deviceModel = mutableListOf<DeviceModel.DeviceKey>()
-            for (i in 0 until 5) {
-                deviceModel.add(DeviceModel.DeviceKey().apply {
-                    this.nodeId = nodeId
-                    this.deviceType = CanDeviceConst.DEVICE_KEY_CABINET_CONTROL_BOARD
-                    this.id = i + 1
-                    this.deviceChange = true
+            deviceModel = mutableListOf<DeviceModel.DeviceKey>().apply {
+                for (i in 0 until 5) add(DeviceModel.DeviceKey().apply {
+                    this.nodeId = nodeId; this.deviceType = CanDeviceConst.DEVICE_KEY_CABINET_CONTROL_BOARD; this.id = i + 1
                 })
             }
         }
         when (index) {
             CanCommands.Command.STATUS -> {
-                deviceModel.forEach {
-                    val keyExists = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
-                    if (it.isExist != keyExists) {
-                        it.deviceChange = true
+                deviceModel.forEach { dev ->
+                    val exists = ((statusData[0].toInt() shr (dev.id - 1)) and 1) == 1
+                    ExistDebouncer.submit("$nodeId-${CanDeviceConst.DEVICE_KEY_CABINET_CONTROL_BOARD}-${dev.id}", exists) { stable ->
+                        if (dev.isExist != stable) {
+                            dev.isExist = stable
+                            dev.deviceChange = true
+                            CanHelper.updateDeviceData(nodeId, deviceModel)
+                        }
                     }
-                    it.isExist = keyExists
                 }
+                return
             }
-
             CanCommands.Command.CONTROL_REG -> {
                 deviceModel.forEach {
-                    val keyLocked = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
-                    if (it.locked != keyLocked) {
-                        it.deviceChange = true
-                    }
                     it.locked = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
                 }
             }
@@ -152,21 +152,14 @@ object DeviceParseStatus {
                 this.nodeId = nodeId
                 this.deviceType = CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD
                 this.id = 0
-                this.deviceChange = true
             })
         }
         when (index) {
             CanCommands.Command.CONTROL_REG -> {
                 deviceModel.filterIsInstance<DeviceModel.MaterialDevice>().forEach {
                     val leftDoorLocked = ((statusData[0].toInt() shr 0) and 1) == 0
-                    if (it.leftDoorLocked != leftDoorLocked) {
-                        it.deviceChange = true
-                    }
                     it.leftDoorLocked = leftDoorLocked
                     val rightDoorLocked = ((statusData[0].toInt() shr 4) and 1) == 0
-                    if (it.leftDoorLocked != rightDoorLocked) {
-                        it.deviceChange = true
-                    }
                     it.rightDoorLocked = rightDoorLocked
                 }
             }

+ 37 - 0
data/src/main/java/com/grkj/data/hardware/can/ExistDebouncer.kt

@@ -0,0 +1,37 @@
+package com.grkj.data.hardware.can
+
+import kotlinx.coroutines.*
+import java.util.concurrent.ConcurrentHashMap
+
+internal object ExistDebouncer {
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
+
+    // 每个设备一个 job + 最新值
+    private val jobs = ConcurrentHashMap<String, Job>()
+    private val latest = ConcurrentHashMap<String, Boolean>()
+
+    // 默认防抖窗口,按你的 CAN 上报频率自己斟酌,100~300ms 都行
+    @Volatile var windowMs: Long = 300
+
+    /**
+     * @param key 唯一标识:nodeId-deviceType-id
+     * @param value 新的 exists 值
+     * @param onEmit 当 value 在窗口内稳定时触发
+     */
+    fun submit(key: String, value: Boolean, onEmit: (Boolean) -> Unit) {
+        val prev = latest.put(key, value)
+        // 如果值没变,直接忽略(你要的“只 exists 变化才更新”)
+        if (prev != null && prev == value) return
+
+        jobs[key]?.cancel()
+        jobs[key] = scope.launch {
+            delay(windowMs)
+            // 窗口结束,若期间没有被新值覆盖,则发射
+            if (latest[key] == value) onEmit(value)
+        }
+    }
+
+    fun cancel(key: String) {
+        jobs.remove(key)?.cancel()
+    }
+}

+ 17 - 0
data/src/main/java/com/grkj/data/hardware/can/HandlerGate.kt

@@ -0,0 +1,17 @@
+package com.grkj.data.hardware.can
+
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicBoolean
+
+object HandlerGate {
+    private val flags = ConcurrentHashMap<String, AtomicBoolean>()
+
+    fun tryEnter(key: String): Boolean {
+        val flag = flags.computeIfAbsent(key) { AtomicBoolean(false) }
+        return flag.compareAndSet(false, true)   // 只有第一个拿到 true
+    }
+
+    fun leave(key: String) {
+        flags[key]?.set(false)
+    }
+}

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

@@ -11,6 +11,7 @@ import com.grkj.data.utils.event.ModbusInitCompleteEvent
 import com.grkj.data.utils.event.ToastEvent
 import com.grkj.shared.utils.extension.isPureZero
 import com.grkj.shared.utils.extension.removeLeadingZeros
+import com.grkj.shared.utils.extension.toHexFromLe
 import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.shared.utils.i18n.I18nManager
 import com.huyuhui.fastble.BleManager
@@ -220,12 +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 < 11) {
+                                if (res.size < 4) {
                                     logger.error("Key rfid error")
                                     return@readKeyRfid
                                 }
-                                val rfid =
-                                    res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
+                                val rfid = res.toHexFromLe()
                                 logger.info("初始化钥匙 RFID : $rfid")
                                 // 更新rfid
                                 updateKeyRfid(dockBean.addr, key.idx, rfid)

+ 1 - 1
iscs_lock/build.gradle.kts

@@ -12,7 +12,7 @@ android {
 
     defaultConfig {
         applicationId = "com.grkj.iscs"
-        minSdk = 24
+        minSdk = 26
         targetSdk = 35
         versionCode = 1
         versionName = "v1.0.0"

+ 8 - 18
iscs_lock/src/main/assets/logback.xml

@@ -2,46 +2,36 @@
     xmlns="https://tony19.github.io/logback-android/xml"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd">
-    <property name="LOG_DIR" value="${EXTERNAL_STORAGE}/iscs/logs"/>
-    <contextListener class="ch.qos.logback.classic.joran.JoranConfigurator">
-        <onStart>
-            <mkdir dir="${LOG_DIR}" />
-        </onStart>
-    </contextListener>
-    <!-- 文件日志输出,生成每日滚动日志 -->
+
+    <!-- 这里只声明占位符,具体值由 Application 里注入 -->
+    <property name="LOG_DIR" value="${LOG_DIR}"/>
+
     <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 日志文件路径,在 Android 中存储在应用的 filesDir/logs 目录 -->
         <file>${LOG_DIR}/app.log</file>
-
-        <!-- 每日滚动日志策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
-            <maxHistory>7</maxHistory>  <!-- 最大保留 7 天日志 -->
-            <totalSizeCap>100MB</totalSizeCap>  <!-- 总日志大小限制 -->
+            <maxHistory>7</maxHistory>
+            <totalSizeCap>100MB</totalSizeCap>
         </rollingPolicy>
-
         <encoder>
             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{~36} - %msg%n</pattern>
         </encoder>
     </appender>
 
-    <!-- Logcat 输出,适用于 Android Studio 的调试 -->
     <appender name="LOGCAT" class="ch.qos.logback.classic.android.LogcatAppender">
         <encoder>
             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{~36}.%M:%line - %msg%n</pattern>
         </encoder>
     </appender>
 
-    <!-- 日志级别配置 -->
+    <!-- 你可以把 ${PACKAGE_NAME} 换成你的根包名,或者直接用 root -->
     <logger name="${PACKAGE_NAME}" level="DEBUG" additivity="false">
         <appender-ref ref="FILE" />
         <appender-ref ref="LOGCAT" />
     </logger>
 
-    <!-- 根日志器,默认级别为 INFO -->
     <root level="DEBUG">
         <appender-ref ref="FILE" />
         <appender-ref ref="LOGCAT" />
     </root>
-
-</configuration>
+</configuration>

+ 2 - 2
iscs_lock/src/main/assets/preset/preset_sys_role.json

@@ -35,7 +35,7 @@
   },
   {
     "roleId": 3,
-    "roleName": "作业负责人",
+    "roleName": "上锁人",
     "roleKey": "jtlocker",
     "roleSort": 3,
     "dataScope": "1",
@@ -52,7 +52,7 @@
   },
   {
     "roleId": 4,
-    "roleName": "作业参与人",
+    "roleName": "共锁人",
     "roleKey": "jtcolocker",
     "roleSort": 4,
     "dataScope": "1",

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

@@ -6,6 +6,7 @@ import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.LoggerContext
 import coil.Coil
 import coil.ImageLoader
 import coil.decode.SvgDecoder
@@ -43,6 +44,7 @@ import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
+import java.io.File
 
 
 /**
@@ -66,11 +68,7 @@ class ISCSApplication : Application() {
             logger.error("异常发生", it)
             true
         }
-        if (ISCSConfig.DEBUG) {
-            LogUtils.setGlobalLogLevel(Level.DEBUG)
-        } else {
-            LogUtils.setGlobalLogLevel(Level.INFO)
-        }
+        initLogger()
         System.loadLibrary("sqlcipher")   // 新库必须手动加载
         MMKV.initialize(this)
         I18nManager.attach(this)
@@ -150,6 +148,20 @@ class ISCSApplication : Application() {
         }
     }
 
+    private fun initLogger() {
+        val logDir = File(getExternalFilesDir(null), "iscs/logs")
+        if (!logDir.exists()) logDir.mkdirs()
+
+        // 把 LOG_DIR 注入给 logback.xml
+        val lc = LoggerFactory.getILoggerFactory() as LoggerContext
+        lc.putProperty("LOG_DIR", logDir.absolutePath)
+        if (ISCSConfig.DEBUG) {
+            LogUtils.setGlobalLogLevel(Level.DEBUG)
+        } else {
+            LogUtils.setGlobalLogLevel(Level.INFO)
+        }
+    }
+
     /**
      * 计划重启
      */

+ 2 - 7
iscs_lock/src/main/java/com/grkj/iscs/features/init/fragment/InitDeviceRegistrationKeyAndLockFragment.kt

@@ -78,14 +78,9 @@ class InitDeviceRegistrationKeyAndLockFragment :
             }
         }
         binding.dockRv.apply {
-            if (MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.RS485.name) == HardwareMode.RS485.name) {
-                linear()
-            } else {
-                grid(2)
-            }
+            linear()
         }.dividerSpace(
-            requireContext().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_4)
-                .toInt(), DividerOrientation.GRID
+            requireContext().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_4).toInt(), DividerOrientation.GRID
         ).setup {
             addType<DockData.KeyDock>(R.layout.item_device_registration_key_layout)
             addType<DockData.LockDock>(R.layout.item_device_registration_lock_layout)

+ 1 - 0
iscs_lock/src/main/java/com/grkj/iscs/features/init/viewmodel/InitDeviceRegistrationKeyAndLockViewModel.kt

@@ -72,6 +72,7 @@ class InitDeviceRegistrationKeyAndLockViewModel @Inject constructor(
                                 }
                             }.toMutableList()
                         )
+                        HardwareBusinessManager.unRegisterInitListener()
                         emit(true)
                     }
                 }

+ 1 - 5
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/hardware_manage/SlotsManageFragment.kt

@@ -56,11 +56,7 @@ class SlotsManageFragment : BaseFragment<FragmentSlotsManageBinding>() {
             navController.popBackStack()
         }
         binding.dockRv.apply {
-            if (MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.RS485.name) == HardwareMode.RS485.name) {
-                linear()
-            } else {
-                grid(2)
-            }
+            linear()
         }.dividerSpace(
             requireContext().resources.getDimension(com.grkj.ui_base.R.dimen.iscs_space_4)
                 .toInt(), DividerOrientation.GRID

+ 51 - 36
iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/MainViewModel.kt

@@ -17,6 +17,7 @@ import com.grkj.data.hardware.can.CanHelper
 import com.grkj.data.hardware.can.DeviceModel
 import com.grkj.data.hardware.modbus.DeviceConst
 import com.grkj.data.domain.logic.IJobTicketLogic
+import com.grkj.data.hardware.can.HandlerGate
 import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.business.BleBusinessManager
@@ -98,6 +99,10 @@ class MainViewModel @Inject constructor(
         }
     }
 
+
+    private fun devKey(nodeId: Int, deviceType: Int, id: Int) =
+        "$nodeId-$deviceType-$id"
+
     /**
      * 注册状态监听
      */
@@ -162,53 +167,63 @@ class MainViewModel @Inject constructor(
                 if (MainDomainData.userInfo == null || ISCSConfig.isDeviceRegistration) {
                     return@registerCanStatusListener
                 }
+
                 deviceDatas.filterIsInstance<DeviceModel.DeviceKey>().forEach { deviceModel ->
-                    when (deviceModel.deviceType) {
-                        CanDeviceConst.DEVICE_KEY_DOCK -> {
-                            if (deviceModel.isExist && deviceModel.deviceChange) {
-                                deviceModel.deviceChange = false
-                                deviceModel.mac.let { mac ->
+                    if (deviceModel.deviceType == CanDeviceConst.DEVICE_KEY_DOCK) {
+                        // 只在 exists 变化(解析层已做)并且当前存在时处理
+                        if (deviceModel.isExist && deviceModel.deviceChange) {
+
+                            val key = devKey(deviceModel.nodeId, deviceModel.deviceType, deviceModel.id)
+                            if (!HandlerGate.tryEnter(key)) {
+                                // 正在处理同一个设备,直接忽略这次回调
+                                return@forEach
+                            }
+
+                            // ⚠️ 不要在这里清 deviceChange;让 finally 里统一清
+                            val mac = deviceModel.mac ?: ""
+
+                            ThreadUtils.runOnIO {
+                                @SuppressLint("MissingPermission")
+                                fun readJobTicket(macAddr: String) {
                                     ThreadUtils.runOnIO {
-                                        @SuppressLint("MissingPermission")
-                                        fun readJobTicket(mac: String) {
-                                            ThreadUtils.runOnIO {
-                                                BleReturnDispatcher.submit(mac) { isConnect ->
-                                                    if (isConnect) {
-                                                        val bleBean =
-                                                            BleConnectionManager.getBleDeviceByMac(
-                                                                mac
-                                                            )
-                                                        Executor.delayOnMain(300) {
-                                                            bleBean?.let {
-                                                                LoadingEvent.sendLoadingEvent(
-                                                                    CommonUtils.getStr("loading_msg_get_ticket_status_start"),
-                                                                    true
-                                                                )
-                                                                BleConnectionManager.getCurrentStatus(
-                                                                    4,
-                                                                    it.bleDevice
-                                                                )
-                                                            }
-                                                        }
-                                                    } else {
-                                                        hideLoading()
-                                                        val req =
-                                                            CanCommands.forDevice(deviceModel.nodeId)
-                                                                .controlLatch(deviceModel.id, 0)
-                                                        CanHelper.writeTo(req) {
+                                        BleReturnDispatcher.submit(macAddr) { isConnect ->
+                                            try {
+                                                if (isConnect) {
+                                                    val bleBean = BleConnectionManager.getBleDeviceByMac(macAddr)
+                                                    Executor.delayOnMain(300) {
+                                                        bleBean?.let {
                                                             LoadingEvent.sendLoadingEvent(
-                                                                CommonUtils.getStr("ticket_get_failed")
+                                                                CommonUtils.getStr("loading_msg_get_ticket_status_start"),
+                                                                true
                                                             )
+                                                            BleConnectionManager.getCurrentStatus(4, it.bleDevice)
                                                         }
                                                     }
+                                                } else {
+                                                    hideLoading()
+                                                    val req = CanCommands.forDevice(deviceModel.nodeId)
+                                                        .controlLatch(deviceModel.id, 0)
+                                                    CanHelper.writeTo(req) {
+                                                        LoadingEvent.sendLoadingEvent(
+                                                            CommonUtils.getStr("ticket_get_failed")
+                                                        )
+                                                    }
                                                 }
+                                            } finally {
+                                                // ✅ 处理完毕再放闸 & 清一次变化标记
+                                                deviceModel.deviceChange = false
+                                                HandlerGate.leave(key)
                                             }
                                         }
-                                        if (!ISCSConfig.isDeviceRegistration) {
-                                            readJobTicket(mac)
-                                        }
                                     }
                                 }
+                                if (!ISCSConfig.isDeviceRegistration && mac.isNotEmpty()) {
+                                    readJobTicket(mac)
+                                } else {
+                                    // 无 mac 或注册模式下:也要放闸/清标记,避免卡死
+                                    deviceModel.deviceChange = false
+                                    HandlerGate.leave(key)
+                                }
                             }
                         }
                     }

+ 30 - 24
iscs_lock/src/main/java/com/grkj/iscs/features/splash/activity/SplashActivity.kt

@@ -52,32 +52,11 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
                 PermissionUtils.requestAllFilesAccessPermission {
                     logger.info("授权结果:${it}")
                     if (it) {
-                        // 已有权限的话,直接预热:
-                        CoroutineScope(Dispatchers.IO).launch {
-                            // 触发构建 + 迁移 + 打开;onOpen 回调里会 DbReadyGate.open()
-                            val db = ISCSDatabase.instance
-                            //todo 测试用,直接进入,不初始化
-//                            DbReadyGate.await()
-//                            withContext(Dispatchers.Main) {
-//                                val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
-//                                val entries = LanguageRegistry.entriesFromSources(targetRegion)
-//                                PresetData.targetRegion =
-//                                    entries.find { it.isSelected }?.region ?: targetRegion
-//                                viewModel.checkPresetData().observe(this@SplashActivity) {
-//                                    viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
-//                                        startActivity(
-//                                            Intent(
-//                                                this@SplashActivity,
-//                                                LoginActivity::class.java
-//                                            )
-//                                        )
-//                                        finish()
-//                                    }
-//                                }
-//                            }
-                        }
+                        initDatabase()
                     }
                 }
+            }else{
+                initDatabase()
             }
         }
         lifecycleScope.launch {
@@ -111,6 +90,33 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
         }
     }
 
+    private fun initDatabase(){
+        // 已有权限的话,直接预热:
+        CoroutineScope(Dispatchers.IO).launch {
+            // 触发构建 + 迁移 + 打开;onOpen 回调里会 DbReadyGate.open()
+            val db = ISCSDatabase.instance
+            //todo 测试用,直接进入,不初始化
+//                            DbReadyGate.await()
+//                            withContext(Dispatchers.Main) {
+//                                val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
+//                                val entries = LanguageRegistry.entriesFromSources(targetRegion)
+//                                PresetData.targetRegion =
+//                                    entries.find { it.isSelected }?.region ?: targetRegion
+//                                viewModel.checkPresetData().observe(this@SplashActivity) {
+//                                    viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
+//                                        startActivity(
+//                                            Intent(
+//                                                this@SplashActivity,
+//                                                LoginActivity::class.java
+//                                            )
+//                                        )
+//                                        finish()
+//                                    }
+//                                }
+//                            }
+        }
+    }
+
     fun initConfig() {
         if (LanguageStore.currentMode() == LanguageStore.Mode.FOLLOW_SYSTEM) {
             val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发

+ 26 - 8
iscs_lock/src/main/java/com/grkj/iscs/receivers/BootAndUnlockReceiver.kt

@@ -1,9 +1,13 @@
 package com.grkj.iscs.receivers
 
+import android.app.AlarmManager
+import android.app.PendingIntent
 import android.content.BroadcastReceiver
 import android.content.Context
 import android.content.Intent
+import android.os.SystemClock
 import com.grkj.iscs.features.splash.activity.SplashActivity
+import com.sik.sikcore.shell.ShellUtils
 import com.sik.sikcore.thread.ThreadUtils
 import org.slf4j.LoggerFactory
 import kotlinx.coroutines.*
@@ -13,15 +17,29 @@ class BootAndUnlockReceiver : BroadcastReceiver() {
 
     override fun onReceive(context: Context, intent: Intent?) {
         if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return
-        logger.debug("BOOT_COMPLETED(无锁设备)→ 直接拉起界面")
-
-        val app = context.applicationContext
         ThreadUtils.runOnMain {
-            delay(2000)
-            val start = Intent(app, SplashActivity::class.java).apply {
-                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
-            }
-            app.startActivity(start)
+            delay(4000)
+            logger.debug("BOOT_COMPLETED(无锁设备)→ 直接拉起界面")
+            ShellUtils.execCmd("am start --user 0 -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n com.grkj.iscs/.features.splash.activity.SplashActivity -f 0x14000000",true)
         }
+
+//        val startIntent = Intent(context, SplashActivity::class.java).apply {
+//            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
+//        }
+//
+//        val pendingIntent = PendingIntent.getActivity(
+//            context,
+//            0,
+//            startIntent,
+//            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+//        )
+//
+//        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+//        // 延迟 2 秒启动
+//        alarmManager.setExact(
+//            AlarmManager.ELAPSED_REALTIME_WAKEUP,
+//            SystemClock.elapsedRealtime() + 4000,
+//            pendingIntent
+//        )
     }
 }

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

@@ -31,7 +31,7 @@
                 android:format12Hour="yyyy-MM-dd    HH:mm"
                 android:format24Hour="yyyy-MM-dd    HH:mm"
                 android:gravity="center_vertical"
-                android:textColor="?attr/colorTextPrimary"
+                android:textColor="?attr/colorLoginHeaderText"
                 android:textSize="@dimen/iscs_text_md"
                 android:textStyle="bold|italic" />
         </FrameLayout>

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

@@ -31,7 +31,7 @@
                 android:format12Hour="yyyy-MM-dd    HH:mm"
                 android:format24Hour="yyyy-MM-dd    HH:mm"
                 android:gravity="center_vertical"
-                android:textColor="?attr/colorTextPrimary"
+                android:textColor="?attr/colorLoginHeaderText"
                 android:textSize="@dimen/iscs_text_md"
                 android:textStyle="bold|italic" />
         </FrameLayout>

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

@@ -26,6 +26,7 @@
                 android:layout_height="wrap_content"
                 android:layout_gravity="center_horizontal"
                 app:i18nKey='@{"loto"}'
+                android:gravity="center"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_h1"
                 android:textStyle="bold" />
@@ -37,6 +38,7 @@
                 android:layout_gravity="center_horizontal"
                 android:layout_marginTop="@dimen/login_sub_title_margin_top"
                 app:i18nKey='@{"loto_en"}'
+                android:gravity="center"
                 android:textColor="?attr/colorTextPrimary"
                 android:textSize="@dimen/iscs_text_h2"
                 android:textStyle="bold" />

+ 12 - 12
iscs_lock/src/main/res/layout/fragment_home.xml

@@ -165,8 +165,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockOngoing"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView
@@ -199,8 +199,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockLocked"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView
@@ -231,8 +231,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockUseHardware"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView
@@ -393,8 +393,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockOngoing"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView
@@ -425,8 +425,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockLocked"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView
@@ -457,8 +457,8 @@
                             android:background="@drawable/bg_home_card_num"
                             android:backgroundTint="?attr/colorHomeBlockUseHardware"
                             android:divider="@drawable/common_divider_small_space_horizontal"
-                            android:gravity="center_vertical"
-                            android:orientation="horizontal"
+                            android:gravity="center"
+                            android:orientation="vertical"
                             android:showDividers="middle">
 
                             <TextView

+ 8 - 18
iscs_mc/src/main/assets/logback.xml

@@ -2,46 +2,36 @@
     xmlns="https://tony19.github.io/logback-android/xml"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="https://tony19.github.io/logback-android/xml https://cdn.jsdelivr.net/gh/tony19/logback-android/logback.xsd">
-    <property name="LOG_DIR" value="${EXTERNAL_STORAGE}/iscs_mc/logs"/>
-    <contextListener class="ch.qos.logback.classic.joran.JoranConfigurator">
-        <onStart>
-            <mkdir dir="${LOG_DIR}" />
-        </onStart>
-    </contextListener>
-    <!-- 文件日志输出,生成每日滚动日志 -->
+
+    <!-- 这里只声明占位符,具体值由 Application 里注入 -->
+    <property name="LOG_DIR" value="${LOG_DIR}"/>
+
     <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
-        <!-- 日志文件路径,在 Android 中存储在应用的 filesDir/logs 目录 -->
         <file>${LOG_DIR}/app.log</file>
-
-        <!-- 每日滚动日志策略 -->
         <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
             <fileNamePattern>${LOG_DIR}/app.%d{yyyy-MM-dd}.log</fileNamePattern>
-            <maxHistory>7</maxHistory>  <!-- 最大保留 7 天日志 -->
-            <totalSizeCap>100MB</totalSizeCap>  <!-- 总日志大小限制 -->
+            <maxHistory>7</maxHistory>
+            <totalSizeCap>100MB</totalSizeCap>
         </rollingPolicy>
-
         <encoder>
             <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{~36} - %msg%n</pattern>
         </encoder>
     </appender>
 
-    <!-- Logcat 输出,适用于 Android Studio 的调试 -->
     <appender name="LOGCAT" class="ch.qos.logback.classic.android.LogcatAppender">
         <encoder>
             <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{~36}.%M:%line - %msg%n</pattern>
         </encoder>
     </appender>
 
-    <!-- 日志级别配置 -->
+    <!-- 你可以把 ${PACKAGE_NAME} 换成你的根包名,或者直接用 root -->
     <logger name="${PACKAGE_NAME}" level="DEBUG" additivity="false">
         <appender-ref ref="FILE" />
         <appender-ref ref="LOGCAT" />
     </logger>
 
-    <!-- 根日志器,默认级别为 INFO -->
     <root level="DEBUG">
         <appender-ref ref="FILE" />
         <appender-ref ref="LOGCAT" />
     </root>
-
-</configuration>
+</configuration>

+ 2 - 2
iscs_mc/src/main/assets/preset/preset_sys_role.json

@@ -35,7 +35,7 @@
   },
   {
     "roleId": 3,
-    "roleName": "作业负责人",
+    "roleName": "上锁人",
     "roleKey": "jtlocker",
     "roleSort": 3,
     "dataScope": "1",
@@ -52,7 +52,7 @@
   },
   {
     "roleId": 4,
-    "roleName": "作业参与人",
+    "roleName": "共锁人",
     "roleKey": "jtcolocker",
     "roleSort": 4,
     "dataScope": "1",

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

@@ -6,6 +6,7 @@ import android.app.PendingIntent
 import android.content.Context
 import android.content.Intent
 import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.LoggerContext
 import coil.Coil
 import coil.ImageLoader
 import coil.decode.SvgDecoder
@@ -47,6 +48,7 @@ import org.greenrobot.eventbus.Subscribe
 import org.greenrobot.eventbus.ThreadMode
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
+import java.io.File
 
 /**
  * 启动入口
@@ -71,11 +73,7 @@ class ISCSMCApplication : Application() {
             logger.error("异常发生", it)
             true
         }
-        if (ISCSConfig.DEBUG) {
-            LogUtils.setGlobalLogLevel(Level.DEBUG)
-        } else {
-            LogUtils.setGlobalLogLevel(Level.INFO)
-        }
+        initLogger()
         System.loadLibrary("sqlcipher")   // 新库必须手动加载
         I18nManager.attach(this)
         I18nManager.init(
@@ -107,6 +105,20 @@ class ISCSMCApplication : Application() {
         }
     }
 
+    private fun initLogger() {
+        val logDir = File(getExternalFilesDir(null), "iscs_mc/logs")
+        if (!logDir.exists()) logDir.mkdirs()
+
+        // 把 LOG_DIR 注入给 logback.xml
+        val lc = LoggerFactory.getILoggerFactory() as LoggerContext
+        lc.putProperty("LOG_DIR", logDir.absolutePath)
+        if (ISCSConfig.DEBUG) {
+            LogUtils.setGlobalLogLevel(Level.DEBUG)
+        } else {
+            LogUtils.setGlobalLogLevel(Level.INFO)
+        }
+    }
+
     private fun initImageLoader() {
         val loader = ImageLoader.Builder(this)
             .components { add(SvgDecoder.Factory()) }  // 支持 SVG

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/material_manage/MaterialExchangeViewModel.kt

@@ -49,7 +49,7 @@ class MaterialExchangeViewModel @Inject constructor(
      */
     fun openDoor(): LiveData<Boolean> {
         return liveData(Dispatchers.IO) {
-            emit(HardwareMode.getCurrentHardwareMode().openDoor())
+            emit(HardwareMode.getCurrentHardwareMode().openDoor(true, true))
         }
     }
 

+ 11 - 5
iscs_mc/src/main/java/com/grkj/iscs_mc/features/splash/activity/SplashActivity.kt

@@ -55,13 +55,11 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
                 PermissionUtils.requestAllFilesAccessPermission {
                     logger.info("授权结果:${it}")
                     if (it) {
-                        // 已有权限的话,直接预热:
-                        CoroutineScope(Dispatchers.IO).launch {
-                            // 触发构建 + 迁移 + 打开;onOpen 回调里会 DbReadyGate.open()
-                            val db = ISCSDatabase.instance
-                        }
+                        initDatabase()
                     }
                 }
+            } else {
+                initDatabase()
             }
         }
         lifecycleScope.launch(Dispatchers.IO) {
@@ -89,6 +87,14 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
         }
     }
 
+    private fun initDatabase() {
+        // 已有权限的话,直接预热:
+        CoroutineScope(Dispatchers.IO).launch {
+            // 触发构建 + 迁移 + 打开;onOpen 回调里会 DbReadyGate.open()
+            val db = ISCSDatabase.instance
+        }
+    }
+
     fun initConfig() {
         if (LanguageStore.currentMode() == LanguageStore.Mode.FOLLOW_SYSTEM) {
             val targetRegion = "US" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发

+ 1 - 1
iscs_mc/src/main/res/layout-land/activity_login.xml

@@ -32,7 +32,7 @@
                 android:format12Hour="yyyy-MM-dd    HH:mm"
                 android:format24Hour="yyyy-MM-dd    HH:mm"
                 android:gravity="center_vertical"
-                android:textColor="?attr/colorTextPrimary"
+                android:textColor="?attr/colorLoginHeaderText"
                 android:textSize="@dimen/iscs_text_md"
                 android:textStyle="bold|italic" />
         </FrameLayout>

+ 1 - 1
iscs_mc/src/main/res/layout/activity_login.xml

@@ -32,7 +32,7 @@
                 android:format12Hour="yyyy-MM-dd    HH:mm"
                 android:format24Hour="yyyy-MM-dd    HH:mm"
                 android:gravity="center_vertical"
-                android:textColor="?attr/colorTextPrimary"
+                android:textColor="?attr/colorLoginHeaderText"
                 android:textSize="@dimen/iscs_text_md"
                 android:textStyle="bold|italic" />
         </FrameLayout>

+ 1 - 1
iscs_mc/src/main/res/layout/activity_main.xml

@@ -73,7 +73,7 @@
                         android:layout_marginLeft="@dimen/iscs_space_2"
                         android:ellipsize="end"
                         android:gravity="center"
-                        android:maxLength="4"
+                        android:maxLength="13"
                         android:singleLine="true"
                         android:textColor="?attr/colorTextPrimary"
                         android:textSize="@dimen/iscs_text_md"

+ 2 - 0
iscs_mc/src/main/res/values-en/strings.xml

@@ -73,4 +73,6 @@
     <string name="total_material">物资\n总数</string>
     <string name="unborrowed_material">可借\n物资</string>
     <string name="you_have_borrowed_materials">您已借出{0}物资</string>
+    <string name="material_manage">物资管理</string>
+    <string name="statistical_analysis">数据统计</string>
 </resources>

+ 2 - 0
iscs_mc/src/main/res/values-zh/strings.xml

@@ -73,4 +73,6 @@
     <string name="total_material">物资\n总数</string>
     <string name="unborrowed_material">可借\n物资</string>
     <string name="you_have_borrowed_materials">您已借出{0}物资</string>
+    <string name="material_manage">物资管理</string>
+    <string name="statistical_analysis">数据统计</string>
 </resources>

+ 2 - 0
iscs_mc/src/main/res/values/strings.xml

@@ -73,4 +73,6 @@
     <string name="total_material">物资\n总数</string>
     <string name="unborrowed_material">可借\n物资</string>
     <string name="you_have_borrowed_materials">您已借出{0}物资</string>
+    <string name="material_manage">物资管理</string>
+    <string name="statistical_analysis">数据统计</string>
 </resources>

+ 1 - 1
shared/build.gradle.kts

@@ -11,7 +11,7 @@ android {
     compileSdk = 35
 
     defaultConfig {
-        minSdk = 24
+        minSdk = 26
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles("consumer-rules.pro")

+ 1 - 0
skin/src/main/res/values/attrs.xml

@@ -34,6 +34,7 @@
     <attr name="colorTextDisabled" format="color" />
 
     <!-- 首页入口 -->
+    <attr name="colorLoginHeaderText" format="color" />
     <attr name="colorHomeMenuTextColor" format="color" />
     <attr name="colorHomeMenuIVTint" format="color" />
     <attr name="colorHomeMenuBgTint" format="color" />

+ 2 - 0
skin/src/main/res/values/theme.xml

@@ -29,6 +29,7 @@
         <item name="colorTextDisabled">@color/palette_text_disabled_dark</item>
 
         <!-- 首页入口(图/文/底) -->
+        <item name="colorLoginHeaderText">@color/palette_black</item>
         <item name="colorHomeMenuTextColor">@color/palette_text_primary_dark</item>
         <item name="colorHomeMenuIVTint">@color/palette_blue</item>
         <item name="colorHomeMenuBgTint">@color/palette_white</item> <!-- 20% 白 -->
@@ -178,6 +179,7 @@
         <item name="colorTextDisabled">@color/palette_text_disabled_dark_light</item>
 
         <!-- 首页入口(图/文/底) -->
+        <item name="colorLoginHeaderText">@color/palette_white</item>
         <item name="colorHomeMenuTextColor">@color/palette_text_primary_dark_light</item>
         <item name="colorHomeMenuIVTint">@color/palette_blue_light_light</item>
         <item name="colorHomeMenuBgTint">@color/palette_white_light</item>

+ 1 - 1
sync/build.gradle.kts

@@ -10,7 +10,7 @@ android {
     compileSdk = 35
 
     defaultConfig {
-        minSdk = 24
+        minSdk = 26
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles("consumer-rules.pro")

+ 1 - 1
ui-base/build.gradle.kts

@@ -11,7 +11,7 @@ android {
     compileSdk = 35
 
     defaultConfig {
-        minSdk = 24
+        minSdk = 26
 
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
         consumerProguardFiles("consumer-rules.pro")

+ 101 - 121
ui-base/src/main/java/com/grkj/ui_base/business/HardwareBusinessManager.kt

@@ -19,6 +19,7 @@ import com.grkj.data.entity.local.DeviceTakeUpdate
 import com.grkj.data.net.req.LockTakeUpdateReq
 import com.grkj.data.utils.event.LoadingEvent
 import com.grkj.shared.utils.extension.removeLeadingZeros
+import com.grkj.shared.utils.extension.toHexFromLe
 import com.grkj.shared.utils.extension.toHexStrings
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.ui_base.listeners.CanDeviceListener
@@ -573,81 +574,75 @@ object HardwareBusinessManager {
      * Can设备
      */
     private fun canDeviceLockHandler(lockBean: DeviceModel.CommonDevice) {
-        if (!lockBean.deviceChange) {
-            return
-        }
-        lockBean.deviceChange = false
+        if (!lockBean.deviceChange) return
+
         if (lockBean.isExist) {
             val req = CanCommands.forDevice(lockBean.nodeId).getSlotRfid_1to5(lockBean.id)
             CanHelper.readFrom(req) { res ->
                 val rfidData = res?.payload ?: byteArrayOf()
-                if (rfidData.size < 11) {
+                if (rfidData.size < 4) {
                     logger.error("Lock rfid error")
+                    lockBean.deviceChange = false
                     return@readFrom
                 }
-                val rfid = rfidData.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
+                val rfid = rfidData.toHexFromLe()
                 lockBean.rfid = rfid
                 ThreadUtils.runOnIO {
-                    val lockStatusReq =
-                        async { DataBusiness.fetchDict(DictConstants.KEY_PAD_LOCK_STATUS) }
+                    val lockStatusReq = async { DataBusiness.fetchDict(DictConstants.KEY_PAD_LOCK_STATUS) }
                     val slotStatus = async { DataBusiness.fetchDict(DictConstants.KEY_SLOT_STATUS) }
                     val slotType = async { DataBusiness.fetchDict(DictConstants.KEY_SLOT_TYPE) }
                     val slotsPageReq = async { DataBusiness.getSlotsPage() }
+
                     val lockStatus = lockStatusReq.await()
                     val slotsPage = slotsPageReq.await()
                     val slotStatusList = slotStatus.await()
                     val slotTypeList = slotType.await()
+
                     LogicManager.hardwareLogic.getIsLockPage { lockData ->
-                        //锁rfid未异常正常请求锁数据,关锁
-                        if (rfid in (lockData?.records?.filter {
-                                it.exStatus == lockStatus.find {
-                                    I18nManager.t(it.dictLabel) == I18nManager.t(
-                                        "abnormal"
-                                    )
-                                }?.dictValue
-                            }?.map { it.lockNfc }?.toMutableList() ?: mutableListOf())) {
+                        val isLockAbnormal = rfid in (lockData?.records?.filter {
+                            it.exStatus == lockStatus.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("abnormal") }?.dictValue
+                        }?.map { it.lockNfc } ?: emptyList())
+
+                        val isSlotAbnormal = slotsPage?.records?.any {
+                            it.slotType == slotTypeList.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("lock") }?.dictValue &&
+                                    it.status == slotStatusList.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("abnormal") }?.dictValue &&
+                                    it.row?.toInt() == lockBean.nodeId && lockBean.id == it.col?.toInt()
+                        } == true
+
+                        if (isLockAbnormal) {
                             PopTip.build().tip(CommonUtils.getStr("lock_exception_tag"))
-                        } else if (slotsPage?.records?.filter {
-                                it.slotType == slotTypeList.find { d ->
-                                    I18nManager.t(d.dictLabel) == I18nManager.t(
-                                        "lock"
-                                    )
-                                }?.dictValue && it.status == slotStatusList.find { d ->
-                                    I18nManager.t(d.dictLabel) == I18nManager.t(
-                                        "abnormal"
-                                    )
-                                }?.dictValue
-                            }
-                                ?.find { it.row?.toInt() == lockBean.nodeId && lockBean.id == it.col?.toInt() } != null) {
+                            lockBean.deviceChange = false
+                            return@getIsLockPage
+                        }
+                        if (isSlotAbnormal) {
                             PopTip.build().tip(CommonUtils.getStr("slot_exception_tag"))
-                        } else {
-                            logger.info("挂锁归还:${lockBean.rfid}")
-                            LogicManager.hardwareLogic.getLockInfo(rfid) {
-                                logger.info("挂锁信息:${it}")
-                                if (it != null && it.lockNfc?.isNotEmpty() == true) {
-                                    // TODO 考虑快速拿取
-                                    val req = CanCommands.forDevice(lockBean.nodeId)
-                                        .controlOne_1to5(lockBean.id, true)
-                                    CanHelper.writeTo(req) { itRst ->
-                                        // 上报锁具信息
-                                        LogicManager.jobTicketLogic.updateLockReturn(
-                                            rfid, SIKCore.getApplication().serialNo()
-                                        ) {}
-                                    }
-                                }
-                                Executor.delayOnMain(200) {
-                                    canListeners.forEach { it.callBack(listOf(lockBean)) }
+                            lockBean.deviceChange = false
+                            return@getIsLockPage
+                        }
+
+                        logger.info("挂锁归还:${lockBean.rfid}")
+                        LogicManager.hardwareLogic.getLockInfo(rfid) {
+                            logger.info("挂锁信息:${it}")
+                            if (it != null && it.lockNfc?.isNotEmpty() == true) {
+                                val ctrl = CanCommands.forDevice(lockBean.nodeId).controlOne_1to5(lockBean.id, true)
+                                CanHelper.writeTo(ctrl) {
+                                    LogicManager.jobTicketLogic.updateLockReturn(
+                                        rfid, SIKCore.getApplication().serialNo()
+                                    ) {}
                                 }
                             }
+                            Executor.delayOnMain(200) {
+                                try { canListeners.forEach { it.callBack(listOf(lockBean)) } }
+                                finally { lockBean.deviceChange = false } // ✅ 最后归零
+                            }
                         }
                     }
                 }
             }
         } else {
             logger.info("挂锁取出-:${lockBean.rfid}")
-            handleDeviceTake(
-                DeviceTakeUpdateEvent(DeviceConst.DEVICE_TYPE_LOCK, lockBean.rfid), lockBean.rfid
-            )
+            handleDeviceTake(DeviceTakeUpdateEvent(DeviceConst.DEVICE_TYPE_LOCK, lockBean.rfid), lockBean.rfid)
+            lockBean.deviceChange = false // 取出分支可在同步处理后立即归零
         }
     }
 
@@ -656,107 +651,92 @@ object HardwareBusinessManager {
      * Can设备
      */
     private fun canDeviceKeyHandler(keyBean: DeviceModel.DeviceKey) {
-        if (!keyBean.deviceChange) {
-            return
-        }
-        keyBean.deviceChange = false
-        logger.info("钥匙状态变化DeviceKeyHandler:${keyBean}")
+        if (!keyBean.deviceChange) return
+
+        logger.info("钥匙状态变化canDeviceKeyHandler:$keyBean")
+
         if (keyBean.isExist) {
-            // 放回钥匙,读取rfid
-            val req = CanCommands.forDevice(keyBean.nodeId).let {
-                if (keyBean.id == 0) {
-                    it.getLeftRfid()
-                } else {
-                    it.getRightRfid()
-                }
-            }
+            val req = CanCommands.forDevice(keyBean.nodeId).let { if (keyBean.id == 0) it.getLeftRfid() else it.getRightRfid() }
             CanHelper.readFrom(req) { res ->
                 val rfidData = res?.payload ?: byteArrayOf()
                 if (ISCSConfig.isInit) {
-                    CanHelper.writeTo(
-                        CanCommands.forDevice(keyBean.nodeId)
-                            .setCharge(keyBean.id == 0, keyBean.id == 1)
-                    ){}
+                    CanHelper.writeTo(CanCommands.forDevice(keyBean.nodeId).setCharge(keyBean.id == 0, keyBean.id == 1)){}
                 }
-                if (rfidData.size < 11) {
+                if (rfidData.size < 4) {
                     logger.error("Key rfid error")
+                    keyBean.deviceChange = false
                     return@readFrom
                 }
-                val rfid = rfidData.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                logger.info("读取到的rfid:${rfid}")
+                val rfid = rfidData.toHexFromLe()
+                logger.info("读取到的rfid:$rfid")
                 keyBean.rfid = rfid
-                logger.info("更新rfid完成:${keyBean}")
+                logger.info("更新rfid完成:$keyBean")
+
                 ThreadUtils.runOnIO {
                     val slotStatus = async { DataBusiness.fetchDict(DictConstants.KEY_SLOT_STATUS) }
                     val slotType = async { DataBusiness.fetchDict(DictConstants.KEY_SLOT_TYPE) }
                     val slotsPageReq = async { DataBusiness.getSlotsPage() }
-                    val keyStatusReq =
-                        async { DataBusiness.fetchDict(DictConstants.KEY_KEY_STATUS) }
+                    val keyStatusReq = async { DataBusiness.fetchDict(DictConstants.KEY_KEY_STATUS) }
                     val keyPageReq = async { DataBusiness.getKeyPage() }
+
                     val keyStatus = keyStatusReq.await()
                     val keyData = keyPageReq.await()
                     val slotsPage = slotsPageReq.await()
                     val slotStatusList = slotStatus.await()
                     val slotTypeList = slotType.await()
-                    //锁钥匙未异常正常请求锁数据,关锁
-                    if (rfid in (keyData?.records?.filter {
-                            it.exStatus == keyStatus.find {
-                                I18nManager.t(it.dictLabel) == I18nManager.t(
-                                    "abnormal"
-                                )
-                            }?.dictValue
-                        }?.map { it.keyNfc }?.toMutableList() ?: mutableListOf())) {
-                        PopTip.build().tip(
-                            CommonUtils.getStr("key_exception_tag")
-                        )
-                    } else if (slotsPage?.records?.filter {
-                            it.slotType == slotTypeList.find { d ->
-                                I18nManager.t(d.dictLabel) == I18nManager.t(
-                                    "key"
-                                )
-                            }?.dictValue && it.status == slotStatusList.find { d ->
-                                I18nManager.t(d.dictLabel) == I18nManager.t(
-                                    "abnormal"
-                                )
-                            }?.dictValue
-                        }
-                            ?.find { it.row?.toInt() == keyBean.nodeId && it.col?.toInt() == (keyBean.nodeId + (keyBean.id) * 2 + 1) } != null) {
-                        PopTip.build().tip(
-                            CommonUtils.getStr("slot_exception_tag")
-                        )
-                    } else {
-                        // 放回钥匙,上锁
-                        val req = CanCommands.forDevice(keyBean.nodeId).controlLatch(keyBean.id, 1)
-                        CanHelper.writeTo(req) {
-                            LogicManager.hardwareLogic.getKeyInfo(rfid) {
-                                logger.info("钥匙:${rfid},${it}")
-                                if (it != null && !it.macAddress.isNullOrEmpty()) {
-                                    keyBean.mac = it.macAddress!!
-                                } else {
-                                    logger.error("Get key info fail : $rfid")
-                                    if (ISCSConfig.isInit) {
-                                        PopTip.build().tip(CommonUtils.getStr("get_key_info_fail"))
-                                    }
 
-                                    val req = CanCommands.forDevice(keyBean.nodeId)
-                                        .controlLatch(keyBean.id, 0)
-                                    CanHelper.writeTo(req){}
-                                }
-                                Executor.delayOnMain(200) {
-                                    canListeners.forEach { it.callBack(listOf(keyBean)) }
+                    val isKeyAbnormal = rfid in (keyData?.records?.filter {
+                        it.exStatus == keyStatus.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("abnormal") }?.dictValue
+                    }?.map { it.keyNfc } ?: emptyList())
+
+                    val isSlotAbnormal = slotsPage?.records?.any {
+                        it.slotType == slotTypeList.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("key") }?.dictValue &&
+                                it.status == slotStatusList.find { d -> I18nManager.t(d.dictLabel) == I18nManager.t("abnormal") }?.dictValue &&
+                                it.row?.toInt() == keyBean.nodeId &&
+                                it.col?.toInt() == (keyBean.nodeId + (keyBean.id) * 2 + 1)
+                    } == true
+
+                    if (isKeyAbnormal) {
+                        PopTip.build().tip(CommonUtils.getStr("key_exception_tag"))
+                        keyBean.deviceChange = false
+                        return@runOnIO
+                    }
+                    if (isSlotAbnormal) {
+                        PopTip.build().tip(CommonUtils.getStr("slot_exception_tag"))
+                        keyBean.deviceChange = false
+                        return@runOnIO
+                    }
+
+                    val ctrl = CanCommands.forDevice(keyBean.nodeId).controlLatch(keyBean.id, 1)
+                    CanHelper.writeTo(ctrl) {
+                        LogicManager.hardwareLogic.getKeyInfo(rfid) {
+                            logger.info("钥匙:$rfid,$it")
+                            if (it != null && !it.macAddress.isNullOrEmpty()) {
+                                keyBean.mac = it.macAddress!!
+                            } else {
+                                logger.error("Get key info fail : $rfid")
+                                if (ISCSConfig.isInit) {
+                                    PopTip.build().tip(CommonUtils.getStr("get_key_info_fail"))
                                 }
+                                val unlock = CanCommands.forDevice(keyBean.nodeId).controlLatch(keyBean.id, 0)
+                                CanHelper.writeTo(unlock){}
+                            }
+                            Executor.delayOnMain(200) {
+                                try { canListeners.forEach { l -> l.callBack(listOf(keyBean)) } }
+                                finally { keyBean.deviceChange = false } // ✅ 放最后
                             }
                         }
                     }
                 }
             }
-        } else if (!keyBean.isCharging) {//增加充电判断,防止无线充电干扰锁仓状态导致判断为取出
-            handleDeviceTake(
-                DeviceTakeUpdateEvent(DeviceConst.DEVICE_TYPE_KEY, keyBean.rfid), keyBean.rfid
-            )
+        } else if (!keyBean.isCharging) {
+            handleDeviceTake(DeviceTakeUpdateEvent(DeviceConst.DEVICE_TYPE_KEY, keyBean.rfid), keyBean.rfid)
             Executor.delayOnMain(200) {
-                canListeners.forEach { it.callBack(listOf(keyBean)) }
+                try { canListeners.forEach { it.callBack(listOf(keyBean)) } }
+                finally { keyBean.deviceChange = false } // ✅ 已有
             }
+        } else {
+            keyBean.deviceChange = false
         }
     }
 

+ 33 - 13
ui-base/src/main/java/com/grkj/ui_base/skin/SkinManager.kt

@@ -5,10 +5,14 @@ import android.app.Application
 import android.content.Context
 import android.content.Intent
 import android.content.res.AssetManager
+import android.os.Build
 import android.os.Process
 import androidx.appcompat.app.AppCompatActivity
 import androidx.appcompat.app.AppCompatDelegate
+import androidx.fragment.app.FragmentActivity
+import androidx.fragment.app.FragmentManager
 import com.grkj.skin.R
+import com.sik.sikandroid.activity.ActivityTracker
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.saveMMKVData
@@ -34,9 +38,9 @@ object SkinManager {
      * - 锁定 NightMode(不跟随系统)
      * - 冷启动重启,保证所有 Activity/Fragment 都用新主题重建
      */
-    fun setSkinAndRestart(context: Context, skin: String) {
+    fun setSkinAndRestart(activity: FragmentActivity, skin: String) {
         KEY_SUFFIX.saveMMKVData(skin)
-        restartApp(context)
+        restartApp(activity)
     }
 
     /** 若你仍想“当前界面无重启切换”,用这个:只切当前 Activity 的主题,由外部决定是否 recreate() */
@@ -74,19 +78,35 @@ object SkinManager {
         KEY_SUFFIX.getMMKVData("Default")
 
     // —— 内部:冷启动重启 —— //
-    private fun restartApp(context: Context) {
-        val appContext = context.applicationContext
-        val pm = appContext.packageManager
-        val launch = pm.getLaunchIntentForPackage(appContext.packageName) ?: return
+    private fun restartApp(activity: FragmentActivity) {
+        val app = activity.applicationContext
+        val launch = app.packageManager.getLaunchIntentForPackage(app.packageName) ?: return
+
+        // 1) 先把当前页的 Fragment 栈清干净(可选,但更稳)
+        runCatching {
+            val fm = activity.supportFragmentManager
+            fm.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+            fm.fragments.toList().forEach { f ->
+                fm.beginTransaction().remove(f).commitNowAllowingStateLoss()
+            }
+        }
+
+        // 2) 起一个“全新任务栈”的 Launcher
         launch.addFlags(
-            Intent.FLAG_ACTIVITY_NEW_TASK or
-                    Intent.FLAG_ACTIVITY_CLEAR_TASK or
-                    Intent.FLAG_ACTIVITY_CLEAR_TOP
+            Intent.FLAG_ACTIVITY_NEW_TASK or       // 新任务
+                    Intent.FLAG_ACTIVITY_CLEAR_TASK or     // 清新任务的 back stack(确保新栈干净)
+                    Intent.FLAG_ACTIVITY_CLEAR_TOP or
+                    Intent.FLAG_ACTIVITY_NO_ANIMATION
         )
-        appContext.startActivity(launch)
-        // 结束当前进程,确保冷启动(避免残留旧视图树)
-        Process.killProcess(Process.myPid())
-        Runtime.getRuntime().exit(0)
+        app.startActivity(launch)
+        activity.overridePendingTransition(0, 0)
+
+        // 3) 把“旧任务栈”整栈干掉(不杀进程)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            activity.finishAndRemoveTask()        // 同时从最近任务移除
+        } else {
+            activity.finishAffinity()             // API<21 退化为整栈 finish
+        }
     }
 }