GrayCarbon 6 месяцев назад
Родитель
Сommit
109a015d24
31 измененных файлов с 2626 добавлено и 470 удалено
  1. 3 2
      app/build.gradle
  2. 20 2
      app/src/main/AndroidManifest.xml
  3. 178 289
      app/src/main/java/com/grkj/iscs_mars/BusinessManager.kt
  4. 6 2
      app/src/main/java/com/grkj/iscs_mars/MyApplication.kt
  5. 343 0
      app/src/main/java/com/grkj/iscs_mars/can/CanCommand.kt
  6. 30 0
      app/src/main/java/com/grkj/iscs_mars/can/CanDeviceConst.kt
  7. 460 0
      app/src/main/java/com/grkj/iscs_mars/can/CanHardwareHelper.kt
  8. 378 0
      app/src/main/java/com/grkj/iscs_mars/can/CanHelper.kt
  9. 361 0
      app/src/main/java/com/grkj/iscs_mars/can/CanReadyPlugin.kt
  10. 48 0
      app/src/main/java/com/grkj/iscs_mars/can/CanSendDelayInterceptor.kt
  11. 36 0
      app/src/main/java/com/grkj/iscs_mars/can/CustomCanConfig.kt
  12. 117 0
      app/src/main/java/com/grkj/iscs_mars/can/DeviceModel.kt
  13. 158 0
      app/src/main/java/com/grkj/iscs_mars/can/DeviceParseStatus.kt
  14. 61 0
      app/src/main/java/com/grkj/iscs_mars/can/DockData.kt
  15. 17 0
      app/src/main/java/com/grkj/iscs_mars/can/HandlerGate.kt
  16. 256 0
      app/src/main/java/com/grkj/iscs_mars/can/IHardwareHelper.kt
  17. 67 0
      app/src/main/java/com/grkj/iscs_mars/can/NodeIdHelper.kt
  18. 23 0
      app/src/main/java/com/grkj/iscs_mars/enums/HardwareMode.kt
  19. 20 0
      app/src/main/java/com/grkj/iscs_mars/extentions/ByteArray.kt
  20. 1 1
      app/src/main/java/com/grkj/iscs_mars/extentions/Context.kt
  21. 11 58
      app/src/main/java/com/grkj/iscs_mars/presentation/PresentationLoginActivity.kt
  22. 0 16
      app/src/main/java/com/grkj/iscs_mars/presentation/PresentationPresenter.kt
  23. 4 2
      app/src/main/java/com/grkj/iscs_mars/util/CrashUtil.kt
  24. 4 3
      app/src/main/java/com/grkj/iscs_mars/util/NetHttpManager.kt
  25. 1 0
      app/src/main/java/com/grkj/iscs_mars/view/activity/HomeActivity.kt
  26. 3 1
      app/src/main/java/com/grkj/iscs_mars/view/fragment/DeviceRegistrationTypeSelectFragment.kt
  27. 15 21
      app/src/main/java/com/grkj/iscs_mars/view/fragment/DockTestFragment.kt
  28. 1 0
      app/src/main/java/com/grkj/iscs_mars/view/presenter/DeviceStatusPresenter.kt
  29. 1 49
      app/src/main/java/com/grkj/iscs_mars/view/presenter/HomePresenter.kt
  30. 2 23
      app/src/main/java/com/grkj/iscs_mars/view/presenter/LoginPresenter.kt
  31. 1 1
      gradle/libs.versions.toml

+ 3 - 2
app/build.gradle

@@ -7,7 +7,7 @@ plugins {
 
 android {
     namespace 'com.grkj.iscs_mars'
-    compileSdk 34
+    compileSdk 35
     viewBinding.enabled = true
 
     dataBinding {
@@ -17,7 +17,7 @@ android {
     defaultConfig {
         applicationId "com.grkj.iscs_mars"
         minSdk 24
-        targetSdk 34
+        targetSdk 35
         versionCode 1
         versionName "1.0.1"
 
@@ -171,4 +171,5 @@ dependencies {
     implementation 'com.github.SilverIceKey:SIKExtension:1.1.76'
     implementation 'com.github.liangjingkanji:BRV:1.6.1'
     implementation("com.github.SilverIceKey:SIKCronJob:1.0.5")
+    implementation("com.github.SilverIceKey:SIKComm:1.0.20")
 }

+ 20 - 2
app/src/main/AndroidManifest.xml

@@ -40,20 +40,25 @@
         tools:targetApi="31">
         <activity
             android:name=".view.activity.test.face.arcsoft.ArcsoftTestActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.test.face.FaceActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.test.fingerprint.FingerPrintActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.HomeActivity"
             android:exported="false"
+
             android:windowSoftInputMode="adjustPan|adjustResize" />
         <activity
             android:name=".view.activity.LoginActivity"
             android:exported="true"
+
             android:launchMode="singleTask"
             android:windowSoftInputMode="stateHidden|adjustPan">
             <intent-filter>
@@ -64,35 +69,46 @@
         </activity>
         <activity
             android:name=".view.activity.CreateTicketActivity"
+
             android:exported="false" />
         <activity
             android:name=".presentation.CurrentTicketActivity"
+
             android:exported="false" />
         <activity
             android:name=".presentation.LockerActivity"
+
             android:exported="false" />
         <activity
             android:name=".presentation.LockerTogetherActivity"
+
             android:exported="false" />
         <activity
             android:name=".presentation.PresentationLoginActivity"
+
             android:exported="false" />
         <activity
             android:name=".presentation.PresentationActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.test.WidgetTestActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.test.RfidActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.test.WebSocketActivity"
+
             android:exported="false" />
+
         <service
             android:name="com.sik.cronjob.services.TaskService"
             android:exported="false"
-            android:process=":CronJobService"/>
+            android:process=":CronJobService" />
+
         <meta-data
             android:name="design_width_in_dp"
             android:value="640" />
@@ -102,10 +118,12 @@
 
         <activity
             android:name=".view.activity.test.ModbusActivity"
+
             android:exported="false" />
         <activity
             android:name=".view.activity.MainActivity"
-            android:exported="true"></activity>
+
+            android:exported="true" />
 
         <receiver
             android:name=".receivers.BootReceiver"

+ 178 - 289
app/src/main/java/com/grkj/iscs_mars/BusinessManager.kt

@@ -3,6 +3,7 @@ package com.grkj.iscs_mars
 import android.bluetooth.BluetoothGattCharacteristic
 import android.content.Context
 import android.content.Intent
+import android.util.Log
 import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.MutableLiveData
 import com.google.gson.Gson
@@ -16,9 +17,16 @@ import com.grkj.iscs_mars.ble.BleReturnDispatcher
 import com.grkj.iscs_mars.ble.BleSendDispatcher
 import com.grkj.iscs_mars.ble.BleUtil
 import com.grkj.iscs_mars.ble.CustomBleWriteCallback
-import com.grkj.iscs_mars.extentions.removeLeadingZeros
+import com.grkj.iscs_mars.can.CanCommands
+import com.grkj.iscs_mars.can.CanDeviceConst
+import com.grkj.iscs_mars.can.CanHardwareHelper
+import com.grkj.iscs_mars.can.CanHelper
+import com.grkj.iscs_mars.can.DeviceModel
+import com.grkj.iscs_mars.can.HandlerGate
+import com.grkj.iscs_mars.enums.HardwareMode
 import com.grkj.iscs_mars.extentions.serialNo
 import com.grkj.iscs_mars.extentions.startsWith
+import com.grkj.iscs_mars.extentions.toHexFromLe
 import com.grkj.iscs_mars.extentions.toHexStrings
 import com.grkj.iscs_mars.modbus.DockBean
 import com.grkj.iscs_mars.modbus.ModBusController
@@ -26,11 +34,8 @@ import com.grkj.iscs_mars.modbus.ModBusController.dockList
 import com.grkj.iscs_mars.modbus.ModBusController.getOneKey
 import com.grkj.iscs_mars.model.Constants.USER_TYPE_LOCKER
 import com.grkj.iscs_mars.model.DeviceConst
-import com.grkj.iscs_mars.model.DeviceConst.DEVICE_TYPE_CARD
-import com.grkj.iscs_mars.model.DeviceConst.DEVICE_TYPE_FINGERPRINT
 import com.grkj.iscs_mars.model.DeviceConst.DEVICE_TYPE_KEY
 import com.grkj.iscs_mars.model.DeviceConst.DEVICE_TYPE_LOCK
-import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_ELEC_LOCK_BOARD
 import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_LOCK
 import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_PORTABLE
@@ -64,7 +69,6 @@ import com.grkj.iscs_mars.model.vo.lock.LockPageRespVO
 import com.grkj.iscs_mars.model.vo.lock.LockTakeUpdateReqVO
 import com.grkj.iscs_mars.model.vo.ticket.LockPointUpdateReqVO
 import com.grkj.iscs_mars.model.vo.ticket.TicketDetailRespVO
-import com.grkj.iscs_mars.service.CheckKeyInfoTask
 import com.grkj.iscs_mars.util.ActivityUtils
 import com.grkj.iscs_mars.util.CommonUtils
 import com.grkj.iscs_mars.util.Executor
@@ -77,7 +81,6 @@ import com.grkj.iscs_mars.view.base.BaseActivity
 import com.grkj.iscs_mars.view.dialog.TipDialog
 import com.huyuhui.fastble.data.BleDevice
 import com.huyuhui.fastble.exception.BleException
-import com.sik.cronjob.managers.CronJobScanner
 import com.sik.sikandroid.activity.ActivityTracker
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.date.TimeUtils
@@ -102,9 +105,7 @@ object BusinessManager {
     var deviceList: MutableList<BleBean> = mutableListOf()
 
     // Modbus数据页面监听
-    class DeviceListener(
-        val key: Any, val callBack: (DockBean) -> Unit
-    )
+    class DeviceListener(val key: Any, val callBack: (List<DeviceModel>) -> Unit)
 
     private val listeners = ArrayList<DeviceListener>()
 
@@ -135,6 +136,8 @@ object BusinessManager {
      */
 //    var checkKeyInfoTask: CheckKeyInfoTask = CheckKeyInfoTask()
 
+    var canManager: CanHardwareHelper? = null
+
     /**
      * 初始化消息总线
      */
@@ -259,18 +262,13 @@ object BusinessManager {
      * 检查我的待办
      */
     suspend fun checkMyTodoForHandleKey(mac: String? = null, delayMillis: Long = 60_0000): Boolean {
-        if (!CAN_RETURN) {
-            return false
-        }
-        val result = suspendCancellableCoroutine<Boolean> { cont ->
+        if (!CAN_RETURN) return false
+        val result = suspendCancellableCoroutine { cont ->
             NetApi.getMySelfState {
                 if (it) {
                     LogUtil.i("蓝牙连接-存在上锁解锁待办")
                     if (BleSendDispatcher.canConnect()) {
                         LogUtil.i("蓝牙连接-发送队列可以连接")
-                        mac?.let {
-//                            connectExistsKey(listOf(it))
-                        }
                     }
                 } else {
                     LogUtil.i("蓝牙连接-没有待办数据,延迟断开所有发送连接")
@@ -284,70 +282,36 @@ object BusinessManager {
         return result
     }
 
-    /**
-     * 连接一把存在的可连接的钥匙
-     */
-    fun connectExistsKey(exceptKeyMac: List<String> = listOf()) {
-        ThreadUtils.runOnIO {
-            // —— 串行请求1 & 2 ——
-            val slotsPage = getSlotsPage()
-
-            // —— 并行加载字典(或按需串行也行) ——
-            val slotStatus =
-                async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_STATUS) }
-            val keyStatus =
-                async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_KEY_STATUS) }
-            val slotType =
-                async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_TYPE) }
-
-            // 等待字典加载完成
-            val slotStatusList = slotStatus.await()
-            val keyStatusList = keyStatus.await()
-            val slotTypeList = slotType.await()
-            withContext(Dispatchers.Default) {
-                val keyPage = withContext(Dispatchers.IO) { getKeyPage() }
-                getOneKey(
-                    slotsPage?.records?.filter {
-                        it.slotType == slotTypeList.find { d -> d.dictLabel == "钥匙" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
-                    }?.toMutableList() ?: mutableListOf(),
-                    (keyPage?.records?.filter { it.exStatus == keyStatusList.find { d -> d.dictLabel == "异常" }?.dictValue }
-                        ?.map { it.keyNfc ?: "" }?.toMutableList() ?: mutableListOf()),
-                    exceptKeyMac
-                )
-            }
-        }
-    }
-
-    /****************************************** ModBus ******************************************/
+    /****************************************** CAN ******************************************/
     /**
      * 链接底座
      */
     fun connectDock(isNeedInit: Boolean = false) {
         val dockData = SPUtils.getDockConfig(SIKCore.getApplication())
         LogUtil.i("基座数据:${dockData}")
-        if (dockData?.isEmpty() == true || dockData == "[]") {
-            return
-        }
-        ModBusController.interruptReadTrashBinStatus(false)
-        ModBusController.start(MyApplication.instance!!.applicationContext)
-        ModBusController.unregisterListener(MyApplication.instance!!.applicationContext)
-
-        if (isNeedInit) {
-            ModBusController.initDevicesStatus()
+        if (dockData?.isEmpty() == true || dockData == "[]") return
+        if (canManager == null) {
+            canManager = CanHardwareHelper()
+            // CAN总线连接和监听
+            canManager?.interruptReadTrashBinStatus(true)
+            canManager?.connectAndAddListener()
+            CanHelper.addDeviceChangeListener(this) { canDeviceStatusHandle(it) }
         }
+        // 是否需要初始化Dock数据
+        // if (isNeedInit) initDevicesStatus()
     }
 
     /**
      * 断开底座链接
      */
     fun disconnectDock() {
-        ModBusController.stop()
+        canManager?.disconnect()
     }
 
     /**
      * 注册状态监听
      */
-    fun registerStatusListener(key: Any, listener: (DockBean) -> Unit) {
+    fun registerStatusListener(key: Any, listener: (List<DeviceModel>) -> Unit) {
         listeners.add(DeviceListener(key, listener))
     }
 
@@ -381,11 +345,12 @@ object BusinessManager {
      * 总的监听,做预处理,其余的所有监听均使用本监听处理后的数据,只允许调用一次
      */
     fun registerMainListener() {
-        ModBusController.registerStatusListener(this) { res ->
-            deviceStatusHandle(res)
+        CanHelper.addDeviceChangeListener(this) { res ->
+            canDeviceStatusHandle(res)
         }
     }
 
+
     /**
      * 硬件状态
      * 1、检测到有钥匙
@@ -394,118 +359,147 @@ object BusinessManager {
      * 4、蓝牙连接
      * 5、蓝牙数据通讯
      */
-    private fun deviceStatusHandle(res: Any) {
-        LogUtil.d("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
-        if (res.isEmpty() || res.any { it.isEmpty() }) {
-            var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
-            val addressList = mutableListOf<String>()
-
-            ModBusController.modBusManager?.mSlaveAddressList?.forEach { itDock ->
-                if (res.none { it.isNotEmpty() && it[0] == itDock }) {
-                    addressList.add("0x${String.format("%02X", itDock)}")
+    private fun canDeviceStatusHandle(res: List<DeviceModel>) {
+        if (res.isEmpty()) return
+        val deviceType = CanHelper.getDeviceTypeByNodeId(res[0].nodeId)
+        when (deviceType) {
+            // 钥匙底座
+            CanDeviceConst.DEVICE_KEY_DOCK -> {
+                if (!CAN_RETURN) return
+                res.filterIsInstance<DeviceModel.DeviceKey>().forEach {
+                    canDeviceKeyHandler(it)
                 }
             }
-            tipStr += addressList
-            ToastUtils.tip(tipStr)
-        }
-        res.forEachIndexed { index, bytes ->
-            val dockBean = ModBusController.updateStatus(bytes) ?: return@forEachIndexed
-            ModBusController.isInitReady = true
-            when (dockBean.type) {
-                DOCK_TYPE_KEY -> {
-                    if (!CAN_RETURN) {
-                        return@forEachIndexed
-                    }
-                    dockBean.getKeyList().forEach { keyBean ->
-                        deviceKeyHandler(dockBean, keyBean)
-                    }
+            // 锁底座
+            CanDeviceConst.DEVICE_LOCK_DOCK -> {
+                if (!CAN_RETURN) return
+                res.filterIsInstance<DeviceModel.CommonDevice>().forEach {
+                    canDeviceLockHandler(it)
                 }
+            }
 
-                DOCK_TYPE_LOCK -> {
-                    if (!CAN_RETURN) {
-                        return@forEachIndexed
-                    }
-                    dockBean.getLockList().forEach { lockBean ->
-                        deviceLockHandler(dockBean, lockBean)
-                    }
-                }
+            CanDeviceConst.DEVICE_KEY_CABINET_CONTROL_BOARD -> {
+                // TODO 钥匙柜主控板
+            }
 
-                DOCK_TYPE_ELEC_LOCK_BOARD -> {
-                    // TODO 占位
-                }
+            CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD -> {
+                // TODO 物资柜主控板
+            }
+        }
+        Executor.delayOnMain(200) {
+            listeners.forEach { it.callBack(res) }
+        }
+        Executor.delayOnMain(200) {
+            if (ISCSDomainData.isDeviceRegistration) {
+                initListener?.invoke()
+            }
+        }
+    }
 
-                DOCK_TYPE_PORTABLE -> {
-                    // TODO 便携式待完善
-                    dockBean.deviceList.forEach { deviceBean ->
-                        if (deviceBean.isExist) {
-                            when (deviceBean.type) {
-                                DEVICE_TYPE_KEY -> {
-                                    if (!CAN_RETURN) {
-                                        return@forEachIndexed
-                                    }
-                                    deviceKeyHandler(dockBean, deviceBean as DockBean.KeyBean)
-                                }
 
-                                DEVICE_TYPE_LOCK -> {
-                                    if (!CAN_RETURN) {
-                                        return@forEachIndexed
-                                    }
-                                    deviceLockHandler(dockBean, deviceBean as DockBean.LockBean)
-                                }
+    /**
+     * 钥匙设备处理
+     * Can设备
+     */
+    private fun canDeviceKeyHandler(keyBean: DeviceModel.DeviceKey) {
+        if (!keyBean.deviceChange) return
+        Log.i("Manager", "钥匙状态变化canDeviceKeyHandler:$keyBean")
+        if (keyBean.isExist) {
+            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 (!ISCSDomainData.isDeviceRegistration) {
+                    CanHelper.writeTo(
+                        CanCommands.forDevice(keyBean.nodeId)
+                            .setCharge(keyBean.id == 0, keyBean.id == 1)
+                    ) {}
+                }
+                if (rfidData.size < 4) {
+                    Log.e("Manager", "Key rfid error")
+                    keyBean.deviceChange = false
+                    return@readFrom
+                }
+                val rfid = rfidData.toHexFromLe()
+                Log.i("Manager", "读取到的rfid:$rfid")
+                keyBean.rfid = rfid
+                Log.i("Manager", "更新rfid完成:$keyBean")
 
-                                DEVICE_TYPE_CARD -> {
-                                    ModBusController.readPortalCaseCardRfid(dockBean.addr) { res ->
-                                        if (res.size < 11) {
-                                            LogUtil.e("Portal Case card rfid error")
-                                            return@readPortalCaseCardRfid
-                                        }
-                                        val rfid = res.copyOfRange(3, 11).toHexStrings(false)
-                                            .removeLeadingZeros()
-                                        LogUtil.i("卡片RFID : $rfid")
-                                    }
-                                }
+                ThreadUtils.runOnIO {
+                    val slotStatus = async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_STATUS) }
+                    val slotType = async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_TYPE) }
+                    val slotsPageReq = async { getSlotsPage() }
+                    val keyStatusReq =
+                        async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_KEY_STATUS) }
+                    val keyPageReq = async { getKeyPage() }
 
-                                DEVICE_TYPE_FINGERPRINT -> {
+                    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 { it.dictLabel == "异常" }?.dictValue }
+                            ?.map { it.keyNfc }?.toMutableList() ?: mutableListOf())) {
+                        ToastUtils.tip(
+                            MyApplication.instance?.applicationContext!!.getString(
+                                R.string.key_exception_tag
+                            )
+                        )
+                    } else if (slotsPage?.records?.filter {
+                            it.slotType == slotTypeList.find { d -> d.dictLabel == "钥匙" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
+                        }
+                            ?.find { it.row?.toInt() == keyBean.nodeId && it.col?.toInt() == (keyBean.nodeId + (keyBean.id) * 2 + 1) } != null) {
+                        ToastUtils.tip(
+                            MyApplication.instance?.applicationContext!!.getString(
+                                R.string.slot_exception_tag
+                            )
+                        )
+                    } else {
+                        val ctrl = CanCommands.forDevice(keyBean.nodeId).controlLatch(keyBean.id, 1)
+                        CanHelper.writeTo(ctrl) { _ ->
+                            NetApi.getKeyInfo(rfid) {
+                                // TODO QUEST ModBusController.updateKeyNewHardware(dockBean.addr, true, it == null)
+                                if (it != null && !it.macAddress.isNullOrEmpty()) {
+                                    keyBean.mac = it.macAddress
+                                } else {
+                                    LogUtil.e("Get key info fail : $rfid")
+                                    if (!ISCSDomainData.isDeviceRegistration) {
+                                        ToastUtils.tip(R.string.get_key_info_fail)
+                                    }
+                                    val unlock = CanCommands.forDevice(keyBean.nodeId).controlLatch(keyBean.id, 0)
+                                    CanHelper.writeTo(unlock) {}
                                 }
                             }
                         }
                     }
                 }
-
-                DeviceConst.DOCK_TYPE_COLLECT -> {
-                    ModBusController.switchStatus(bytes) {}
-                }
-            }
-            Executor.delayOnMain(200) {
-                if (!ISCSDomainData.isDeviceRegistration) {
-                    listeners.forEach { it.callBack(dockBean) }
-                }
-            }
-        }
-        Executor.delayOnMain(200) {
-            if (ISCSDomainData.isDeviceRegistration) {
-                initListener?.invoke()
             }
+        } else if (!keyBean.isCharging) {
+            unregisterConnectListener(keyBean.mac)
+            sendEventMsg(MsgEvent(MSG_EVENT_DEVICE_TAKE_UPDATE, DeviceTakeUpdateMsg(DEVICE_TYPE_KEY, keyBean.rfid)))
         }
     }
 
     /**
-     * 挂锁处理
+     * 设备挂锁处理
+     * Can设备
      */
-    private fun deviceLockHandler(
-        dockBean: DockBean, lockBean: DockBean.LockBean
-    ) {
+    private fun canDeviceLockHandler(lockBean: DeviceModel.CommonDevice) {
+        if (!lockBean.deviceChange) return
         if (lockBean.isExist) {
-            ModBusController.readLockRfid(dockBean.addr, lockBean.idx) { res ->
-                if (res.size < 11) {
-                    LogUtil.e("Lock rfid error")
-                    return@readLockRfid
-                }
-                val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                ModBusController.updateLockRfid(
-                    dockBean.addr, lockBean.idx, rfid
-                )
+            val req = CanCommands.forDevice(lockBean.nodeId).getSlotRfid_1to5(lockBean.id)
+            CanHelper.readFrom(req) { res ->
+                val rfidData = res?.payload ?: byteArrayOf()
+                if (rfidData.size < 4) {
+                    LogUtil.i("Lock rfid error")
+                    lockBean.deviceChange = false
+                    return@readFrom
+                }
+                val rfid = rfidData.toHexFromLe()
+                lockBean.rfid = rfid
                 ThreadUtils.runOnIO {
                     val lockStatusReq =
                         async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_PAD_LOCK_STATUS) }
@@ -514,10 +508,12 @@ object BusinessManager {
                     val slotType =
                         async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_TYPE) }
                     val slotsPageReq = async { getSlotsPage() }
-                    var lockStatus = lockStatusReq.await()
+                    val lockStatus = lockStatusReq.await()
                     val slotsPage = slotsPageReq.await()
                     val slotStatusList = slotStatus.await()
                     val slotTypeList = slotType.await()
+
+
                     NetApi.getIsLockPage { lockData ->
                         //锁rfid未异常正常请求锁数据,关锁
                         if (rfid in (lockData?.records?.filter { it.exStatus == lockStatus.find { it.dictLabel == "异常" }?.dictValue }
@@ -530,26 +526,21 @@ object BusinessManager {
                         } else if (slotsPage?.records?.filter {
                                 it.slotType == slotTypeList.find { d -> d.dictLabel == "锁" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
                             }
-                                ?.find { it.row?.toInt() == dockBean.row && (lockBean.idx + 1) == it.col?.toInt() } != null) {
+                                ?.find { it.row?.toInt() == lockBean.nodeId && lockBean.id == it.col?.toInt() } != null) {
                             ToastUtils.tip(
                                 MyApplication.instance?.applicationContext!!.getString(
                                     R.string.slot_exception_tag
                                 )
                             )
                         } else {
-                            NetApi.getLockInfo(rfid) {
-                                if (it != null) {
+                            NetApi.getLockInfo(rfid) { lockInfo ->
+                                if (lockInfo != null) {
                                     NetApi.getLockStateByNfc(rfid) {
                                         if (it != 1) {
-                                            ModBusController.controlLockBuckle(
-                                                false, dockBean.addr, lockBean.idx
-                                            ) { itRst ->
-                                                if (itRst.isNotEmpty()) {
-                                                    // 上报锁具信息
-                                                    NetApi.updateLockReturn(
-                                                        rfid, MyApplication.instance!!.serialNo()
-                                                    ) {}
-                                                }
+                                            val ctrl = CanCommands.forDevice(lockBean.nodeId).controlOne_1to5(lockBean.id, true)
+                                            CanHelper.writeTo(ctrl) {
+                                                // 上报锁具信息
+                                                NetApi.updateLockReturn(rfid, MyApplication.instance!!.serialNo()) {}
                                             }
                                         } else {
                                             ToastUtils.tip(CommonUtils.getStr(R.string.ticket_not_finish_can_not_return_lock))
@@ -563,105 +554,8 @@ object BusinessManager {
             }
         } else {
             LogUtil.i("挂锁取出-:${lockBean.rfid}")
-            sendEventMsg(
-                MsgEvent(
-                    MSG_EVENT_DEVICE_TAKE_UPDATE,
-                    DeviceTakeUpdateMsg(DEVICE_TYPE_LOCK, lockBean.rfid)
-                )
-            )
-        }
-    }
-
-    private fun deviceKeyHandler(dockBean: DockBean, keyBean: DockBean.KeyBean) {
-        if (keyBean.isExist) {
-            // 放回钥匙,读取rfid
-            ModBusController.readKeyRfid(
-                dockBean.addr, if (keyBean.isLeft) 0 else 1
-            ) { isLeft, res ->
-                if (!ISCSDomainData.isDeviceRegistration) {
-                    ModBusController.controlKeyCharge(
-                        true, keyBean.isLeft, dockBean.addr
-                    )
-                }
-                if (res.size < 11) {
-                    LogUtil.e("Key rfid error")
-                    return@readKeyRfid
-                }
-                val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                ThreadUtils.runOnIO {
-                    val slotStatus =
-                        async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_STATUS) }
-                    val slotType =
-                        async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_SLOT_TYPE) }
-                    val slotsPageReq = async { getSlotsPage() }
-                    val keyStatusReq =
-                        async { fetchDict<CommonDictRespVO>(DictAndSystemConstants.KEY_KEY_STATUS) }
-                    val keyPageReq = async { getKeyPage() }
-                    var keyStatus = keyStatusReq.await()
-                    var 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 { it.dictLabel == "异常" }?.dictValue }
-                            ?.map { it.keyNfc }?.toMutableList() ?: mutableListOf())) {
-                        ToastUtils.tip(
-                            MyApplication.instance?.applicationContext!!.getString(
-                                R.string.key_exception_tag
-                            )
-                        )
-                    } else if (slotsPage?.records?.filter {
-                            it.slotType == slotTypeList.find { d -> d.dictLabel == "钥匙" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
-                        }
-                            ?.find { it.row?.toInt() == dockBean.row && it.col?.toInt() == (dockBean.col + (if (keyBean.isLeft) 0 else 1) * 2) } != null) {
-                        ToastUtils.tip(
-                            MyApplication.instance?.applicationContext!!.getString(
-                                R.string.slot_exception_tag
-                            )
-                        )
-                    } else {
-                        ModBusController.updateKeyRfid(
-                            dockBean.addr, keyBean.isLeft, rfid
-                        )
-                        // 放回钥匙,上锁
-                        ModBusController.controlKeyBuckle(
-                            false, keyBean.isLeft, dockBean.addr
-                        ) {
-                            NetApi.getKeyInfo(rfid) {
-                                ModBusController.updateKeyNewHardware(
-                                    dockBean.addr, true, it == null
-                                )
-                                if (it != null && !it.macAddress.isNullOrEmpty()) {
-                                    ModBusController.updateKeyMac(
-                                        dockBean.addr, keyBean.isLeft, it.macAddress
-                                    )
-                                    ModBusController.updateKeyReadyStatus(
-                                        it.macAddress, false, 5
-                                    )
-                                } else {
-                                    LogUtil.e("Get key info fail : $rfid")
-                                    if (!ISCSDomainData.isDeviceRegistration) {
-                                        ToastUtils.tip(R.string.get_key_info_fail)
-                                    }
-                                    ModBusController.controlKeyBuckle(
-                                        true, keyBean.isLeft, dockBean.addr
-                                    )
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-        } else if (!keyBean.isCharging) {//增加充电判断,防止无线充电干扰锁仓状态导致判断为取出
-            // 移出待连监听集合,防止connectKey循环失败
-            keyBean.mac?.let {
-                unregisterConnectListener(it)
-            }
-            sendEventMsg(
-                MsgEvent(
-                    MSG_EVENT_DEVICE_TAKE_UPDATE, DeviceTakeUpdateMsg(DEVICE_TYPE_KEY, keyBean.rfid)
-                )
-            )
+            sendEventMsg(MsgEvent(MSG_EVENT_DEVICE_TAKE_UPDATE, DeviceTakeUpdateMsg(DEVICE_TYPE_LOCK, lockBean.rfid)))
+            lockBean.deviceChange = false // 取出分支可在同步处理后立即归零
         }
     }
 
@@ -1815,11 +1709,14 @@ object BusinessManager {
         mDeviceTakeList.removeIf { it.deviceType == deviceType && it.nfc == nfc }
     }
 
+    private fun devKey(nodeId: Int, deviceType: Int, id: Int) =
+        "$nodeId-$deviceType-$id"
+
     private fun handleDeviceTake(deviceTakeUpdateBO: DeviceTakeUpdateMsg, rfid: String? = null) {
         LogUtil.i("$deviceTakeUpdateBO")
         when (deviceTakeUpdateBO.deviceType) {
             // 钥匙
-            0 -> {
+            DEVICE_TYPE_KEY -> {
                 mDeviceTakeList.find { it.deviceType == DEVICE_TYPE_KEY && it.nfc == deviceTakeUpdateBO.nfc }
                     ?.let { info ->
                         if (mDeviceTakeList.any { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.ticketId == info.ticketId }) {
@@ -1841,35 +1738,29 @@ object BusinessManager {
                                         UpdateTicketProgressMsg(info.ticketId)
                                     )
                                 )
-                                //钥匙取出之后重新再连一把钥匙待机
-                                ModBusController.getKeyByRfid(
-                                    info.nfc
-                                )?.mac?.let {
+                                // 钥匙取出之后重新再连一把钥匙待机
+                                HardwareMode.getCurrentHardwareMode().getKeyMacByRfid(info.nfc)?.let {
+                                    HardwareMode.getCurrentHardwareMode().getKeyBeanByMac(it)?.let { keyBean ->
+                                        HandlerGate.leave(devKey(keyBean.addr, keyBean.type, keyBean.idx))
+                                    }
                                     BleSendDispatcher.scheduleDisconnect(it)
                                 }
-                                //待机数不够就再连一把,但不能是原来那把
-                                ModBusController.getKeyByRfid(
-                                    info.nfc
-                                )?.mac?.let {
-                                    ThreadUtils.runOnIO {
-                                        checkMyTodoForHandleKey(it)
+                                // 待机数不够就再连一把,但不能是原来那把
+                                HardwareMode.getCurrentHardwareMode().getKeyMacByRfid(info.nfc)?.let {
+                                    HardwareMode.getCurrentHardwareMode().getKeyBeanByMac(it)?.let { keyBean ->
+                                        HandlerGate.leave(devKey(keyBean.addr, keyBean.type, keyBean.idx))
                                     }
+                                    ThreadUtils.runOnIO { checkMyTodoForHandleKey(it) }
                                 }
                             }
                         }
                     } ?: sendLoadingEventMsg(null, false)
             }
             // 挂锁
-            1 -> {
+            DEVICE_TYPE_LOCK -> {
                 mDeviceTakeList.find { it.deviceType == DEVICE_TYPE_LOCK && it.nfc == deviceTakeUpdateBO.nfc }
                     ?.let { info ->
-                        NetApi.updateLockTake(
-                            mutableListOf(
-                                LockTakeUpdateReqVO(
-                                    info.ticketId, info.nfc, MyApplication.instance?.serialNo()!!
-                                )
-                            )
-                        ) { isSuccess ->
+                        NetApi.updateLockTake(mutableListOf(LockTakeUpdateReqVO(info.ticketId, info.nfc, MyApplication.instance?.serialNo()!!))) { isSuccess ->
                             Executor.runOnMain {
                                 if (isSuccess == false) {
                                     LogUtil.e("Lock take report fail")
@@ -1903,11 +1794,9 @@ object BusinessManager {
                                 // 检查有无当前工作票的钥匙
                                 mDeviceTakeList.find { it.deviceType == DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
                                     ?.let { itKey ->
-                                        sendLoadingEventMsg(
-                                            MyApplication.instance?.applicationContext!!.getString(
+                                        sendLoadingEventMsg(MyApplication.instance?.applicationContext!!.getString(
                                                 R.string.ble_connecting
-                                            )
-                                        )
+                                            ))
                                         handleGiveKey(itKey)
                                     }
                             }

+ 6 - 2
app/src/main/java/com/grkj/iscs_mars/MyApplication.kt

@@ -41,8 +41,12 @@ class MyApplication : Application() {
         NetHttpManager.getInstance().initCtx(this)
 
         BusinessManager.initMsgEventBus()
-        ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
-        ArcSoftUtil.initEngine(SIKCore.getApplication())
+        try {
+            ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
+            ArcSoftUtil.initEngine(SIKCore.getApplication())
+        } catch (e: Exception) {
+
+        }
 
         NetApi.logout()
         SPUtils.clearLoginUser(this)

+ 343 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanCommand.kt

@@ -0,0 +1,343 @@
+package com.grkj.iscs_mars.can
+
+import com.sik.comm.impl_can.SdoDialect
+import com.sik.comm.impl_can.SdoRequest
+import kotlin.math.max
+import kotlin.math.min
+
+/**
+ * CAN 指令集(无类型探测版)
+ *
+ * - 不再依赖设备类型读取;直接按节点拿到一个“通用命令集”
+ * - 同时提供 EKeyDock / FiveLock / KeyCabinet / MaterialCabinet 的方法
+ * - 你按实际设备只调用相关的方法即可;调用不支持的寄存器会返回 Abort/超时,但不会阻塞分发
+ */
+object CanCommands {
+
+    /** SDO 协议指令集(修正读响应常量) */
+    val sdoDialect: SdoDialect = SdoDialect(
+        READ = 0x40,
+        READ_1B = 0x4F,  // ✅ 读1B响应
+        READ_2B = 0x4B,  // ✅ 读2B响应
+        READ_4B = 0x43,  // ✅ 读4B响应
+        READ_ERROR = 0x80,
+        WRITE_1B = 0x2F,
+        WRITE_2B = 0x2B,
+        WRITE_4B = 0x23,
+        WRITE_ACK = 0x60,
+        WRITE_ERROR = 0x80
+    )
+
+    /**
+     * 指令 主索引
+     */
+    object Command {
+        /**
+         * 设备类型
+         */
+        const val DEVICE_TYPE = 0x6000
+
+        /**
+         * 版本号
+         */
+        const val VERSION = 0x6003
+
+        /**
+         * 状态
+         */
+        const val STATUS = 0x6010
+
+        /**
+         * 控制状态
+         */
+        const val CONTROL_REG = 0x6011
+
+        /**
+         * 物资柜存储状态
+         */
+        const val STORAGE_REG = 0x6012
+
+        /**
+         * 照明/消毒
+         */
+        const val LIGHT_REG = 0x6015
+
+        /**
+         * 灯带控制
+         */
+        const val LED_STRIP_CONTROL = 0x6016
+
+        /**
+         * 温度
+         */
+        const val TEMPERATURE = 0x6017
+
+        /**
+         * 湿度
+         */
+        const val HUMIDITY = 0x6018
+
+        /**
+         * 左钥匙rfid/挂锁RFID/钥匙柜RFID
+         */
+        const val RFID = 0x6020
+
+        /**
+         * 右钥匙RFID
+         */
+        const val RIGHT_KEY_RFID = 0x6024
+
+    }
+
+    // ========= 通用区:所有节点都能用 =========
+    object Common {
+        /** 版本 (R) → 0x6003/0x00, 4B: HW主,HW子,SW主,SW子 */
+        fun getDeviceVersion(nodeId: Int): SdoRequest.Read =
+            SdoRequest.Read(
+                nodeId,
+                Command.VERSION,
+                0x00,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+
+        /** 大多数设备复用的状态寄存器 (R) → 0x6010/0x00, 2B */
+        fun getStatus(nodeId: Int): SdoRequest.Read =
+            SdoRequest.Read(
+                nodeId,
+                Command.STATUS,
+                0x00,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+
+        /**
+         * 获取设备类型
+         */
+        fun getDeviceType(nodeId: Int): SdoRequest.Read =
+            SdoRequest.Read(
+                nodeId,
+                Command.DEVICE_TYPE,
+                0x00,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+    }
+
+    /**
+     * 统一返回“通用命令集”(不做类型判断)
+     */
+    fun forDevice(nodeId: Int): GenericCommands = GenericCommands(nodeId)
+
+    /** 通用命令集:把各家寄存器方法都放这(按需调用) */
+    class GenericCommands(val nodeId: Int) {
+
+        // ---- EKeyDock(左右位)/ 以及很多板子兼容的 0x6011 语义 ----
+
+        /** 控制/状态 (R/W) 0x6011/0x00, 2B:写仅置相关位,其余写0;读回含工作位 */
+        fun readControlReg(): SdoRequest.Read =
+            SdoRequest.Read(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+
+        /** 设置左右卡扣(bit0=左卡扣,bit4=右卡扣) */
+        fun setLatch(left: Boolean? = null, right: Boolean? = null): SdoRequest.Write {
+            var v = 0
+            if (left != null) v = v or ((if (left) 1 else 0) shl 0)
+            if (right != null) v = v or ((if (right) 1 else 0) shl 4)
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(v, 0b0001_0001),
+                2,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 设置左右充电(bit1=左充电,bit5=右充电) */
+        fun setCharge(leftOn: Boolean? = null, rightOn: Boolean? = null): SdoRequest.Write {
+            var v = 0
+            if (leftOn != null) v = v or ((if (leftOn) 1 else 0) shl 1)
+            if (rightOn != null) v = v or ((if (rightOn) 1 else 0) shl 5)
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(v, 0b0010_0010),
+                2,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 单侧卡扣语法糖:keySlotId: 0左/1右;status: 0解锁/1锁住 */
+        fun controlLatch(keySlotId: Int, status: Int): SdoRequest.Write {
+            require(keySlotId in 0..1) { "keySlotId must be 0(left)/1(right)" }
+            require(status == 0 || status == 1) { "status must be 0/1" }
+            val bit = if (keySlotId == 0) 0 else 4
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE((status and 1) shl bit, 1 shl bit),
+                2, timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 左/右 RFID (R) 4B 小端(常见地址) */
+        fun getLeftRfid(): SdoRequest.Read = SdoRequest.Read(
+            nodeId,
+            Command.RFID,
+            0x00,
+            timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+        )
+
+        fun getRightRfid(): SdoRequest.Read = SdoRequest.Read(
+            nodeId,
+            Command.RIGHT_KEY_RFID,
+            0x00,
+            timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+        )
+
+        // ---- FiveLock / KeyCabinet(常见 1..5 位同构写法,寄存器通常与 0x6011 兼容) ----
+
+        /** 一次写入 5 位控制(低5位有效),适配 5路/柜体同构 */
+        fun setLatchBits_1to5(bits01to05: Int): SdoRequest.Write {
+            val v = bits01to05 and 0b1_1111
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(v, 0b1_1111),
+                2,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /**
+         * 批量写入 5 位控制(低5位有效),适配 5路/柜体同构
+         */
+        fun setLatchBits_1to5(target: Int, control: Int): SdoRequest.Write {
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(control, target),
+                2,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 单位控制(1..5) */
+        fun controlOne_1to5(slotIndex1to5: Int, locked: Boolean): SdoRequest.Write {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            val v = (if (locked) 1 else 0) shl (slotIndex1to5 - 1)
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(v, 1 shl (slotIndex1to5 - 1)),
+                2, timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 开门控制 */
+        fun controlDoorOpen(left: Boolean?, right: Boolean?): SdoRequest.Write {
+            var v = 0
+            if (left != null) v = v or (1 shl 0)
+            if (right != null) v = v or (1 shl 4)
+            return SdoRequest.Write(
+                nodeId,
+                Command.CONTROL_REG,
+                0x00,
+                shortLE(v, v),
+                2
+            )
+        }
+
+        /** 1..5 位 RFID 常见映射:0x6020..0x6024 */
+        fun getSlotRfid_1to5(slotIndex1to5: Int): SdoRequest.Read {
+            require(slotIndex1to5 in 1..5) { "slotIndex must be 1..5" }
+            return SdoRequest.Read(
+                nodeId,
+                Command.RFID,
+                0x00 + (slotIndex1to5 - 1),
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        // ---- MaterialCabinet(RGB/温湿度扩展) ----
+
+        /** RGB 状态灯 (R/W) 0x6016/0x00, 4B: B[0..7],G[8..15],R[16..23], 模式[24..26], 时间[27..29], 单位[30], 锁定[31] */
+        fun getRgb(): SdoRequest.Read = SdoRequest.Read(
+            nodeId,
+            Command.LED_STRIP_CONTROL,
+            0x00,
+            timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+        )
+
+        fun setRgb(
+            leftLed: Boolean,
+            r: Int, g: Int, b: Int,      // 0..255
+            mode: Int,                    // 0关/1常亮/2闪烁/3呼吸/4流水
+            timeStep: Int,                // 0..7(实际=+1)
+            secondsUnit: Boolean,         // false=100ms, true=1000ms
+            lockControl: Boolean          // 是否锁定主控权
+        ): SdoRequest.Write {
+            val rb = clamp8(b)
+            val gg = clamp8(g)
+            val rr = clamp8(r)
+            val mm = clamp3(mode)
+            val tt = clamp3(timeStep)
+            var v = 0
+            v = v or (rb and 0xFF)
+            v = v or ((gg and 0xFF) shl 8)
+            v = v or ((rr and 0xFF) shl 16)
+            v = v or ((mm and 0x07) shl 24)
+            v = v or ((tt and 0x07) shl 27)
+            v = v or ((if (secondsUnit) 1 else 0) shl 30)
+            v = v or ((if (lockControl) 1 else 0) shl 31)
+            return SdoRequest.Write(
+                nodeId,
+                Command.LED_STRIP_CONTROL,
+                if (leftLed) 0x00 else 0x01,
+                intLE(v),
+                4,
+                timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+            )
+        }
+
+        /** 温湿度(常见扩展) */
+        fun getTemperature(): SdoRequest.Read = SdoRequest.Read(
+            nodeId,
+            Command.TEMPERATURE,
+            0x00,
+            timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+        )
+
+        fun getHumidity(): SdoRequest.Read = SdoRequest.Read(
+            nodeId,
+            Command.HUMIDITY,
+            0x00,
+            timeoutMs = CustomCanConfig.instance.readTimeoutMs.toLong()
+        )
+
+        // ---- 通用状态 ----
+        fun getStatus(): SdoRequest.Read = Common.getStatus(nodeId)
+        fun getVersion(): SdoRequest.Read = Common.getDeviceVersion(nodeId)
+    }
+
+    // ========= Byte 打包工具(LE) =========
+    private fun shortLE(control: Int, target: Int): ByteArray =
+        byteArrayOf((control and 0xFF).toByte(), (target and 0xFF).toByte())
+
+    private fun intLE(v: Int): ByteArray = byteArrayOf(
+        (v and 0xFF).toByte(),
+        ((v ushr 8) and 0xFF).toByte(),
+        ((v ushr 16) and 0xFF).toByte(),
+        ((v ushr 24) and 0xFF).toByte()
+    )
+
+    private fun clamp8(x: Int) = min(255, max(0, x))
+    private fun clamp3(x: Int) = min(7, max(0, x))
+}

+ 30 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanDeviceConst.kt

@@ -0,0 +1,30 @@
+package com.grkj.iscs_mars.can
+
+/**
+ * 硬件常量
+ */
+object CanDeviceConst {
+    /**
+     * 未知类型
+     */
+    const val UNKNOWN = -1
+    /**
+     * 电子钥匙底座
+     */
+    const val DEVICE_KEY_DOCK = 0
+
+    /**
+     * 5路挂锁底座
+     */
+    const val DEVICE_LOCK_DOCK = 1
+
+    /**
+     * 钥匙柜控制板
+     */
+    const val DEVICE_KEY_CABINET_CONTROL_BOARD = 2
+
+    /**
+     * 物资柜主控制板
+     */
+    const val DEVICE_MATERIAL_CABINET_CONTROL_BOARD = 3
+}

+ 460 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanHardwareHelper.kt

@@ -0,0 +1,460 @@
+package com.grkj.iscs_mars.can
+
+import com.grkj.iscs_mars.extentions.toHexFromLe
+import com.grkj.iscs_mars.model.DeviceConst
+import com.grkj.iscs_mars.model.vo.hardware.CabinetSlotsRecord
+import com.sik.comm.impl_can.SdoOp
+import com.sik.comm.impl_can.toCommMessage
+import com.sik.sikcore.thread.ThreadUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * Can硬件读写帮助类
+ */
+class CanHardwareHelper : IHardwareHelper {
+    private val logger: Logger = LoggerFactory.getLogger(CanHardwareHelper::class.java)
+    override fun connectAndAddListener() {
+        ThreadUtils.runOnIO {
+            CanHelper.connect()
+            // StartListenerEvent.sendStartListenerEvent()
+        }
+    }
+
+    /**
+     * 断开连接操作
+     */
+    fun disconnect() {
+        ThreadUtils.runOnIO { CanHelper.disconnect() }
+    }
+
+    override fun getKeyMacByRfid(rfid: String): String? {
+        return CanHelper.getKeyByRfid(rfid)?.mac
+    }
+
+    override fun getLocks(
+        needLockCount: Int,
+        exceptionSlots: MutableList<CabinetSlotsRecord>,
+        exceptionLocks: MutableList<String>
+    ): MutableMap<Any, MutableList<Pair<Int, String>>> {
+        return CanHelper.getLocks(needLockCount, exceptionSlots, exceptionLocks)
+    }
+
+    override fun getRfidByKeyMac(keyMac: String): String? {
+        return CanHelper.getKeyDeviceByMac(keyMac)?.rfid
+    }
+
+    override fun getSlotPosition(keyNfc: String): String {
+        val keyData = CanHelper.getKeyByRfid(keyNfc)
+        // return if (keyData == null) I18nManager.t("not_in_slot") else "${keyData.nodeId}-${keyData.id + 1}"
+        return if (keyData == null) "not_in_slot" else "${keyData.nodeId}-${keyData.id + 1}"
+    }
+
+    override fun getExistsKeyMac(): List<String> {
+        val keyDevices = CanHelper.getDeviceByDeviceType(DeviceConst.DEVICE_TYPE_KEY)
+        return keyDevices.flatMap { it.value }.filterIsInstance<DeviceModel.DeviceKey>()
+            .filter { it.isExist }
+            .map { it.mac }
+    }
+
+    override fun getKeyDockData(): List<DockData.KeyDock> {
+        val deviceData = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_KEY_DOCK)
+        val keyDock = deviceData.map {
+            DockData.KeyDock().apply {
+                addr = it.key
+                type = CanDeviceConst.DEVICE_KEY_DOCK
+                keyData.addAll(
+                    it.value.filterIsInstance<DeviceModel.DeviceKey>().map {
+                        DockData.KeyDock.KeyBean().apply {
+                            this.addr = it.nodeId
+                            this.idx = it.id
+                            this.row = it.nodeId
+                            this.rfid = it.rfid
+                            this.mac = it.mac
+                            this.isExist = it.isExist
+                            this.type = it.deviceType
+                            this.newHardware = it.newHardware
+                        }
+                    })
+            }
+        }
+        return keyDock
+    }
+
+    override fun getDockData(): List<DockData> {
+        return getKeyDockData() + getLockDockData()
+    }
+
+    override fun updateKeyMac(rfid: String, mac: String) {
+        CanHelper.getKeyByRfid(rfid)?.mac = mac
+    }
+
+    override fun controlAllKeyBuckleClose(complete: () -> Unit) {
+        getKeyDockData().forEach {
+            val req = CanCommands.forDevice(it.addr).setLatch(true, true)
+            CanHelper.writeTo(req) {
+                complete()
+            }
+        }
+    }
+
+    override fun allSlotOn() {
+        getDockData().forEach {
+            when (it.type) {
+                CanDeviceConst.DEVICE_KEY_DOCK -> {
+                    controlAllKeyBuckleOpen()
+                }
+
+                CanDeviceConst.DEVICE_LOCK_DOCK -> {
+                    controlLockBuckle(
+                        true,
+                        it.addr,
+                        (it as DockData.LockDock).lockData.map { it.idx }.toMutableList()
+                    ) {}
+                }
+            }
+        }
+    }
+
+    override fun allSlotOff() {
+        getDockData().forEach {
+            when (it.type) {
+                CanDeviceConst.DEVICE_KEY_DOCK -> {
+                    controlAllKeyBuckleClose()
+                }
+
+                CanDeviceConst.DEVICE_LOCK_DOCK -> {
+                    controlLockBuckle(
+                        false,
+                        it.addr,
+                        (it as DockData.LockDock).lockData.map { it.idx }.toMutableList()
+                    ) {}
+                }
+            }
+        }
+    }
+
+    override fun hasNewLock(): Boolean {
+        return getLockDockData().flatMap { it.lockData }.any { it.newHardware }
+    }
+
+    override suspend fun openDoor(left: Boolean?, right: Boolean?): Boolean {
+        val materialCabinets =
+            CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD)
+        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> {
+        return getLockDockData().flatMap { it.lockData }.filter { it.newHardware }.map { it.rfid }
+    }
+
+    override fun removeNewHardwareLock(lockRfid: List<String>) {
+        val deviceData = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_LOCK_DOCK)
+        deviceData.flatMap { it.value }.filter { it.rfid in lockRfid }.forEach {
+            it.newHardware = false
+        }
+    }
+
+    override fun controlKeyBuckle(
+        isOpen: Boolean,
+        mac: String,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        val keyDevice = CanHelper.getKeyDeviceByMac(mac)
+        keyDevice?.let {
+            val req = CanCommands.forDevice(keyDevice.nodeId)
+                .controlLatch(keyDevice.id, if (isOpen) 0 else 1)
+            CanHelper.writeTo(req) { res ->
+                if (isOpen) {
+                    controlKeyCharge(false, keyDevice.id, keyDevice.nodeId) {
+                        done?.invoke(res.toCommMessage().payload)
+                    }
+                } else {
+                    done?.invoke(res.toCommMessage().payload)
+                }
+            }
+        }
+    }
+
+    override fun controlLockBuckle(
+        isOpen: Boolean,
+        slaveAddress: Int?,
+        lockIdxList: MutableList<Int>,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val target = lockIdxList.fold(0) { acc, i ->
+                acc or (1 shl (i - 1))
+            }
+            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)
+            }
+        }
+    }
+
+    override fun controlKeyCharge(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val leftOn = if (idx == 0 && isOpen) true else if (idx == 0) false else null
+            val rightOn = if (idx == 1 && isOpen) true else if (idx == 1) false else null
+            val req = CanCommands.forDevice(slaveAddress)
+                .setCharge(leftOn, rightOn)
+            CanHelper.writeTo(req) {
+                done?.invoke(it.toCommMessage().payload)
+            }
+        }
+    }
+
+    override fun controlKeyCharge(
+        isOpen: Boolean,
+        mac: String,
+        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
+            val req = CanCommands.forDevice(it.nodeId)
+                .setCharge(leftOn, rightOn)
+            CanHelper.writeTo(req) {
+                done?.invoke(it.toCommMessage().payload)
+            }
+        }
+    }
+
+    override fun controlAllKeyBuckleOpen(complete: () -> Unit) {
+        val keys = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_KEY_DOCK)
+        keys.forEach {
+            val req = CanCommands.forDevice(it.key).setLatch(false, false)
+            CanHelper.writeTo(req) {
+                controlAllKeyChargeDown()
+                complete()
+            }
+        }
+    }
+
+    override fun checkDock(): Boolean {
+        return false
+    }
+
+    override fun interruptReadTrashBinStatus(interrupt: Boolean) {
+
+    }
+
+    override fun controlAllKeyChargeDown() {
+        val keys = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_KEY_DOCK)
+        keys.forEach {
+            val req = CanCommands.forDevice(it.key).setCharge(false, false)
+            CanHelper.writeTo(req) {}
+        }
+    }
+
+    override fun getKeyBeanByMac(mac: String): DockData.KeyDock.KeyBean? {
+        val deviceKey = CanHelper.getKeyDeviceByMac(mac)
+        return deviceKey?.let {
+            DockData.KeyDock.KeyBean().apply {
+                this.addr = it.nodeId
+                this.idx = it.id
+                this.row = it.nodeId
+                this.rfid = it.rfid
+                this.mac = it.mac
+                this.isExist = it.isExist
+                this.type = it.deviceType
+                this.isReady = it.isReady
+                this.newHardware = it.newHardware
+            }
+        }
+    }
+
+    override fun updateKeyReadyStatus(
+        mac: String,
+        isReady: Boolean,
+        from: Int
+    ) {
+        logger.info("updateKeyReadyStatus mac:$mac isReady:$isReady from:$from")
+        val deviceKey = CanHelper.getKeyDeviceByMac(mac)
+        deviceKey?.isReady = isReady
+    }
+
+    override fun updateKeyPower(power: Int, mac: String) {
+        val deviceKey = CanHelper.getKeyDeviceByMac(mac)
+        deviceKey?.power = power
+    }
+
+    override fun controlLockBuckle(
+        isOpen: Boolean,
+        slaveAddress: Int?,
+        lockIdxList: Int,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val target = lockIdxList
+            val req = CanCommands.forDevice(slaveAddress).controlOne_1to5(target, !isOpen)
+            CanHelper.writeTo(req) {
+                done?.invoke(it.toCommMessage().payload)
+            }
+        }
+    }
+
+    override fun readKeyRfidStr(
+        slaveAddress: Int?,
+        idx: Int,
+        done: ((Int, String) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val req = CanCommands.forDevice(slaveAddress).let {
+                if (idx == 0) {
+                    it.getLeftRfid()
+                } else {
+                    it.getRightRfid()
+                }
+            }
+            CanHelper.readFrom(req) {
+                it?.let {
+                    logger.info("返回的数据:${it.toCommMessage().payload}")
+                    done?.invoke(idx, it.toCommMessage().payload.toHexFromLe())
+                }
+            }
+        }
+    }
+
+    override fun updateKeyRfid(addr: Int, idx: Int, rfid: String) {
+        CanHelper.getDeviceByNodeId(addr).find { it.id == idx }?.rfid = rfid
+    }
+
+    override fun updateKeyNewHardware(
+        addr: Int,
+        idx: Int,
+        newHardware: Boolean
+    ) {
+        CanHelper.getDeviceByNodeId(addr).find { it.id == idx }?.newHardware = newHardware
+    }
+
+    override fun updateKeyMac(addr: Int, idx: Int, macAddress: String) {
+        (CanHelper.getDeviceByNodeId(addr).find { it.id == idx } as? DeviceModel.DeviceKey)?.mac =
+            macAddress
+    }
+
+    override fun updateLockRfid(addr: Int, idx: Int, rfid: String) {
+        CanHelper.getDeviceByNodeId(addr).find { it.id == idx }?.rfid = rfid
+    }
+
+    override fun updateLockNewHardware(
+        addr: Int,
+        idx: Int,
+        newHardware: Boolean
+    ) {
+        CanHelper.getDeviceByNodeId(addr).find { it.id == idx }?.newHardware = newHardware
+    }
+
+    override fun readLockRfidStr(
+        slaveAddress: Int?,
+        lockIdx: Int,
+        done: ((String) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val req = CanCommands.forDevice(slaveAddress).let {
+                it.getSlotRfid_1to5(lockIdx)
+            }
+            CanHelper.readFrom(req) {
+                it?.let {
+                    logger.info("返回的数据:${it.toCommMessage().payload}")
+                    done?.invoke(it.toCommMessage().payload.toHexFromLe())
+                }
+            }
+        }
+    }
+
+    override fun getPortableDock(): List<DockData.PortableDock> {
+        return listOf()
+    }
+
+    override fun controlKeyBuckle(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        slaveAddress?.let {
+            val req = CanCommands.forDevice(slaveAddress).controlLatch(idx, if (isOpen) 0 else 1)
+            CanHelper.writeTo(req) { res ->
+                if (isOpen) {
+                    controlKeyCharge(!isOpen, idx, slaveAddress) {
+                        done?.invoke(res.toCommMessage().payload)
+                    }
+                } else {
+                    done?.invoke(res.toCommMessage().payload)
+                }
+            }
+        }
+    }
+
+    override fun getLockDockData(): List<DockData.LockDock> {
+        val deviceData = CanHelper.getDeviceByDeviceType(CanDeviceConst.DEVICE_LOCK_DOCK)
+        val lockDock = deviceData.map {
+            DockData.LockDock().apply {
+                addr = it.key
+                type = CanDeviceConst.DEVICE_LOCK_DOCK
+                lockData.addAll(it.value.filterIsInstance<DeviceModel.CommonDevice>().map {
+                    DockData.LockDock.LockBean().apply {
+                        this.addr = it.nodeId
+                        this.idx = it.id
+                        this.row = it.nodeId
+                        this.rfid = it.rfid
+                        this.isExist = it.isExist
+                        this.type = it.deviceType
+                        this.newHardware = it.newHardware
+                    }
+                })
+            }
+        }
+        return lockDock
+    }
+
+    override fun controlKeyLockAndCharge(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((ByteArray) -> Unit)?
+    ) {
+        controlKeyBuckle(isOpen, idx, slaveAddress) {
+            if (isOpen) {
+                controlKeyCharge(false, idx, slaveAddress, done)
+            } else {
+                done?.invoke(it)
+            }
+        }
+    }
+
+    override fun getLockSlotPosition(lockNfc: String): String {
+        val lockData = CanHelper.getLockByRfid(lockNfc)
+        // return if (lockData == null) I18nManager.t("not_in_slot") else "${lockData.nodeId}-${lockData.id + 1}"
+        return if (lockData == null) "not_in_slot" else "${lockData.nodeId}-${lockData.id + 1}"
+    }
+
+    override suspend fun getOneKey(
+        exceptionSlots: List<CabinetSlotsRecord>,
+        exceptionKeysRfid: List<String>,
+        exceptionKeysMac: List<String>
+    ): Pair<Any, String?>? {
+        return CanHelper.getOneKey(exceptionSlots, exceptionKeysRfid, exceptionKeysMac)
+    }
+}

+ 378 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanHelper.kt

@@ -0,0 +1,378 @@
+package com.grkj.iscs_mars.can
+
+import android.annotation.SuppressLint
+import com.grkj.iscs_mars.ble.BleReturnDispatcher
+import com.grkj.iscs_mars.ble.BleSendDispatcher
+import com.grkj.iscs_mars.model.vo.hardware.CabinetSlotsRecord
+import com.huyuhui.fastble.BleManager
+import com.sik.comm.core.protocol.ProtocolManager
+import com.sik.comm.core.protocol.ProtocolType
+import com.sik.comm.impl_can.CanProtocol
+import com.sik.comm.impl_can.SdoRequest
+import com.sik.comm.impl_can.SdoResponse
+import com.sik.comm.impl_can.toCommMessage
+import com.sik.comm.impl_can.toSdoResponse
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+/**
+ * can总线帮助工具
+ */
+object CanHelper {
+    private val logger: Logger = LoggerFactory.getLogger(CanHelper::class.java)
+
+    /**
+     * 作用域
+     */
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+    /**
+     * 节点对应的设备类型列表
+     */
+    private val nodeMap: HashMap<Int, Int> = hashMapOf()
+
+    /**
+     * 设备数据
+     */
+    private val deviceData: HashMap<Int, List<DeviceModel>> = hashMapOf()
+
+    /**
+     * 设备变更监听器
+     */
+    private val deviceChangeListeners: HashMap<Any, (List<DeviceModel>) -> Unit> = hashMapOf()
+
+    /**
+     * 设备状态监听器
+     */
+    private val deviceStatusListener: CanReadyPlugin.DeviceStatusListener =
+        object : CanReadyPlugin.DeviceStatusListener {
+
+            override fun deviceStatus(nodeId: Int, index: Int, statusData: ByteArray) {
+                when (nodeMap[nodeId]) {
+                    CanDeviceConst.DEVICE_KEY_DOCK -> {
+                        DeviceParseStatus.parseKeyDockStatus(nodeId, index, statusData)
+                    }
+
+                    CanDeviceConst.DEVICE_LOCK_DOCK -> {
+                        DeviceParseStatus.parseLockDockStatus(nodeId, index, statusData)
+
+                    }
+
+                    CanDeviceConst.DEVICE_KEY_CABINET_CONTROL_BOARD -> {
+                        DeviceParseStatus.parseKeyCabinetControlBoardStatus(
+                            nodeId, index, statusData
+                        )
+
+                    }
+
+                    CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD -> {
+                        DeviceParseStatus.parseMaterialCabinetControlBoardStatus(
+                            nodeId, index, statusData
+                        )
+
+                    }
+                }
+            }
+        }
+
+    /**
+     * 添加设备变更监听器
+     */
+    fun addDeviceChangeListener(key: Any, listener: (List<DeviceModel>) -> Unit) {
+        deviceChangeListeners.put(key, listener)
+    }
+
+    /**
+     * 移除设备变更监听器
+     */
+    fun removeDeviceChangeListener(key: Any) {
+        deviceChangeListeners.remove(key)
+    }
+
+    /**
+     * 根据节点id获取设备
+     */
+    fun getDeviceByNodeId(nodeId: Int): List<DeviceModel> {
+        return deviceData[nodeId] ?: emptyList()
+    }
+
+    /**
+     * 根据设备类型获取设备
+     */
+    fun getDeviceByDeviceType(deviceType: Int): Map<Int, List<DeviceModel>> {
+        return deviceData.flatMap { it.value }.filter { it.deviceType == deviceType }
+            .groupBy { it.nodeId }
+    }
+
+    /**
+     * 根据节点id获取设备类型
+     */
+    fun getDeviceTypeByNodeId(nodeId: Int): Int {
+        return nodeMap[nodeId] ?: CanDeviceConst.UNKNOWN
+    }
+
+    /**
+     * 更新硬件数据
+     */
+    fun updateDeviceData(nodeId: Int, deviceData: List<DeviceModel>) {
+        this.deviceData[nodeId] = deviceData
+        deviceChangeListeners.forEach {
+            it.value.invoke(deviceData)
+        }
+    }
+
+    /**
+     * 连接
+     */
+    fun connect() {
+        val canProtocol = CanProtocol()
+        ProtocolManager.register(ProtocolType.CAN, canProtocol)
+        // 绑定配置(每个节点一个 deviceId,建议 "can0@<nodeId>")
+        ProtocolManager.bindDeviceConfig(CustomCanConfig.instance)
+        canProtocol.registerConfig(CustomCanConfig.instance)
+        CanReadyPlugin.registerDeviceStatusListener(this, deviceStatusListener)
+        // 连接
+        ProtocolManager.connect(CustomCanConfig.instance.deviceId)
+    }
+
+    fun disconnect() {
+        ProtocolManager.disconnect(CustomCanConfig.instance.deviceId)
+    }
+
+    /**
+     * 根据设备类型获取节点id
+     */
+    fun getNodeIdByDeviceType(deviceType: Int): List<Int> {
+        return nodeMap.filter { it.value == deviceType }.map { it.key }
+    }
+
+    /**
+     * 添加节点
+     */
+    fun addNode(nodeId: Int, deviceType: Int) {
+        nodeMap.put(nodeId, deviceType)
+    }
+
+    /**
+     * 读取
+     */
+    fun readFrom(req: SdoRequest.Read, callback: (SdoResponse.ReadData?) -> Unit) {
+        scope.launch(Dispatchers.IO) {
+            runCatching {
+                ProtocolManager.getProtocol(CustomCanConfig.instance.deviceId)
+                    .send(CustomCanConfig.instance.deviceId, req.toCommMessage())
+            }.onSuccess { rsp ->
+                callback(rsp.toSdoResponse() as SdoResponse.ReadData)
+            }.onFailure {
+                logger.info("读取失败:${it}")
+                callback(null)
+            }
+        }
+    }
+
+    /**
+     * 写入到
+     */
+    fun writeTo(req: SdoRequest.Write, callback: (SdoResponse.WriteAck) -> Unit = {}) {
+        scope.launch(Dispatchers.IO) {
+            runCatching {
+                ProtocolManager.getProtocol(CustomCanConfig.instance.deviceId)
+                    .send(CustomCanConfig.instance.deviceId, req.toCommMessage())
+            }.onSuccess { rsp ->
+                callback(rsp.toSdoResponse() as SdoResponse.WriteAck)
+            }.onFailure {
+                logger.info("写入失败:${it}")
+            }
+        }
+    }
+
+    /**
+     * 写入同步
+     */
+    suspend fun writeTo(req: SdoRequest.Write): SdoResponse.WriteAck? {
+        return runCatching {
+            ProtocolManager.getProtocol(CustomCanConfig.instance.deviceId)
+                .send(CustomCanConfig.instance.deviceId, req.toCommMessage())
+        }.onSuccess { rsp ->
+            rsp
+        }.onFailure {
+            logger.info("写入失败:${it}")
+            null
+        }.getOrNull()?.toSdoResponse() as? SdoResponse.WriteAck
+    }
+
+    /**
+     * 根据mac获取钥匙
+     */
+    fun getKeyDeviceByMac(keyMac: String): DeviceModel.DeviceKey? {
+        return deviceData.flatMap { it.value }.filterIsInstance<DeviceModel.DeviceKey>()
+            .find { it.mac == keyMac }
+    }
+
+    /**
+     * 根据rfid获取钥匙
+     */
+    fun getKeyByRfid(rfid: String): DeviceModel.DeviceKey? {
+        return deviceData.flatMap { it.value }.filterIsInstance<DeviceModel.DeviceKey>()
+            .find { it.rfid == rfid }
+    }
+
+    /**
+     * 根据rfid获取挂锁
+     */
+    fun getLockByRfid(lockNfc: String): DeviceModel.CommonDevice? {
+        return deviceData.flatMap { it.value }.filterIsInstance<DeviceModel.CommonDevice>()
+            .find { it.rfid == lockNfc }
+    }
+
+    /**
+     * 获取挂锁
+     */
+    fun getLocks(
+        needLockCount: Int,
+        exceptionSlots: MutableList<CabinetSlotsRecord>,
+        exceptionLocks: MutableList<String>
+    ): MutableMap<Any, MutableList<Pair<Int, String>>> {
+
+        val map = mutableMapOf<Any, MutableList<Pair<Int, String>>>()
+        logger.info("需要的锁具: $needLockCount")
+        if (needLockCount <= 0) return map
+
+        // 1) 拿数据 + 排序(要接收返回值)
+        val lockDockList = deviceData.values
+            .flatten()
+            .filterIsInstance<DeviceModel.CommonDevice>()
+            .filter { !it.isException }
+            .sortedBy { it.nodeId }
+
+        logger.info("锁具基座列表: $lockDockList")
+        logger.info("异常锁rfid: $exceptionLocks")
+        logger.info("异常锁仓位: ${exceptionSlots.joinToString(",") { "${it.row},${it.col}" }}")
+
+        // 2) 预先把“该 nodeId 下哪些 id(列) 被禁用”整理成 Map,过滤更清晰
+        val blockedIdsByNode: Map<Int, Set<Int>> =
+            exceptionSlots
+                .filter { it.row != null }
+                .groupBy(
+                    keySelector = { it.row!!.toInt() },                 // nodeId
+                    valueTransform = { (it.col?.toInt() ?: 1) - 1 }      // 被占/异常的列 -> 对应 lock.id
+                )
+                .mapValues { (_, cols) -> cols.toSet() }
+
+        // 3) 过滤合法锁
+        val validLocks = lockDockList.filter { l ->
+            l.isExist &&
+                    l.rfid !in exceptionLocks &&
+                    blockedIdsByNode[l.nodeId]?.contains(l.id) != true
+        }
+
+        // 4) 取够数量并分组 -> 显式写回 map
+        val selected = validLocks.take(needLockCount)
+        val grouped = selected.groupBy { it.nodeId }
+            .mapValues { (_, list) -> list.map { it.id to it.rfid }.toMutableList() }
+
+        map.putAll(grouped)
+
+        logger.info("待取锁: $map")
+        return map
+    }
+
+    /**
+     * 获取一把钥匙
+     */
+    @SuppressLint("MissingPermission")
+    suspend fun getOneKey(
+        exceptionSlots: List<CabinetSlotsRecord>,
+        exceptionKeysRfid: List<String>,
+        exceptionKeysMac: List<String> = mutableListOf(),
+    ): Pair<Any, String?>? {
+        // 1. 过滤并准备钥匙列表
+        val slotCols = exceptionSlots.mapNotNull { it.col?.toInt() }
+        val noBelongDevice = BleManager.getAllConnectedDevice().filter {
+            !BleSendDispatcher.isConnected(it.mac) && !BleSendDispatcher.isConnecting(it.mac) && !BleReturnDispatcher.isConnected(
+                it.mac
+            ) && !BleReturnDispatcher.isConnecting(it.mac)
+        }
+        logger.info("检查到不属于任何队列的设备:${noBelongDevice.map { it.mac }},立即断开连接让路")
+        noBelongDevice.forEach {
+            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(
+                    kb.mac
+                ) && !BleReturnDispatcher.isConnecting(kb.mac)
+            }
+
+        logger.info("蓝牙连接-获取到钥匙信息:${keyList}")
+        if (keyList.isEmpty()) {
+            return null
+        }
+        val sendConnectingAndConnected = BleSendDispatcher.getConnectedAndConnecting()
+        //如果已连接和连接中的设备不在钥匙列表中,直接断开连接让路
+        sendConnectingAndConnected.filter { it !in keyList.map { it.mac } }.forEach {
+            BleSendDispatcher.scheduleDisconnect(it)
+        }
+        keyList = keyList.sortedWith(compareByDescending<DeviceModel.DeviceKey> {
+            BleSendDispatcher.isConnected(it.mac) || BleSendDispatcher.isConnecting(
+                it.mac
+            )
+        }    // 主键:在线优先
+            .thenByDescending { it.power }                                                // 三级:电量越高
+        )
+        val connectedKey = keyList.find { BleSendDispatcher.isConnected(it.mac) }
+        if (connectedKey != null) {
+            return connectedKey.nodeId to connectedKey.rfid
+        }
+        val connectingKey = keyList.find { BleSendDispatcher.isConnecting(it.mac) }
+        if (connectingKey != null) {
+            val result = suspendCoroutine { cont ->
+                BleSendDispatcher.submit(connectingKey.mac) { connected ->
+                    if (connected) {
+                        cont.resume(connectingKey.nodeId to connectingKey.rfid)
+                    } else {
+                        cont.resume(null)
+                    }
+                }
+            }
+            if (result != null) {
+                return result
+            }
+        }
+        for (kb in keyList) {
+            val mac = kb.mac
+            val result = suspendCoroutine { cont ->
+                BleSendDispatcher.submit(mac) { connected ->
+                    if (connected) {
+                        logger.info("蓝牙连接完成 :${mac}")
+                        // 找到第一个能连的:从 keyDockList 里拿同 rfid 的 addr
+                        logger.info("蓝牙连接-找到的底座地址 :${kb.nodeId}")
+                        if (cont.context.isActive) {
+                            cont.resume(kb.nodeId to kb.rfid)
+                        }
+                    } else {
+                        if (cont.context.isActive) {
+                            cont.resume(null)
+                        }
+                    }
+                }
+            }
+            if (result != null) {
+                return result
+            }
+        }
+
+        // 一个都没成功
+        logger.error("getOneKey : no key match")
+        return null
+    }
+}

+ 361 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanReadyPlugin.kt

@@ -0,0 +1,361 @@
+package com.grkj.iscs_mars.can
+
+import android.util.Log
+import com.grkj.iscs_mars.enums.HardwareMode
+import com.grkj.iscs_mars.extentions.toHexFromLe
+import com.grkj.iscs_mars.util.log.LogUtil
+import com.sik.comm.core.model.ProtocolState
+import com.sik.comm.core.plugin.CommPlugin
+import com.sik.comm.core.plugin.PluginScope
+import com.sik.comm.impl_can.SdoRequest
+import com.sik.comm.impl_can.SdoResponse
+import com.sik.sikcore.extension.toJson
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withTimeoutOrNull
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
+class CanReadyPlugin : CommPlugin {
+    private val logger: Logger = LoggerFactory.getLogger(CanReadyPlugin::class.java)
+
+    /** 轮询哪些节点 */
+    private val activeNodes = mutableSetOf<Int>()
+
+    /** 周期 */
+    private val pollMsStatus = 200L   // 每节点读 0x6010 的间隔
+
+    /** 协程域 & 单个轮询任务 */
+    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+    private var pollJob: Job? = null
+
+    companion object {
+
+        /**
+         * 设备状态监听
+         */
+        private val deviceStatusListener: HashMap<Any, DeviceStatusListener> = hashMapOf()
+
+        /**
+         * 注册监听
+         */
+        fun registerDeviceStatusListener(key: Any, listener: DeviceStatusListener) {
+            deviceStatusListener[key] = listener
+        }
+
+        /**
+         * 移除监听
+         */
+        fun unRegisterDeviceStatusListener(key: Any) {
+            deviceStatusListener.remove(key)
+        }
+    }
+
+    override fun onStateChanged(scopeP: PluginScope) {
+        super.onStateChanged(scopeP)
+        when (scopeP.state) {
+            ProtocolState.READY -> startPollingSingleLoop()
+            ProtocolState.DISCONNECTED -> stopPolling()
+            else -> Unit
+        }
+    }
+
+    /** 单协程,逐节点轮询 */
+    private fun startPollingSingleLoop() {
+        pollJob?.cancel()
+        pollJob = scope.launch {
+            NodeIdHelper.scanRangeLinear({ nodeId ->
+                safeRead(CanCommands.Common.getDeviceType(nodeId))?.let {
+                    logger.info("硬件类型:${it.payload[0].toInt()}")
+                    activeNodes.add(nodeId)
+                    CanHelper.addNode(nodeId, it.payload[0].toInt())
+                }
+            }, endInclusive = 14)
+            logger.info("当前检测到的硬件:{}", activeNodes)
+            for (nodeId in activeNodes) {
+                try {
+                    val cmds = CanCommands.forDevice(nodeId)
+
+                    // 1) 读状态 0x6010/00 (2B)
+                    if (CanHelper.getDeviceTypeByNodeId(nodeId) != CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD) {
+                        safeRead(cmds.getStatus())?.let { rd ->
+                            deviceStatusListener.forEach {
+                                it.value.deviceStatus(
+                                    nodeId,
+                                    rd.index,
+                                    rd.payload
+                                )
+                            }
+                        }
+                    }
+                    safeRead(cmds.readControlReg())?.let { rd ->
+                        deviceStatusListener.forEach {
+                            it.value.deviceStatus(
+                                nodeId,
+                                rd.index,
+                                rd.payload
+                            )
+                        }
+                    }
+
+                } catch (t: Throwable) {
+                    // 单个节点出错不影响整体循环
+                    logger.warn("poll node={} error: {}", nodeId, t.toString())
+                }
+
+                // 给总线/固件留点缝,避免贴脸轰
+                delay(pollMsStatus)
+            }
+            HardwareMode.getCurrentHardwareMode().controlAllKeyBuckleClose()
+            initLock()    // 打开所有无锁的卡扣、关闭所有有锁的卡扣、读取所有锁的RFID
+            initKey()     // 打开所有无钥匙的卡扣、关闭所有有钥匙的卡扣、读取所有钥匙的RFID
+            while (isActive) {
+                for (nodeId in activeNodes) {
+                    try {
+                        val cmds = CanCommands.forDevice(nodeId)
+
+                        // 1) 读状态 0x6010/00 (2B)
+                        if (CanHelper.getDeviceTypeByNodeId(nodeId) != CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD) {
+                            safeRead(cmds.getStatus())?.let { rd ->
+                                deviceStatusListener.forEach {
+                                    try {
+                                        it.value.deviceStatus(nodeId, rd.index, rd.payload)
+                                    } catch (e: Exception){
+                                        LogUtil.e("deviceStatusListener $e")
+                                    }
+
+                                }
+                            }
+                        }
+                        safeRead(cmds.readControlReg())?.let { rd ->
+                            deviceStatusListener.forEach {
+                                it.value.deviceStatus(
+                                    nodeId,
+                                    rd.index,
+                                    rd.payload
+                                )
+                            }
+                        }
+
+                    } catch (t: Throwable) {
+                        // 单个节点出错不影响整体循环
+                        logger.warn("poll node={} error: {}", nodeId, t.toString())
+                    }
+
+                    // 给总线/固件留点缝,避免贴脸轰
+                    delay(pollMsStatus)
+                }
+            }
+        }.also { job ->
+            job.invokeOnCompletion { e ->
+                if (e != null) logger.warn("single-loop completed with error: {}", e.toString())
+                else logger.info("single-loop stopped.")
+            }
+        }
+    }
+
+    /**
+     * 初始化锁具——打开所有无锁的卡扣、读取RFID
+     */
+    private fun initLock() {
+        logger.info(
+            "initLock : ${
+                HardwareMode.getCurrentHardwareMode().getLockDockData().toJson()
+            }"
+        )
+        HardwareMode.getCurrentHardwareMode().getLockDockData()
+            .forEach { dockBean ->
+                val hasLockIdxList =
+                    dockBean.lockData.filter { it.isExist }.map { it.idx } as MutableList<Int>
+                val noLockIdxList =
+                    dockBean.lockData.filter { !it.isExist }.map { it.idx } as MutableList<Int>
+
+                hasLockIdxList.forEach { idx ->
+                    val readRfidReq = CanCommands.forDevice(dockBean.addr).getSlotRfid_1to5(idx)
+                    CanHelper.readFrom(readRfidReq) { result ->
+                        val res = result?.payload ?: return@readFrom
+                        if (res.size < 4) {
+                            logger.error("Lock rfid error")
+                            return@readFrom
+                        }
+                        val rfid = res.toHexFromLe()
+                        logger.info("初始化锁具 RFID : $rfid")
+                        HardwareMode.getCurrentHardwareMode()
+                            .updateLockRfid(dockBean.addr, idx, rfid)
+                        // todo 为设备录入增加
+//                        LogicManager.hardwareLogic.getLockInfo(rfid) {
+//                            HardwareMode.getCurrentHardwareMode().updateLockNewHardware(
+//                                dockBean.addr,
+//                                idx,
+//                                it == null || it.lockNfc?.isEmpty() == true
+//                            )
+//                        }
+                    }
+                }
+                HardwareMode.getCurrentHardwareMode()
+                    .controlLockBuckle(false, dockBean.addr, hasLockIdxList)
+                HardwareMode.getCurrentHardwareMode()
+                    .controlLockBuckle(true, dockBean.addr, noLockIdxList)
+            }
+    }
+
+    /**
+     * 初始化钥匙
+     */
+    private fun initKey() {
+        logger.info("initKey : ${HardwareMode.getCurrentHardwareMode().getKeyDockData()}")
+//        HardwareMode.getCurrentHardwareMode().getKeyDockData()
+//            .forEach { dockBean ->
+//                if (dockBean.keyData.isEmpty()) {
+//                    ISCSConfig.canInitDevice = true
+//                    ModbusInitCompleteEvent.sendModbusInitCompleteEvent()
+//                } else {
+//                    dockBean.keyData.forEach { key ->
+//                        if (key.isExist) {
+//                            logger.info("initKey : ${dockBean.addr} : ${key.idx == 0}")
+//                            HardwareMode.getCurrentHardwareMode()
+//                                .readKeyRfidStr(dockBean.addr, key.idx) { idx, rfid ->
+//                                    logger.info("初始化钥匙 RFID : $rfid")
+//                                    // 更新rfid
+//                                    HardwareMode.getCurrentHardwareMode()
+//                                        .updateKeyRfid(dockBean.addr, key.idx, rfid)
+//                                    // 蓝牙准备操作
+//                                    LogicManager.hardwareLogic.getKeyInfo(rfid) { keyInfo ->
+//                                        logger.info("getKeyInfo : $rfid - ${keyInfo?.macAddress}")
+//                                        HardwareMode.getCurrentHardwareMode().updateKeyNewHardware(
+//                                            dockBean.addr,
+//                                            key.idx,
+//                                            keyInfo == null || keyInfo.keyNfc?.isEmpty() == true || keyInfo.macAddress?.isEmpty() == true
+//                                        )
+//                                        logger.info(
+//                                            "钥匙是否为新设备:{}",
+//                                            keyInfo == null || keyInfo.keyNfc?.isEmpty() == true || keyInfo.macAddress?.isEmpty() == true
+//                                        )
+//                                        if (keyInfo != null && !keyInfo.macAddress.isNullOrEmpty()) {
+//                                            // 更新mac
+//                                            HardwareMode.getCurrentHardwareMode().updateKeyMac(
+//                                                dockBean.addr,
+//                                                key.idx,
+//                                                keyInfo.macAddress!!
+//                                            )
+//                                            //已经初始化完成才会去连接
+//                                            if (ISCSConfig.isInit) {
+//                                                HardwareMode.getCurrentHardwareMode()
+//                                                    .controlKeyCharge(true, key.idx, dockBean.addr)
+//                                            }
+//                                            HardwareMode.getCurrentHardwareMode()
+//                                                .controlKeyBuckle(false, key.idx, dockBean.addr)
+//                                        } else {
+//                                            if (ISCSConfig.isInit) {
+//                                                ToastEvent.sendToastEvent(I18nManager.t("get_key_info_fail"))
+//                                            }
+//                                            HardwareMode.getCurrentHardwareMode()
+//                                                .controlKeyLockAndCharge(
+//                                                    true,
+//                                                    key.idx,
+//                                                    dockBean.addr
+//                                                )
+//                                        }
+//                                        val isKeyReady =
+//                                            HardwareMode.getCurrentHardwareMode().getKeyDockData()
+//                                                .all {
+//                                                    it.keyData.filter { it.type == DeviceConst.DEVICE_TYPE_KEY }
+//                                                        .filterIsInstance<DockBean.KeyBean>()
+//                                                        .filter { it.isExist }
+//                                                        .all {
+//                                                            logger.info("钥匙信息:${it.rfid}")
+//                                                            it.rfid?.isNotEmpty() == true
+//                                                        }
+//                                                }
+//                                        logger.info("钥匙是否准备完毕:${isKeyReady},${ISCSConfig.isInit}")
+//                                        if (isKeyReady && ISCSConfig.isInit) {
+//                                            ISCSConfig.canInitDevice = true
+//                                            logger.info("发送初始化完成事件")
+//                                            ModbusInitCompleteEvent.sendModbusInitCompleteEvent()
+//                                        } else if (isKeyReady) {
+//                                            ISCSConfig.canInitDevice = true
+//                                            ModbusInitCompleteEvent.sendModbusInitCompleteEvent()
+//                                        }
+//                                    }
+//                                }
+//                        } else {
+//                            HardwareMode.getCurrentHardwareMode()
+//                                .controlKeyBuckle(true, key.idx, dockBean.addr)
+//                            val isKeyReady =
+//                                HardwareMode.getCurrentHardwareMode().getKeyDockData()
+//                                    .all {
+//                                        it.keyData.filter { it.type == DeviceConst.DEVICE_TYPE_KEY }
+//                                            .filterIsInstance<DockBean.KeyBean>()
+//                                            .filter { it.isExist }
+//                                            .all {
+//                                                logger.info("钥匙信息:${it.rfid}")
+//                                                it.rfid?.isNotEmpty() == true
+//                                            }
+//                                    }
+//                            logger.info("钥匙是否准备完毕:${isKeyReady},${ISCSConfig.isInit}")
+//                            if (isKeyReady && ISCSConfig.isInit) {
+//                                ISCSConfig.canInitDevice = true
+//                                logger.info("发送初始化完成事件")
+//                                ModbusInitCompleteEvent.sendModbusInitCompleteEvent()
+//                            } else if (isKeyReady) {
+//                                ISCSConfig.canInitDevice = true
+//                                ModbusInitCompleteEvent.sendModbusInitCompleteEvent()
+//                            }
+//                        }
+//
+//                    }
+//                }
+//            }
+    }
+
+    private fun stopPolling() {
+        pollJob?.cancel()
+        pollJob = null
+        logger.info("CAN poll stopped.")
+    }
+
+    /** 一次性读取:超时返回 null,不抛异常、不打崩循环 */
+    private suspend fun safeRead(
+        req: SdoRequest.Read,
+        timeoutMs: Long = 400
+    ): SdoResponse.ReadData? {
+        return try {
+            withTimeoutOrNull(timeoutMs) {
+                suspendCoroutine { cont ->
+                    CanHelper.readFrom(req) { rsp -> cont.resume(rsp) }
+                }
+            }.also { rsp ->
+                if (rsp == null) {
+                    logger.debug(
+                        "read timeout node={} idx=0x{} sub=0x{}",
+                        req.nodeId, req.index.toString(16), req.subIndex.toString(16)
+                    )
+                }
+            }
+        } catch (t: Throwable) {
+            logger.warn(
+                "read exception node={} idx=0x{} sub=0x{} : {}",
+                req.nodeId, req.index.toString(16), req.subIndex.toString(16), t.toString()
+            )
+            null
+        }
+    }
+
+
+    /**
+     * 设备状态监听
+     */
+    interface DeviceStatusListener {
+        /**
+         * 设备状态
+         */
+        fun deviceStatus(nodeId: Int, index: Int, statusData: ByteArray)
+    }
+}

+ 48 - 0
app/src/main/java/com/grkj/iscs_mars/can/CanSendDelayInterceptor.kt

@@ -0,0 +1,48 @@
+package com.grkj.iscs_mars.can
+
+import com.sik.comm.core.interceptor.CommInterceptor
+import com.sik.comm.core.interceptor.InterceptorChain
+import com.sik.comm.core.model.CommMessage
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import android.os.SystemClock
+import kotlin.math.max
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * CAN 总线发送最小间隔拦截器(带协程锁)
+ */
+class CanSendDelayInterceptor(
+    private val minIntervalMs: Long = 200L  // 可配置
+) : CommInterceptor {
+
+    private val logger: Logger = LoggerFactory.getLogger(CanSendDelayInterceptor::class.java)
+
+    companion object {
+        // 用单一互斥量串行化“检查→等待→发送→更新时间”
+        private val mutex = Mutex()
+        // 用 elapsedRealtime 计时间隔,避免受系统时间调整影响
+        @Volatile var lastSendTick: Long = 0L
+    }
+
+    override suspend fun intercept(chain: InterceptorChain, original: CommMessage): CommMessage {
+        return mutex.withLock {
+            val now = SystemClock.elapsedRealtime()
+            val elapsed = now - lastSendTick
+            val waitMs = max(0L, minIntervalMs - elapsed)
+
+            if (waitMs > 0) {
+                delay(waitMs)
+            }
+
+            // 发送
+            val rsp = chain.proceed(original)
+
+            // 注意:以“发送完成时刻”作为下次基准;如果你更想“开始发送时刻”,把这行挪到 proceed() 之前即可
+            lastSendTick = SystemClock.elapsedRealtime()
+            rsp
+        }
+    }
+}

+ 36 - 0
app/src/main/java/com/grkj/iscs_mars/can/CustomCanConfig.kt

@@ -0,0 +1,36 @@
+package com.grkj.iscs_mars.can
+
+import com.sik.comm.core.interceptor.CommInterceptor
+import com.sik.comm.core.plugin.CommPlugin
+import com.sik.comm.impl_can.CanConfig
+
+/**
+ * 自定义can配置
+ */
+class CustomCanConfig : CanConfig(
+    deviceId = "can0@1",
+    interfaceName = "can0",
+    defaultNodeId = 1,
+    sdo = CanCommands.sdoDialect,
+    readTimeoutMs = 200
+) {
+    companion object {
+        val instance by lazy { CustomCanConfig() }
+    }
+
+    /**
+     * 发送延迟拦截器
+     */
+    private val canSendDelayInterceptor = CanSendDelayInterceptor()
+
+    /**
+     * 状态读取插件
+     */
+    private val canReadyPlugin = CanReadyPlugin()
+
+    override val additionalInterceptors: List<CommInterceptor>
+        get() = super.additionalInterceptors + canSendDelayInterceptor
+
+    override val additionalPlugins: List<CommPlugin>
+        get() = super.additionalPlugins + canReadyPlugin
+}

+ 117 - 0
app/src/main/java/com/grkj/iscs_mars/can/DeviceModel.kt

@@ -0,0 +1,117 @@
+package com.grkj.iscs_mars.can
+
+/**
+ * 设备模型
+ */
+sealed class DeviceModel {
+    /**
+     * 节点id
+     */
+    var nodeId: Int = 0
+
+    /**
+     * 设备类型
+     */
+    var deviceType: Int = 0
+
+    /**
+     * 设备id
+     * 钥匙0代表左,1代表右
+     */
+    var id: Int = 0
+
+    /**
+     * 是否锁定
+     */
+    var locked: Boolean = false
+
+    /**
+     * 是否为新设备
+     */
+    var newHardware: Boolean = false
+
+    /**
+     * rfid
+     */
+    var rfid: String = ""
+
+    /**
+     * 是否存在钥匙
+     */
+    var isExist: Boolean = false
+
+    /**
+     * 是否异常
+     */
+    var isException: Boolean = false
+
+    /**
+     * 是否就绪
+     */
+    var isReady: Boolean = false
+
+    /**
+     * 设备变化
+     */
+    var deviceChange: Boolean = false
+        set(value) {
+            if (field != value) {
+                android.util.Log.e("DeviceChange", "targetParam changed: $field -> $value\n" + android.util.Log.getStackTraceString(Exception("who touched targetParam")))
+            }
+            field = value
+        }
+
+
+    /**
+     * 双钥匙
+     */
+    class DeviceKey : DeviceModel() {
+        /**
+         * MAC地址
+         */
+        var mac: String = ""
+
+        /**
+         * 是否在充电
+         */
+        var isCharging: Boolean = false
+
+        /**
+         * 电量
+         */
+        var power: Int = 0
+        override fun toString(): String {
+            return "DeviceKey(mac='$mac', isCharging=$isCharging, power=$power, super=${super.toString()})"
+        }
+
+    }
+
+    /**
+     * 通用设备
+     */
+    class CommonDevice : DeviceModel()
+
+    /**
+     * 物资柜设备
+     */
+    class MaterialDevice : DeviceModel() {
+        /**
+         * 左门是否上锁
+         */
+        var leftDoorLocked: Boolean = false
+
+        /**
+         * 右门是否上锁
+         */
+        var rightDoorLocked: Boolean = false
+        override fun toString(): String {
+            return "MaterialDevice(leftDoorLocked=$leftDoorLocked, rightDoorLocked=$rightDoorLocked, super=${super.toString()})"
+        }
+
+
+    }
+
+    override fun toString(): String {
+        return "DeviceModel(nodeId=$nodeId, deviceType=$deviceType, id=$id, locked=$locked, newHardware=$newHardware, rfid='$rfid', isExist=$isExist, deviceChange=$deviceChange)"
+    }
+}

+ 158 - 0
app/src/main/java/com/grkj/iscs_mars/can/DeviceParseStatus.kt

@@ -0,0 +1,158 @@
+package com.grkj.iscs_mars.can
+
+
+/**
+ * 设备状态转换
+ */
+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 isFirst = deviceModel.isEmpty()
+        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 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
+                rightKeyModel.isCharging = ((rightKeyData.toInt() shr 1) and 1) == 1
+
+                if (leftKeyModel.isExist != leftExists) {
+                    leftKeyModel.isExist = leftExists
+                    leftKeyModel.deviceChange = true && !isFirst
+                }
+                if (rightKeyModel.isExist != rightExists) {
+                    rightKeyModel.isExist = rightExists
+                    rightKeyModel.deviceChange = true && !isFirst
+                }
+            }
+
+            CanCommands.Command.CONTROL_REG -> {
+                val keyData = statusData[0]
+                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)
+        val isFirst = deviceModel.isEmpty()
+        if (deviceModel.isEmpty()) {
+            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 { dev ->
+                    val exists = ((statusData[0].toInt() shr (dev.id - 1)) and 1) == 1
+                    if (dev.isExist != exists) {
+                        dev.isExist = exists
+                        dev.deviceChange = true && !isFirst
+                        CanHelper.updateDeviceData(nodeId, deviceModel)
+                    }
+                }
+                return
+            }
+
+            CanCommands.Command.CONTROL_REG -> {
+                deviceModel.forEach {
+                    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)
+        val isFirst = deviceModel.isEmpty()
+        if (deviceModel.isEmpty()) {
+            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 { dev ->
+                    val exists = ((statusData[0].toInt() shr (dev.id - 1)) and 1) == 1
+                    if (dev.isExist != exists) {
+                        dev.isExist = exists
+                        dev.deviceChange = true && !isFirst
+                        CanHelper.updateDeviceData(nodeId, deviceModel)
+                    }
+                }
+                return
+            }
+
+            CanCommands.Command.CONTROL_REG -> {
+                deviceModel.forEach {
+                    it.locked = ((statusData[0].toInt() shr (it.id - 1)) and 1) == 1
+                }
+            }
+        }
+        CanHelper.updateDeviceData(nodeId, deviceModel)
+    }
+
+    /**
+     * 物资柜控制板状态转换
+     */
+    fun parseMaterialCabinetControlBoardStatus(nodeId: Int, index: Int, statusData: ByteArray) {
+        var deviceModel = CanHelper.getDeviceByNodeId(nodeId)
+        val isFirst = deviceModel.isEmpty()
+        if (deviceModel.isEmpty()) {
+            deviceModel = mutableListOf<DeviceModel.MaterialDevice>()
+            deviceModel.add(DeviceModel.MaterialDevice().apply {
+                this.nodeId = nodeId
+                this.deviceType = CanDeviceConst.DEVICE_MATERIAL_CABINET_CONTROL_BOARD
+                this.id = 0
+            })
+        }
+        when (index) {
+            CanCommands.Command.CONTROL_REG -> {
+                deviceModel.filterIsInstance<DeviceModel.MaterialDevice>().forEach {
+                    val leftDoorLocked = ((statusData[0].toInt() shr 0) and 1) == 0
+                    it.leftDoorLocked = leftDoorLocked
+                    val rightDoorLocked = ((statusData[0].toInt() shr 4) and 1) == 0
+                    it.rightDoorLocked = rightDoorLocked
+                    it.deviceChange = true && !isFirst
+                }
+            }
+        }
+        CanHelper.updateDeviceData(nodeId, deviceModel)
+    }
+
+}

+ 61 - 0
app/src/main/java/com/grkj/iscs_mars/can/DockData.kt

@@ -0,0 +1,61 @@
+package com.grkj.iscs_mars.can
+
+/**
+ * Dock数据
+ */
+open class DockData {
+    var addr: Int = 0
+    var row: Int = 0
+    var col: Int = 0
+    var type: Int = -1
+
+    /**
+     * 设备数据
+     */
+    open class DeviceBean {
+        var addr: Int = 0
+        var idx: Int = 0
+        var isExist: Boolean = false
+        var row: Int = 0
+        var type: Int = -1
+        var rfid: String = ""
+        var isReady: Boolean = false
+        var newHardware: Boolean = false
+        override fun toString(): String {
+            return "DeviceBean(addr=$addr, idx=$idx, isExist=$isExist, row=$row, type=$type, rfid='$rfid', isReady=$isReady, newHardware=$newHardware)"
+        }
+
+    }
+
+    /**
+     * 钥匙Dock
+     */
+    class KeyDock : DockData() {
+        var keyData: MutableList<KeyBean> = mutableListOf()
+
+        class KeyBean : DeviceBean() {
+            var mac: String = ""
+        }
+
+        override fun toString(): String {
+            return "KeyDock(keyData=$keyData)"
+        }
+    }
+
+    /**
+     * 挂锁Dock
+     */
+    class LockDock : DockData() {
+        val lockData: MutableList<LockBean> = mutableListOf()
+
+        class LockBean : DeviceBean() {
+        }
+    }
+
+    /**
+     * 便携Dock
+     */
+    class PortableDock : DockData() {
+        val deviceData: MutableList<DeviceBean> = mutableListOf()
+    }
+}

+ 17 - 0
app/src/main/java/com/grkj/iscs_mars/can/HandlerGate.kt

@@ -0,0 +1,17 @@
+package com.grkj.iscs_mars.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)
+    }
+}

+ 256 - 0
app/src/main/java/com/grkj/iscs_mars/can/IHardwareHelper.kt

@@ -0,0 +1,256 @@
+package com.grkj.iscs_mars.can
+
+import com.grkj.iscs_mars.model.vo.hardware.CabinetSlotsRecord
+
+
+/**
+ * 硬件操作帮助类
+ */
+interface IHardwareHelper {
+
+    /**
+     * 连接并监听
+     */
+    fun connectAndAddListener()
+
+    /**
+     * 根据钥匙的RFID获取MAC地址
+     */
+    fun getKeyMacByRfid(rfid: String): String?
+
+    /**
+     * 获取挂锁
+     */
+    fun getLocks(
+        needLockCount: Int,
+        exceptionSlots: MutableList<CabinetSlotsRecord>,
+        exceptionLocks: MutableList<String>
+    ): MutableMap<Any, MutableList<Pair<Int, String>>>
+
+    /**
+     * 获取钥匙
+     */
+    suspend fun getOneKey(
+        exceptionSlots: List<CabinetSlotsRecord>,
+        exceptionKeysRfid: List<String>,
+        exceptionKeysMac: List<String> = mutableListOf()
+    ): Pair<Any, String?>?
+
+    /**
+     * 根据钥匙的MAC地址获取RFID
+     */
+    fun getRfidByKeyMac(keyMac: String): String?
+
+    /**
+     * 根据钥匙的RFID获取位置
+     */
+    fun getSlotPosition(keyNfc: String): String
+
+    /**
+     * 根据挂锁的RFID获取位置
+     */
+    fun getLockSlotPosition(lockNfc: String): String
+
+    /**
+     * 获取已经存在的钥匙MAC
+     */
+    fun getExistsKeyMac(): List<String>
+
+    /**
+     * 控制钥匙仓锁控和充电
+     */
+    fun controlKeyLockAndCharge(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 获取钥匙数据
+     */
+    fun getKeyDockData(): List<DockData.KeyDock>
+
+    /**
+     * 获取挂锁数据
+     */
+    fun getLockDockData(): List<DockData.LockDock>
+
+    /**
+     * 获取便携柜数据
+     */
+    fun getPortableDock(): List<DockData.PortableDock>
+
+    /**
+     * 控制钥匙仓位
+     */
+    fun controlKeyBuckle(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((res: ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 控制钥匙仓位根据mac地址
+     */
+    fun controlKeyBuckle(isOpen: Boolean, mac: String, done: ((ByteArray) -> Unit)? = null)
+
+    /**
+     * 控制仓位充电
+     */
+    fun controlKeyCharge(
+        isOpen: Boolean,
+        idx: Int,
+        slaveAddress: Int?,
+        done: ((res: ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 根据mac地址开关充电
+     */
+    fun controlKeyCharge(
+        isOpen: Boolean,
+        mac: String,
+        done: ((res: ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 控制锁仓
+     */
+    fun controlLockBuckle(
+        isOpen: Boolean,
+        slaveAddress: Int?,
+        lockIdxList: Int,
+        done: ((res: ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 批量控制锁仓
+     */
+    fun controlLockBuckle(
+        isOpen: Boolean,
+        slaveAddress: Int?,
+        lockIdxList: MutableList<Int>,
+        done: ((res: ByteArray) -> Unit)? = null
+    )
+
+    /**
+     * 读取钥匙的rfid
+     */
+    fun readKeyRfidStr(
+        slaveAddress: Int?,
+        idx: Int,
+        done: ((idx: Int, res: String) -> Unit)? = null
+    )
+
+    /**
+     * 读取挂锁rfid
+     */
+    fun readLockRfidStr(slaveAddress: Int?, lockIdx: Int, done: ((res: String) -> Unit)? = null)
+
+    /**
+     * 打开所有钥匙仓位
+     */
+    fun controlAllKeyBuckleOpen(complete: () -> Unit = {})
+
+    /**
+     * 关闭所有钥匙仓位
+     */
+    fun controlAllKeyBuckleClose(complete: () -> Unit = {})
+
+    /**
+     * 检查硬件连接数量是否错误
+     */
+    fun checkDock(): Boolean
+
+    /**
+     * 暂停读取
+     */
+    fun interruptReadTrashBinStatus(interrupt: Boolean)
+
+    /**
+     * 关闭所有钥匙充电
+     */
+    fun controlAllKeyChargeDown()
+
+    /**
+     * 更新钥匙电量
+     */
+    fun updateKeyPower(power: Int, mac: String)
+
+    /**
+     * 根据mac获取钥匙信息
+     */
+    fun getKeyBeanByMac(mac: String): DockData.KeyDock.KeyBean?
+
+    /**
+     * 更新就绪状态
+     */
+    fun updateKeyReadyStatus(mac: String, isReady: Boolean, from: Int)
+
+    /**
+     * 获取基座数据
+     */
+    fun getDockData(): List<DockData>
+
+    /**
+     * 更新钥匙mac
+     */
+    fun updateKeyMac(rfid: String, mac: String)
+
+    /**
+     * 全仓位开
+     */
+    fun allSlotOn()
+
+    /**
+     * 全仓位关
+     */
+    fun allSlotOff()
+
+    /**
+     * 是否有新挂锁
+     */
+    fun hasNewLock(): Boolean
+
+    /**
+     * 获取新挂锁RFID
+      */
+    fun getNewLockRFID(): List<String>
+
+    /**
+     * 移除要设备挂锁状态
+     */
+    fun removeNewHardwareLock(lockRfid: List<String>)
+
+    /**
+     * 更新锁rfid
+     */
+    fun updateLockRfid(addr: Int, idx: Int, rfid: String)
+
+    /**
+     * 更新锁是否为新设备
+     */
+    fun updateLockNewHardware(addr: Int, idx: Int, newHardware: Boolean)
+
+    /**
+     * 更新钥匙rfid
+     */
+    fun updateKeyRfid(addr: Int, idx: Int, rfid: String)
+
+    /**
+     * 更新钥匙是否为新设备
+     */
+    fun updateKeyNewHardware(addr: Int, idx: Int, newHardware: Boolean)
+
+    /**
+     * 更新钥匙mac
+     */
+    fun updateKeyMac(addr: Int, idx: Int, macAddress: String)
+
+    /**
+     * 开门
+     */
+    suspend fun openDoor(left: Boolean? = null, right: Boolean? = null): Boolean
+}

+ 67 - 0
app/src/main/java/com/grkj/iscs_mars/can/NodeIdHelper.kt

@@ -0,0 +1,67 @@
+package com.grkj.iscs_mars.can
+
+/**
+ * NodeId 枚举工具:
+ * - 方式A:线性递增 0x01..0xFF
+ * - 方式B:按置位个数递增 + 位索引字典序(组合序)
+ */
+object NodeIdHelper {
+
+    private const val MAX_BITS = 8
+    private const val MAX_MASK = (1 shl MAX_BITS) - 1 // 0xFF
+
+    /**
+     * 方式A:线性枚举(默认 0x01..0xFF)
+     * @param startInclusive 起始(含),会被裁剪到 [1, 0xFF]
+     * @param endInclusive   结束(含),会被裁剪到 [1, 0xFF]
+     */
+    suspend fun scanRangeLinear(
+        emit: suspend (Int) -> Unit,
+        startInclusive: Int = 1,
+        endInclusive: Int = MAX_MASK
+    ) {
+        val start = (startInclusive and MAX_MASK).coerceIn(1, MAX_MASK)
+        val end = (endInclusive and MAX_MASK).coerceIn(1, MAX_MASK)
+        require(start <= end) { "start must be <= end (within 1..$MAX_MASK)" }
+        for (id in start..end) emit(id)
+    }
+
+    /**
+     * 方式B:按置位个数递增(1..8),同一置位个数内按“位索引组合字典序”。
+     * 输出范围:0x01..0xFF,且无 0。
+     */
+    suspend fun scanRangeBySetBits(emit: suspend (Int) -> Unit) {
+        for (k in 1..MAX_BITS) {
+            generateKBitMasks(k, MAX_BITS, emit)
+        }
+    }
+
+    /** 生成固定置位个数 k 的所有掩码(组合字典序)。*/
+    private suspend fun generateKBitMasks(
+        k: Int,
+        n: Int,
+        emit: suspend (Int) -> Unit
+    ) {
+        val idx = IntArray(k) { it } // [0,1,2,...,k-1]
+        while (true) {
+            var mask = 0
+            for (i in 0 until k) mask = mask or (1 shl idx[i])
+            emit(mask)
+
+            var pos = k - 1
+            while (pos >= 0 && idx[pos] == pos + n - k) pos--
+            if (pos < 0) break
+            idx[pos]++
+            for (j in pos + 1 until k) idx[j] = idx[j - 1] + 1
+        }
+    }
+
+    /** 小工具:线性下一个(到头回 0x01) */
+    fun nextAfterLinear(cur: Int): Int {
+        val x = cur and MAX_MASK
+        return if (x in 1 until MAX_MASK) x + 1 else 1
+    }
+
+    /** 小工具:格式化成 0xNN */
+    fun toHex(id: Int): String = "0x%02X".format(id and MAX_MASK)
+}

+ 23 - 0
app/src/main/java/com/grkj/iscs_mars/enums/HardwareMode.kt

@@ -0,0 +1,23 @@
+package com.grkj.iscs_mars.enums
+
+import com.grkj.iscs_mars.can.CanHardwareHelper
+import com.grkj.iscs_mars.can.IHardwareHelper
+
+/**
+ * 硬件模式
+ */
+enum class HardwareMode(val iHardwareHelper: IHardwareHelper) {
+
+    // RS485(ModBusHardwareHelper()),
+    CAN(CanHardwareHelper());
+
+    companion object {
+        /**
+         * 根据当前硬件模式获取硬件读写帮助类
+         */
+        fun getCurrentHardwareMode(): IHardwareHelper {
+            val currentHardwareMode = valueOf(CAN.name)
+            return currentHardwareMode.iHardwareHelper
+        }
+    }
+}

+ 20 - 0
app/src/main/java/com/grkj/iscs_mars/extentions/ByteArray.kt

@@ -94,4 +94,24 @@ fun ByteArray.crc16(from: Int = 0, to: Int = size) : ByteArray {
     val c1 = (0xff00 and value shr 8).toByte()
     val c2 = (0xff and value).toByte()
     return byteArrayOf(c1, c2)
+}
+
+/**
+ * ByteArray扩展:小端字节序 → 十六进制字符串(大端显示)
+ */
+fun ByteArray.toHexFromLe(
+    prefix: Boolean = false,
+    upper: Boolean = true,
+    width: Int? = null
+): String {
+    val table = if (upper) "0123456789ABCDEF" else "0123456789abcdef"
+    val sb = StringBuilder(this.size * 2)
+    for (i in this.lastIndex downTo 0) {
+        val v = this[i].toInt() and 0xFF
+        sb.append(table[v ushr 4])
+        sb.append(table[v and 0x0F])
+    }
+    var s = sb.toString()
+    if (width != null) s = s.padStart(width, '0')
+    return if (prefix) "0x$s" else s
 }

+ 1 - 1
app/src/main/java/com/grkj/iscs_mars/extentions/Context.kt

@@ -23,7 +23,7 @@ fun Context.removeNetObserver(observer: Observer<Boolean>) {
 
 @SuppressLint("MissingPermission")
 fun Context.serialNo(): String {
-    return DeviceUtils.getDeviceId(length = 10)
+    return "20FFFFFFA0"//DeviceUtils.getDeviceId(length = 10)
 }
 
 /**

+ 11 - 58
app/src/main/java/com/grkj/iscs_mars/presentation/PresentationLoginActivity.kt

@@ -6,6 +6,8 @@ import android.view.View
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs_mars.BusinessManager
+import com.grkj.iscs_mars.can.CanDeviceConst
+import com.grkj.iscs_mars.can.DeviceModel
 import com.grkj.iscs_mars.view.base.BaseActivity
 import com.grkj.iscs_mars.databinding.ActivityPresentationLoginBinding
 import com.grkj.iscs_mars.extentions.removeLeadingZeros
@@ -66,65 +68,16 @@ class PresentationLoginActivity : BaseActivity<ActivityPresentationLoginBinding>
             handleLoading(false)
         }
 
-        BusinessManager.registerStatusListener(this) { dockBean ->
-            when (dockBean.type) {
-                DOCK_TYPE_PORTABLE -> {
-                    dockBean.deviceList.forEach { deviceBean ->
-                        if (deviceBean.isExist) {
-                            when (deviceBean.type) {
-                                DEVICE_TYPE_CARD -> {
-                                    ModBusController.readPortalCaseCardRfid(dockBean.addr) { res ->
-                                        if (res.size >= 11) {
-                                            val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                                            LogUtil.i("卡片RFID : $rfid")
-                                            if(window.decorView.visibility != View.VISIBLE) {
-                                                val current = ActivityUtils.currentActivity()
-                                                if (current is LockerActivity) {
-                                                    current.showDlg(rfid)
-                                                } else if (current is LockerTogetherActivity) {
-                                                    current.showDlg(rfid)
-                                                }
-                                            } else {
-                                                if (PresentationManager.cardList.any { it.rfid == rfid && it.isLocker}) {
-                                                    ToastUtils.tip("登录成功,欢迎 $rfid")
-                                                    val intent = Intent(this, LockerActivity::class.java)
-                                                    intent.putExtra("card", rfid)
-                                                    startActivity(intent)
-                                                } else if (PresentationManager.cardList.any { it.rfid == rfid && !it.isLocker}) {
-                                                    ToastUtils.tip("登录成功,欢迎 $rfid")
-                                                    val intent = Intent(this, LockerTogetherActivity::class.java)
-                                                    intent.putExtra("card", rfid)
-                                                    startActivity(intent)
-                                                } else {
-                                                    ToastUtils.tip("登录失败,$rfid 没有权限")
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
+        BusinessManager.registerStatusListener(this) { deviceDatas ->
 
-
-                                DEVICE_TYPE_KEY -> {
-                                    Executor.runOnMain {
-//                                        BusinessManager.getTicketStatusBusiness(PresentationManager.keyMac, this) { b, s, rst ->
-//                                            handleLoading(b, s)
-//                                            if (!s.isNullOrEmpty() && s.startsWith("工作票完成接收")) {
-//                                                val ticket = s.substring(2)
-//                                                val ticketStatusBean = Gson().fromJson(ticket, WorkTicketStatusBean::class.java)
-//                                                PresentationManager.updateWorkTicket(ticketStatusBean)
-//                                            }
-//                                        }
-                                    }
-                                }
-                                DEVICE_TYPE_LOCK -> {
-                                    Executor.runOnMain {
-                                        ModBusController.readLockRfid(dockBean.addr, deviceBean.idx) { res ->
-                                            val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                                            PresentationManager.updateLockReturnStatus(rfid)
-                                        }
-                                    }
-                                }
-                            }
+            deviceDatas.filterIsInstance<DeviceModel.DeviceKey>().forEach { deviceModel ->
+                when(deviceModel.deviceType) {
+                    CanDeviceConst.DEVICE_LOCK_DOCK -> {
+                        Executor.runOnMain {
+//                            ModBusController.readLockRfid(dockBean.addr, deviceBean.idx) { res ->
+//                                val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
+//                                PresentationManager.updateLockReturnStatus(rfid)
+//                            }
                         }
                     }
                 }

+ 0 - 16
app/src/main/java/com/grkj/iscs_mars/presentation/PresentationPresenter.kt

@@ -9,23 +9,7 @@ import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_PORTABLE
 class PresentationPresenter : BasePresenter<IPresentationView>() {
     fun registerStatusListener() {
         BusinessManager.registerStatusListener(this) { dockBean ->
-            when (dockBean.type) {
-                DOCK_TYPE_PORTABLE -> {
-                    // TODO 便携式待完善
-                    dockBean.deviceList.forEach { deviceBean ->
-                        if (deviceBean.isExist) {
-                            when (deviceBean.type) {
-                                DEVICE_TYPE_KEY -> {
-                                    // TODO 读工作票
-                                }
-                                DEVICE_TYPE_LOCK -> {
 
-                                }
-                            }
-                        }
-                    }
-                }
-            }
         }
     }
 }

+ 4 - 2
app/src/main/java/com/grkj/iscs_mars/util/CrashUtil.kt

@@ -6,7 +6,9 @@ import android.content.pm.PackageManager
 import android.os.Build
 import android.os.Looper
 import com.grkj.iscs_mars.util.log.LogUtil
-import java.io.*
+import java.io.PrintWriter
+import java.io.StringWriter
+import java.io.Writer
 import java.lang.reflect.Field
 import java.text.DateFormat
 import java.text.SimpleDateFormat
@@ -91,7 +93,7 @@ class CrashUtil : Thread.UncaughtExceptionHandler {
             val pm = ctx.packageManager
             val pi = pm.getPackageInfo(ctx.packageName, PackageManager.GET_ACTIVITIES)
             pi?.let {
-                val versionName = if (it.versionName == null) "null" else it.versionName
+                val versionName = if (it.versionName == null) "null" else (it.versionName ?: "null")
                 val versionCode = it.versionCode.toString()
                 infos["versionName"] = versionName
                 infos["versionCode"] = versionCode

+ 4 - 3
app/src/main/java/com/grkj/iscs_mars/util/NetHttpManager.kt

@@ -1,6 +1,7 @@
 package com.grkj.iscs_mars.util
 
 import android.content.Context
+import android.util.Log
 import cn.zhxu.data.Mapper
 import cn.zhxu.okhttps.HTTP
 import cn.zhxu.okhttps.HttpResult
@@ -171,12 +172,12 @@ class NetHttpManager {
                         callback(it.body, null, it.status)
                     }
                 } else {
-                    var bobyStr = it.body.toString()
+                    val bodyStr = it.body.toString()
                     callback(
-                        null, if (bobyStr.isNullOrEmpty()) {
+                        null, if (bodyStr.isNullOrEmpty()) {
                             it.toString()
                         } else {
-                            bobyStr
+                            bodyStr
                         }, it.status
                     )
                 }

+ 1 - 0
app/src/main/java/com/grkj/iscs_mars/view/activity/HomeActivity.kt

@@ -7,6 +7,7 @@ import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.Observer
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
+import com.grkj.iscs_mars.can.CanHardwareHelper
 import com.grkj.iscs_mars.databinding.ActivityHomeBinding
 import com.grkj.iscs_mars.extentions.toByteArrays
 import com.grkj.iscs_mars.extentions.toHexStrings

+ 3 - 1
app/src/main/java/com/grkj/iscs_mars/view/fragment/DeviceRegistrationTypeSelectFragment.kt

@@ -1,6 +1,7 @@
 package com.grkj.iscs_mars.view.fragment
 
 import com.grkj.iscs_mars.R
+import com.grkj.iscs_mars.can.CanHardwareHelper
 import com.grkj.iscs_mars.databinding.FragmentDeviceRegistrationTypeSelectBinding
 import com.grkj.iscs_mars.enums.DeviceInputTypeEnum
 import com.grkj.iscs_mars.modbus.ModBusController
@@ -39,6 +40,7 @@ class DeviceRegistrationTypeSelectFragment :
         super.onResume()
         ISCSDomainData.isDeviceRegistration = true
         LogUtil.d("设备录入-打开所有钥匙仓并关闭充电")
-        ModBusController.controlAllKeyBuckleOpen()
+        // ModBusController.controlAllKeyBuckleOpen()
+        CanHardwareHelper().controlAllKeyBuckleOpen {  }
     }
 }

+ 15 - 21
app/src/main/java/com/grkj/iscs_mars/view/fragment/DockTestFragment.kt

@@ -4,6 +4,7 @@ import android.view.View
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs_mars.R
+import com.grkj.iscs_mars.can.CanHardwareHelper
 import com.grkj.iscs_mars.databinding.FragmentDockTestBinding
 import com.grkj.iscs_mars.extentions.removeLeadingZeros
 import com.grkj.iscs_mars.extentions.toHexStrings
@@ -12,7 +13,6 @@ import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs_mars.model.DeviceConst.DOCK_TYPE_LOCK
 import com.grkj.iscs_mars.util.SPUtils
 import com.grkj.iscs_mars.util.ToastUtils
-import com.grkj.iscs_mars.util.log.LogUtil
 import com.grkj.iscs_mars.view.base.BaseFragment
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
@@ -50,7 +50,7 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                 dock.deviceList = mutableListOf(1, 2)
             }
             mLockDockList.forEach { dock ->
-                dock.deviceList = mutableListOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
+                dock.deviceList = mutableListOf(1, 2, 3, 4, 5)
             }
         }
 
@@ -70,18 +70,16 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                     override fun convert(holder: ViewHolder, deviceIndex: Int, position: Int) {
                         holder.setText(R.id.tv_name, getString(R.string.device_index, deviceIndex))
                         holder.setOnClickListener(R.id.tv_turn_on) {
-                            ModBusController.controlKeyBuckle(true, deviceIndex == 1, dock.address)
-                            ModBusController.controlKeyCharge(
-                                false,
-                                deviceIndex == 1,
-                                dock.address
-                            ) {
-                                LogUtil.i("关闭充电:${it.toHexStrings()}")
+                            CanHardwareHelper().apply {
+                                controlKeyBuckle(true, deviceIndex - 1, dock.address.toInt())
+                                controlKeyCharge(false, deviceIndex - 1, dock.address.toInt())
                             }
                         }
                         holder.setOnClickListener(R.id.tv_turn_off) {
-                            ModBusController.controlKeyBuckle(false, deviceIndex == 1, dock.address)
-                            ModBusController.controlKeyCharge(true, deviceIndex == 1, dock.address)
+                            CanHardwareHelper().apply {
+                                controlKeyBuckle(false, deviceIndex - 1, dock.address.toInt())
+                                controlKeyCharge(true, deviceIndex - 1, dock.address.toInt())
+                            }
                         }
                         holder.setOnClickListener(R.id.tv_read) {
                             ModBusController.readKeyRfid(
@@ -114,10 +112,10 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                     override fun convert(holder: ViewHolder, deviceIndex: Int, position: Int) {
                         holder.setText(R.id.tv_name, getString(R.string.device_index, deviceIndex))
                         holder.setOnClickListener(R.id.tv_turn_on) {
-                            ModBusController.controlLockBuckle(true, dock.address, deviceIndex - 1)
+                            CanHardwareHelper().controlLockBuckle(true, dock.address.toInt(), deviceIndex)
                         }
                         holder.setOnClickListener(R.id.tv_turn_off) {
-                            ModBusController.controlLockBuckle(false, dock.address, deviceIndex - 1)
+                            CanHardwareHelper().controlLockBuckle(false, dock.address.toInt(), deviceIndex)
                         }
                         holder.setOnClickListener(R.id.tv_read) {
                             ModBusController.readLockRfid(
@@ -137,26 +135,22 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
         mBinding?.cbOpen?.setOnClickListener {
             mKeyDockList.forEach { dock ->
                 dock.deviceList?.forEach { deviceIndex ->
-                    ModBusController.controlKeyBuckle(true, deviceIndex == 1, dock.address)
+                    CanHardwareHelper().controlKeyBuckle(true, deviceIndex - 1, dock.address.toInt())
                 }
             }
             mLockDockList.forEach { dock ->
-                dock.deviceList?.forEach { deviceIndex ->
-                    ModBusController.controlLockBuckle(true, dock.address, deviceIndex - 1)
-                }
+                CanHardwareHelper().controlLockBuckle(true, dock.address.toInt(), dock.deviceList ?: mutableListOf())
             }
         }
 
         mBinding?.cbClose?.setOnClickListener {
             mKeyDockList.forEach { dock ->
                 dock.deviceList?.forEach { deviceIndex ->
-                    ModBusController.controlKeyBuckle(false, deviceIndex == 1, dock.address)
+                    CanHardwareHelper().controlKeyBuckle(false, deviceIndex - 1, dock.address.toInt())
                 }
             }
             mLockDockList.forEach { dock ->
-                dock.deviceList?.forEach { deviceIndex ->
-                    ModBusController.controlLockBuckle(false, dock.address, deviceIndex - 1)
-                }
+                CanHardwareHelper().controlLockBuckle(false, dock.address.toInt(), dock.deviceList ?: mutableListOf())
             }
         }
     }

+ 1 - 0
app/src/main/java/com/grkj/iscs_mars/view/presenter/DeviceStatusPresenter.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs_mars.view.presenter
 
 import android.annotation.SuppressLint
 import android.graphics.Bitmap
+import android.util.Log
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs_mars.BusinessManager

+ 1 - 49
app/src/main/java/com/grkj/iscs_mars/view/presenter/HomePresenter.kt

@@ -28,55 +28,7 @@ class HomePresenter : BasePresenter<IHomeView>() {
      */
     fun registerStatusListener() {
         BusinessManager.registerStatusListener(this) { dockBean ->
-            if (!BusinessManager.CAN_RETURN || ISCSDomainData.isDeviceRegistration) {
-                return@registerStatusListener
-            }
-            when (dockBean.type) {
-                DOCK_TYPE_KEY -> {
-                    dockBean.getKeyList().forEach { keyBean ->
-                        if (keyBean.isExist) {
-                            Executor.repeatOnMain({
-                                keyBean.mac?.let { mac ->
-                                    ThreadUtils.runOnIO {
-                                        fun readJobTicket(mac: String) {
-                                            BleReturnDispatcher.submit(mac) { isConnect ->
-                                                if (isConnect) {
-                                                    val bleBean =
-                                                        BusinessManager.getBleDeviceByMac(mac)
-                                                    Executor.delayOnMain(300) {
-                                                        bleBean?.let {
-                                                            BusinessManager.sendLoadingEventMsg(
-                                                                mContext?.getString(
-                                                                    R.string.loading_msg_get_ticket_status_start
-                                                                )
-                                                            )
-                                                            BusinessManager.getCurrentStatus(
-                                                                4,
-                                                                it.bleDevice
-                                                            )
-                                                        }
-                                                    }
-                                                } else {
-                                                    ModBusController.controlKeyBuckle(true, mac) {
-                                                        ToastUtils.tip(
-                                                            CommonUtils.getStr(R.string.ticket_get_failed)
-                                                                .toString()
-                                                        )
-                                                    }
-                                                }
-                                            }
-                                        }
-                                        if (!ISCSDomainData.isDeviceRegistration) {
-                                            readJobTicket(mac)
-                                        }
-                                    }
-                                }
-                                return@repeatOnMain keyBean.mac == null
-                            }, 1000, true)
-                        }
-                    }
-                }
-            }
+
         }
     }
 

+ 2 - 23
app/src/main/java/com/grkj/iscs_mars/view/presenter/LoginPresenter.kt

@@ -4,11 +4,7 @@ import android.content.Context
 import android.graphics.Bitmap
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
-import com.grkj.iscs_mars.extentions.removeLeadingZeros
-import com.grkj.iscs_mars.extentions.toHexStrings
-import com.grkj.iscs_mars.modbus.ModBusController
 import com.grkj.iscs_mars.model.Constants
-import com.grkj.iscs_mars.model.DeviceConst.DEVICE_TYPE_CARD
 import com.grkj.iscs_mars.model.vo.FileStreamReqParam
 import com.grkj.iscs_mars.model.vo.user.UserInfoRespVO
 import com.grkj.iscs_mars.util.BitmapUtil
@@ -16,7 +12,6 @@ import com.grkj.iscs_mars.util.Executor
 import com.grkj.iscs_mars.util.NetApi
 import com.grkj.iscs_mars.util.SPUtils
 import com.grkj.iscs_mars.util.ToastUtils
-import com.grkj.iscs_mars.util.log.LogUtil
 import com.grkj.iscs_mars.view.base.BasePresenter
 import com.grkj.iscs_mars.view.iview.ILoginView
 
@@ -104,24 +99,8 @@ class LoginPresenter : BasePresenter<ILoginView>() {
     }
 
     fun registerListener() {
-        BusinessManager.registerStatusListener(this) { dockBean ->
-            dockBean.deviceList.forEach { deviceBean ->
-                if (!deviceBean.isExist) {
-                    return@forEach
-                }
-                when (deviceBean.type) {
-                    DEVICE_TYPE_CARD -> {
-                        ModBusController.readPortalCaseCardRfid(dockBean.addr) { res ->
-                            if (res.size >= 11) {
-                                val rfid =
-                                    res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
-                                LogUtil.i("卡片RFID : $rfid")
-                                // TODO 跳转页面处理
-                            }
-                        }
-                    }
-                }
-            }
+        BusinessManager.registerStatusListener(this) {
+
         }
     }
 

+ 1 - 1
gradle/libs.versions.toml

@@ -1,5 +1,5 @@
 [versions]
-agp = "8.5.2"
+agp = "8.6.0"
 kotlin = "2.1.10"
 coreKtx = "1.13.0"
 junit = "4.13.2"