Przeglądaj źródła

merge(从主分支合并)

周文健 5 miesięcy temu
rodzic
commit
9172f53561
66 zmienionych plików z 2497 dodań i 1078 usunięć
  1. 1 0
      .gitignore
  2. 0 3
      .idea/.gitignore
  3. 0 1
      .idea/.name
  4. 0 5
      .idea/codeStyles/codeStyleConfig.xml
  5. 0 18
      .idea/deploymentTargetSelector.xml
  6. 0 6
      .idea/kotlinc.xml
  7. 0 10
      .idea/migrations.xml
  8. 12 0
      app/build.gradle
  9. 1 1
      app/src/main/AndroidManifest.xml
  10. 263 197
      app/src/main/java/com/grkj/iscs/BusinessManager.kt
  11. 4 1
      app/src/main/java/com/grkj/iscs/MyApplication.kt
  12. 10 5
      app/src/main/java/com/grkj/iscs/extentions/Context.kt
  13. 238 31
      app/src/main/java/com/grkj/iscs/modbus/DockBean.kt
  14. 64 0
      app/src/main/java/com/grkj/iscs/modbus/FrameTask.kt
  15. 94 0
      app/src/main/java/com/grkj/iscs/modbus/MBFrame.kt
  16. 142 0
      app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt
  17. 13 0
      app/src/main/java/com/grkj/iscs/modbus/ModBusConstants.kt
  18. 333 72
      app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt
  19. 125 426
      app/src/main/java/com/grkj/iscs/modbus/ModBusManager.kt
  20. 12 7
      app/src/main/java/com/grkj/iscs/modbus/PortManager.kt
  21. 10 0
      app/src/main/java/com/grkj/iscs/modbus/StatusListener.kt
  22. 1 0
      app/src/main/java/com/grkj/iscs/model/DeviceConst.kt
  23. 29 0
      app/src/main/java/com/grkj/iscs/model/ISCSDatabase.kt
  24. 11 0
      app/src/main/java/com/grkj/iscs/model/ISCSMigrations.kt
  25. 20 3
      app/src/main/java/com/grkj/iscs/model/UrlConsts.kt
  26. 41 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/CabinetSlotsRespVo.kt
  27. 28 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/UpdateHardwareEsStatusReqVO.kt
  28. 2 0
      app/src/main/java/com/grkj/iscs/util/BitmapUtil.kt
  29. 204 35
      app/src/main/java/com/grkj/iscs/util/NetApi.kt
  30. 1 34
      app/src/main/java/com/grkj/iscs/util/NetHttpManager.kt
  31. 23 16
      app/src/main/java/com/grkj/iscs/util/SPUtils.kt
  32. 2 0
      app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt
  33. 2 0
      app/src/main/java/com/grkj/iscs/view/dialog/LoginDialog.kt
  34. 151 16
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt
  35. 45 8
      app/src/main/java/com/grkj/iscs/view/fragment/JobProgressFragment.kt
  36. 135 75
      app/src/main/java/com/grkj/iscs/view/fragment/StepFragment.kt
  37. 20 0
      app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt
  38. 6 0
      app/src/main/java/com/grkj/iscs/view/iview/ISwitchStatusView.kt
  39. 19 1
      app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt
  40. 76 25
      app/src/main/java/com/grkj/iscs/view/presenter/JobProgressPresenter.kt
  41. 27 2
      app/src/main/java/com/grkj/iscs/view/presenter/StepPresenter.kt
  42. 7 0
      app/src/main/java/com/grkj/iscs/view/presenter/SwitchStatusPresenter.kt
  43. 7 1
      app/src/main/java/com/grkj/iscs/view/step_mode/IStepMode.kt
  44. 6 2
      app/src/main/java/com/grkj/iscs/view/step_mode/StepMode1.kt
  45. 15 5
      app/src/main/java/com/grkj/iscs/view/step_mode/StepMode2.kt
  46. 81 46
      app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt
  47. 8 0
      app/src/main/res/drawable/common_status_circle.xml
  48. 7 0
      app/src/main/res/drawable/divider_dock_lock_status.xml
  49. 21 0
      app/src/main/res/drawable/dock_has_lock.xml
  50. 2 2
      app/src/main/res/drawable/dock_lock_selector.xml
  51. 9 0
      app/src/main/res/drawable/dock_no_lock.xml
  52. 9 0
      app/src/main/res/drawable/menu_icon_device_input.xml
  53. 21 0
      app/src/main/res/drawable/red_stroke_bg.xml
  54. 14 0
      app/src/main/res/layout/fragment_switch_status.xml
  55. 57 15
      app/src/main/res/layout/item_rv_key_dock_status.xml
  56. 34 6
      app/src/main/res/layout/item_rv_lock_dock_child_status.xml
  57. 1 1
      app/src/main/res/layout/item_rv_lock_dock_status.xml
  58. BIN
      app/src/main/res/mipmap/dock_has_lock.png
  59. BIN
      app/src/main/res/mipmap/dock_no_lock.png
  60. 4 0
      app/src/main/res/values-en/strings.xml
  61. 4 0
      app/src/main/res/values-zh/strings.xml
  62. 4 0
      app/src/main/res/values/colors.xml
  63. 3 1
      app/src/main/res/values/dimens.xml
  64. 4 0
      app/src/main/res/values/strings.xml
  65. 1 0
      build.gradle
  66. 13 1
      gradle/libs.versions.toml

+ 1 - 0
.gitignore

@@ -16,3 +16,4 @@
 .cxx
 local.properties
 .idea/
+.kotlin

+ 0 - 3
.idea/.gitignore

@@ -1,3 +0,0 @@
-# Default ignored files
-/shelf/
-/workspace.xml

+ 0 - 1
.idea/.name

@@ -1 +0,0 @@
-ISCS

+ 0 - 5
.idea/codeStyles/codeStyleConfig.xml

@@ -1,5 +0,0 @@
-<component name="ProjectCodeStyleConfiguration">
-  <state>
-    <option name="USE_PER_PROJECT_SETTINGS" value="true" />
-  </state>
-</component>

+ 0 - 18
.idea/deploymentTargetSelector.xml

@@ -1,18 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="deploymentTargetSelector">
-    <selectionStates>
-      <SelectionState runConfigName="app">
-        <option name="selectionMode" value="DROPDOWN" />
-        <DropdownSelection timestamp="2024-09-06T03:12:23.017429800Z">
-          <Target type="DEFAULT_BOOT">
-            <handle>
-              <DeviceId pluginId="LocalEmulator" identifier="path=C:\Users\Frankensteinly\.android\avd\Pixel_6_Pro_API_31.avd" />
-            </handle>
-          </Target>
-        </DropdownSelection>
-        <DialogSelection />
-      </SelectionState>
-    </selectionStates>
-  </component>
-</project>

+ 0 - 6
.idea/kotlinc.xml

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="KotlinJpsPluginSettings">
-    <option name="version" value="1.9.0" />
-  </component>
-</project>

+ 0 - 10
.idea/migrations.xml

@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
-  <component name="ProjectMigrations">
-    <option name="MigrateToGradleLocalJavaHome">
-      <set>
-        <option value="$PROJECT_DIR$" />
-      </set>
-    </option>
-  </component>
-</project>

+ 12 - 0
app/build.gradle

@@ -1,6 +1,8 @@
 plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.jetbrains.kotlin.android)
+    id("org.jetbrains.kotlin.kapt")
+    id("com.google.devtools.ksp")
 }
 
 android {
@@ -83,6 +85,16 @@ dependencies {
     implementation libs.autosize
     implementation libs.fastble
 
+    implementation(libs.viewmodel.ktx)
+    implementation(libs.viewmodel.livedata.ktx)
+    implementation(libs.viewmodel.savestate)
+    kapt(libs.viewmodel.compiler)
+
+    implementation(libs.room.runtime)
+    implementation(libs.room.ktx)
+    implementation(libs.room.testing)
+    ksp(libs.room.compiler)
+
     implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
 
     // RV通用Adapter  https://github.com/hongyangAndroid/base-adapter

+ 1 - 1
app/src/main/AndroidManifest.xml

@@ -53,7 +53,7 @@
         <activity
             android:name=".view.activity.LoginActivity"
             android:exported="true"
-            android:launchMode="singleTask" >
+            android:launchMode="singleTask">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
 

+ 263 - 197
app/src/main/java/com/grkj/iscs/BusinessManager.kt

@@ -67,6 +67,11 @@ import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.activity.LoginActivity
 import com.grkj.iscs.view.base.BaseActivity
 import com.grkj.iscs.view.dialog.TipDialog
+import com.sik.sikcore.activity.ActivityTracker
+import com.sik.sikcore.extension.getMMKVData
+import com.sik.sikcore.extension.saveMMKVData
+import com.sik.sikcore.extension.toJson
+import com.tencent.mmkv.MMKV
 import pub.devrel.easypermissions.AfterPermissionGranted
 
 /**
@@ -214,27 +219,39 @@ object BusinessManager {
     }
 
     /****************************************** ModBus ******************************************/
-
+    /**
+     * 链接底座
+     */
     fun connectDock(isNeedInit: Boolean = false) {
-        // 不再使用SlaveCount,改用地址池
-//        ModBusController.setSlaveCount(100)
         ModBusController.interruptReadTrashBinStatus(false)
         ModBusController.start(MyApplication.instance!!.applicationContext)
         ModBusController.unregisterListener(MyApplication.instance!!.applicationContext)
 
         if (isNeedInit) {
             ModBusController.initDevicesStatus()
+            ModBusController.updateAllBuckleStatus {
+                //todo 上传开关信息
+            }
         }
     }
 
+    /**
+     * 断开底座链接
+     */
     fun disconnectDock() {
         ModBusController.stop()
     }
 
+    /**
+     * 注册状态监听
+     */
     fun registerStatusListener(key: Any, listener: (DockBean) -> Unit) {
         listeners.add(DeviceListener(key, listener))
     }
 
+    /**
+     * 取消注册状态监听
+     */
     fun unregisterListener(key: Any) {
         val it = listeners.iterator()
         while (it.hasNext()) {
@@ -249,236 +266,256 @@ object BusinessManager {
      */
     fun registerMainListener() {
         ModBusController.registerStatusListener(this) { res ->
-            LogUtil.i("硬件状态:${(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)}")
-                    }
+            deviceStatusHandle(res)
+        }
+    }
+
+    /**
+     * 硬件状态
+     */
+    private fun deviceStatusHandle(res: Any) {
+        LogUtil.i("硬件状态:${(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)}")
                 }
-                tipStr += addressList
-                ToastUtils.tip(tipStr)
             }
-            res.forEach { bytes ->
-                val dockBean = ModBusController.updateStatus(bytes) ?: return@forEach
-                ModBusController.isInitReady = true
-                if (!CAN_RETURN) {
-                    return@forEach
-                }
-                when (dockBean.type) {
-                    DOCK_TYPE_KEY -> {
-                        dockBean.getKeyList().forEach { keyBean ->
-                            if (keyBean.isExist) {
-                                sendLoadingEventMsg(CommonUtils.getStr(R.string.ble_connecting))
-                                // 放回钥匙,读取rfid
-                                ModBusController.readKeyRfid(
+            tipStr += addressList
+            ToastUtils.tip(tipStr)
+        }
+        res.forEach { bytes ->
+            val dockBean = ModBusController.updateStatus(bytes) ?: return@forEach
+            ModBusController.isInitReady = true
+            if (!CAN_RETURN) {
+                return@forEach
+            }
+            when (dockBean.type) {
+                DOCK_TYPE_KEY -> {
+                    dockBean.getKeyList().forEach { keyBean ->
+                        if (keyBean.isExist) {
+                            sendLoadingEventMsg(CommonUtils.getStr(R.string.ble_connecting))
+                            // 放回钥匙,读取rfid
+                            ModBusController.readKeyRfid(
+                                dockBean.addr,
+                                if (keyBean.isLeft) 0 else 1
+                            ) { isLeft, res ->
+                                if (res.size < 11) {
+                                    LogUtil.e("Key rfid error")
+                                    return@readKeyRfid
+                                }
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                    .removeLeadingZeros()
+                                ModBusController.updateKeyRfid(
                                     dockBean.addr,
-                                    if (keyBean.isLeft) 0 else 1
-                                ) { isLeft, res ->
-                                    if (res.size < 11) {
-                                        LogUtil.e("Key rfid error")
-                                        return@readKeyRfid
-                                    }
-                                    val rfid = res.copyOfRange(3, 11).toHexStrings(false)
-                                        .removeLeadingZeros()
-                                    ModBusController.updateKeyRfid(
-                                        dockBean.addr,
-                                        keyBean.isLeft,
-                                        rfid
-                                    )
-                                    NetApi.getKeyInfo(rfid) {
-                                        if (it != null && !it.macAddress.isNullOrEmpty()) {
-                                            ModBusController.updateKeyMac(
-                                                dockBean.addr,
-                                                keyBean.isLeft,
-                                                it.macAddress
-                                            )
-                                            ModBusController.updateKeyReadyStatus(
-                                                it.macAddress,
-                                                false,
-                                                5
-                                            )
-                                            // TODO 移动到HomePresenter里,根据业务进行处理
-//                                            showKeyReturnDialog(it.macAddress, isLeft, dockBean.addr.toInt())
-//                                            registerConnectListener(it.macAddress)
-                                        } else {
-                                            LogUtil.e("Get key info fail : $rfid")
-                                            ToastUtils.tip(R.string.get_key_info_fail)
-                                        }
+                                    keyBean.isLeft,
+                                    rfid
+                                )
+                                NetApi.getKeyInfo(rfid) {
+                                    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")
+                                        ToastUtils.tip(R.string.get_key_info_fail)
                                     }
-                                    // TODO 蓝牙通信
                                 }
+                                // TODO 蓝牙通信
+                            }
 //                                ModBusController.controlKeyBuckle(false, isLeft = true, dockBean.addr.toInt() - 1)
-                            } else {
-                                // 移出待连监听集合,防止connectKey循环失败
-                                keyBean.mac?.let {
-                                    unregisterConnectListener(it)
-                                }
-                                sendEventMsg(
-                                    MsgEvent(
-                                        MSG_EVENT_DEVICE_TAKE_UPDATE,
-                                        DeviceTakeUpdateMsg(DEVICE_TYPE_KEY, keyBean.rfid)
-                                    )
-                                )
+                        } else {
+                            // 移出待连监听集合,防止connectKey循环失败
+                            keyBean.mac?.let {
+                                unregisterConnectListener(it)
                             }
+                            sendEventMsg(
+                                MsgEvent(
+                                    MSG_EVENT_DEVICE_TAKE_UPDATE,
+                                    DeviceTakeUpdateMsg(DEVICE_TYPE_KEY, keyBean.rfid)
+                                )
+                            )
                         }
                     }
+                }
 
-                    DOCK_TYPE_LOCK -> {
-                        dockBean.getLockList().forEach { lockBean ->
-                            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
-                                    )
-                                    NetApi.getLockInfo(rfid) {
-                                        if (it != null) {
-                                            // TODO 考虑快速拿取
-                                            ModBusController.controlLockBuckle(
-                                                false,
-                                                dockBean.addr,
-                                                lockBean.idx
-                                            ) { itRst ->
-                                                if (itRst.isNotEmpty()) {
-                                                    // 上报锁具信息
-                                                    NetApi.updateLockReturn(
-                                                        rfid,
-                                                        MyApplication.instance!!.serialNo()
-                                                    ) {}
-                                                }
+                DOCK_TYPE_LOCK -> {
+                    dockBean.getLockList().forEach { lockBean ->
+                        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
+                                )
+                                NetApi.getLockInfo(rfid) {
+                                    if (it != null) {
+                                        // TODO 考虑快速拿取
+                                        ModBusController.controlLockBuckle(
+                                            false,
+                                            dockBean.addr,
+                                            lockBean.idx
+                                        ) { itRst ->
+                                            if (itRst.isNotEmpty()) {
+                                                // 上报锁具信息
+                                                NetApi.updateLockReturn(
+                                                    rfid,
+                                                    MyApplication.instance!!.serialNo()
+                                                ) {}
                                             }
                                         }
                                     }
                                 }
-                            } else {
-                                sendEventMsg(
-                                    MsgEvent(
-                                        MSG_EVENT_DEVICE_TAKE_UPDATE,
-                                        DeviceTakeUpdateMsg(DEVICE_TYPE_LOCK, lockBean.rfid)
-                                    )
-                                )
                             }
+                        } else {
+                            sendEventMsg(
+                                MsgEvent(
+                                    MSG_EVENT_DEVICE_TAKE_UPDATE,
+                                    DeviceTakeUpdateMsg(DEVICE_TYPE_LOCK, lockBean.rfid)
+                                )
+                            )
                         }
                     }
+                }
 
-                    DOCK_TYPE_ELEC_LOCK_BOARD -> {
-                        // TODO 占位
-                    }
+                DOCK_TYPE_ELEC_LOCK_BOARD -> {
+                    // TODO 占位
+                }
 
-                    DOCK_TYPE_PORTABLE -> {
-                        // TODO 便携式待完善
-                        dockBean.deviceList.forEach { deviceBean ->
-                            if (deviceBean.isExist) {
-                                when (deviceBean.type) {
-                                    DEVICE_TYPE_KEY -> {
-                                        ModBusController.readKeyRfid(
+                DOCK_TYPE_PORTABLE -> {
+                    // TODO 便携式待完善
+                    dockBean.deviceList.forEach { deviceBean ->
+                        if (deviceBean.isExist) {
+                            when (deviceBean.type) {
+                                DEVICE_TYPE_KEY -> {
+                                    ModBusController.readKeyRfid(
+                                        dockBean.addr,
+                                        deviceBean.idx
+                                    ) { isLeft, res ->
+                                        if (res.size < 11) {
+                                            LogUtil.e("Key rfid error")
+                                            return@readKeyRfid
+                                        }
+                                        val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                            .removeLeadingZeros()
+                                        ModBusController.updateKeyRfid(
                                             dockBean.addr,
-                                            deviceBean.idx
-                                        ) { isLeft, res ->
-                                            if (res.size < 11) {
-                                                LogUtil.e("Key rfid error")
-                                                return@readKeyRfid
-                                            }
-                                            val rfid = res.copyOfRange(3, 11).toHexStrings(false)
-                                                .removeLeadingZeros()
-                                            ModBusController.updateKeyRfid(
+                                            true,
+                                            rfid
+                                        )
+                                        NetApi.getKeyInfo(rfid) {
+                                            ModBusController.updateKeyNewHardware(
                                                 dockBean.addr,
                                                 true,
-                                                rfid
+                                                it == null
                                             )
-                                            NetApi.getKeyInfo(rfid) {
-                                                if (it != null && !it.macAddress.isNullOrEmpty()) {
-                                                    ModBusController.updateKeyMac(
-                                                        dockBean.addr,
-                                                        isLeft,
-                                                        it.macAddress
-                                                    )
+                                            if (it != null && !it.macAddress.isNullOrEmpty()) {
+                                                ModBusController.updateKeyMac(
+                                                    dockBean.addr,
+                                                    isLeft,
+                                                    it.macAddress
+                                                )
 //                                                    showKeyReturnDialog(it.macAddress, isLeft, dockBean.addr)
-                                                } else {
-                                                    ToastUtils.tip(R.string.get_key_info_fail)
-                                                }
+                                            } else {
+                                                ToastUtils.tip(R.string.get_key_info_fail)
                                             }
-                                            // TODO 蓝牙通信
                                         }
-//                                        ModBusController.controlKeyBuckle(false, isLeft = true, dockBean.addr.toInt() - 1)
+                                        // TODO 蓝牙通信
                                     }
+                                }
 
-                                    DEVICE_TYPE_LOCK -> {
-                                        ModBusController.readLockRfid(
+                                DEVICE_TYPE_LOCK -> {
+                                    ModBusController.readLockRfid(
+                                        dockBean.addr,
+                                        deviceBean.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,
-                                            deviceBean.idx
-                                        ) { res ->
-                                            if (res.size < 11) {
-                                                LogUtil.e("Lock rfid error")
-                                                return@readLockRfid
-                                            }
-                                            val rfid = res.copyOfRange(3, 11).toHexStrings(false)
-                                                .removeLeadingZeros()
-                                            ModBusController.updateLockRfid(
+                                            deviceBean.idx,
+                                            rfid
+                                        )
+
+                                        NetApi.getLockInfo(rfid) {
+                                            ModBusController.updateLockNewHardware(
                                                 dockBean.addr,
                                                 deviceBean.idx,
-                                                rfid
+                                                it == null
                                             )
-
-                                            NetApi.getLockInfo(rfid) {
-                                                if (it != null) {
-                                                    // TODO 考虑快速拿取
-                                                    ModBusController.controlLockBuckle(
-                                                        false,
-                                                        dockBean.addr,
-                                                        deviceBean.idx
-                                                    ) { itRst ->
-                                                        if (itRst.isNotEmpty()) {
-                                                            // 上报锁具信息
-                                                            NetApi.updateLockReturn(
-                                                                rfid,
-                                                                MyApplication.instance!!.serialNo()
-                                                            ) {}
-                                                        }
+                                            if (it != null) {
+                                                // TODO 考虑快速拿取
+                                                ModBusController.controlLockBuckle(
+                                                    false,
+                                                    dockBean.addr,
+                                                    deviceBean.idx
+                                                ) { itRst ->
+                                                    if (itRst.isNotEmpty()) {
+                                                        // 上报锁具信息
+                                                        NetApi.updateLockReturn(
+                                                            rfid,
+                                                            MyApplication.instance!!.serialNo()
+                                                        ) {}
                                                     }
                                                 }
                                             }
                                         }
                                     }
+                                }
 
-                                    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")
+                                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")
                                     }
+                                }
 
-                                    DEVICE_TYPE_FINGERPRINT -> {
+                                DEVICE_TYPE_FINGERPRINT -> {
 
-                                    }
                                 }
                             }
                         }
                     }
                 }
-                Executor.delayOnMain(200) {
-                    listeners.forEach { it.callBack(dockBean) }
-                }
+            }
+            Executor.delayOnMain(200) {
+                listeners.forEach { it.callBack(dockBean) }
             }
         }
     }
 
+    /**
+     * 更新所有锁仓状态
+     */
+    fun updateAllBuckleStatus(done: () -> Unit) {
+        ModBusController.updateAllBuckleStatus(done)
+    }
+
     /**
      * 钥匙归还提示确认弹框,当前策略:作业票未完成禁止归还钥匙
      */
@@ -580,7 +617,7 @@ object BusinessManager {
             val msg =
                 MyApplication.instance!!.applicationContext.resources.getString(R.string.lock_is_not_enough)
             LogUtil.w(msg)
-            tipStr = msg
+            lockMap.clear()
         }
 
         var key: Pair<Byte, DockBean.KeyBean?>? = null
@@ -605,6 +642,12 @@ object BusinessManager {
         callBack.invoke(key, lockMap)
     }
 
+    /**
+     * 获取开关量数据
+     */
+    fun getSwitchData(): MutableList<DockBean.SwitchBean> {
+        return ModBusController.getSwitchData()
+    }
 
     /****************************************** 蓝牙 ******************************************/
     /******************************************蓝牙通用准备******************************************/
@@ -678,7 +721,7 @@ object BusinessManager {
         }
         isPreparing = true
         val listener = connectListeners[0]
-        if (ActivityUtils.currentActivity() == null) {
+        if (ActivityTracker.getCurrentActivity() == null) {
             LogUtil.w("Ignore connectKey : ${listener.mac} no current activity")
             isPreparing = false
             return
@@ -1406,6 +1449,9 @@ object BusinessManager {
         }
     }
 
+    /**
+     * 获取当前钥匙的状态
+     */
     fun getCurrentStatus(
         from: Int,
         bleDevice: BleDevice,
@@ -1567,8 +1613,11 @@ object BusinessManager {
                     data.taskCode?.toLong()!!,
                     keyNfc!!,
                     MyApplication.instance!!.serialNo()
-                ) { isSuccess ->
-                    if (!isSuccess) {
+                ) { isSuccess, msg ->
+                    if (!isSuccess && msg != MyApplication.instance?.applicationContext!!.getString(
+                            R.string.ticket_lost
+                        )
+                    ) {
                         SPUtils.saveUpdateKeyReturn(
                             MyApplication.instance!!,
                             UpdateKeyReturnBO(data.taskCode?.toLong()!!, keyNfc!!)
@@ -1604,8 +1653,15 @@ object BusinessManager {
      */
     fun handleVirtualKeyReturn(taskCode: Long, keyNfc: String, done: () -> Unit) {
         // 上报钥匙归还
-        NetApi.updateKeyReturn(taskCode, keyNfc, MyApplication.instance!!.serialNo()) { isSuccess ->
-            if (!isSuccess) {
+        NetApi.updateKeyReturn(
+            taskCode,
+            keyNfc,
+            MyApplication.instance!!.serialNo()
+        ) { isSuccess, msg ->
+            if (!isSuccess && msg != MyApplication.instance?.applicationContext!!.getString(
+                    R.string.ticket_lost
+                )
+            ) {
                 SPUtils.saveUpdateKeyReturn(
                     MyApplication.instance!!,
                     UpdateKeyReturnBO(taskCode, keyNfc)
@@ -1732,13 +1788,10 @@ object BusinessManager {
                                 if (isSuccess == false) {
                                     LogUtil.e("Lock take report fail")
                                     ToastUtils.tip(R.string.lock_take_report_fail)
-                                    if (mDeviceTakeList.any { it.deviceType == DEVICE_TYPE_LOCK && it.ticketId == info.ticketId }) {
-                                        sendLoadingEventMsg(
-                                            MyApplication.instance?.applicationContext?.getString(
-                                                R.string.take_out_lock_tip,
-                                                mDeviceTakeList.count { it.deviceType == DEVICE_TYPE_LOCK && it.ticketId == info.ticketId })
-                                        )
-                                    }
+                                    SPUtils.saveTicketTakeLockException(info.ticketId)
+                                    mDeviceTakeList.removeIf { it.deviceType == DEVICE_TYPE_LOCK && it.nfc == info.nfc }
+                                    mDeviceTakeList.removeIf { it.deviceType == DEVICE_TYPE_KEY &&  it.ticketId == info.ticketId }
+                                    sendLoadingEventMsg(null, false)
                                     return@runOnMain
                                 }
                                 // 检查是不是要发钥匙了
@@ -1757,6 +1810,10 @@ object BusinessManager {
                                     LogUtil.i("All locks are taken")
                                     sendLoadingEventMsg(null, false)
                                 }
+                                if (SPUtils.getTicketTakeLockException(info.ticketId)) {
+                                    ToastUtils.tip(R.string.current_ticket_report_lock_take_exception_tip)
+                                    return@runOnMain
+                                }
                                 // 检查有无当前工作票的钥匙
                                 mDeviceTakeList.find { it.deviceType == DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
                                     ?.let { itKey ->
@@ -1769,6 +1826,9 @@ object BusinessManager {
         }
     }
 
+    /**
+     * 分配钥匙
+     */
     private fun handleGiveKey(deviceTakeUpdateBO: DeviceTakeUpdateBO) {
         getCurrentStatus(
             2,
@@ -1812,6 +1872,9 @@ object BusinessManager {
         }
     }
 
+    /**
+     * 根据当前模式进行处理
+     */
     private fun handleCurrentMode(currentModeMsg: CurrentModeMsg) {
         when (currentModeMsg.mode) {
             // 工作模式
@@ -1910,9 +1973,12 @@ object BusinessManager {
         itemsToRemove.forEach { itData ->
             NetApi.updateKeyReturn(
                 itData.ticketId, itData.keyNfc, context.serialNo()
-            ) { isSuccess ->
+            ) { isSuccess, msg ->
                 count++
-                if (isSuccess) {
+                if (isSuccess || msg == MyApplication.instance?.applicationContext!!.getString(
+                        R.string.ticket_lost
+                    )
+                ) {
                     returnList.remove(itData)
                     getBleBeanByRfid(itData.keyNfc)?.bleDevice?.let {
                         switchReadyMode(it)

+ 4 - 1
app/src/main/java/com/grkj/iscs/MyApplication.kt

@@ -34,7 +34,10 @@ class MyApplication : Application() {
         NetHttpManager.getInstance().initCtx(this)
 
         BusinessManager.initMsgEventBus()
-        ArcSoftUtil.checkActiveStatus(this)
+        //todo 模拟器用不了
+//        ArcSoftUtil.checkActiveStatus(this)
+
+        SIKCore.init(this)
 
         SIKCore.init(this)
 

+ 10 - 5
app/src/main/java/com/grkj/iscs/extentions/Context.kt

@@ -26,13 +26,14 @@ fun Context.removeNetObserver(observer: Observer<Boolean>) {
 }
 
 @SuppressLint("MissingPermission")
-fun Context.serialNo() : String {
+fun Context.serialNo(): String {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
         && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
-        == PackageManager.PERMISSION_GRANTED) {
-        return Build.getSerial().toUpperCase(Locale.ROOT)
+        == PackageManager.PERMISSION_GRANTED
+    ) {
+        return Build.getSerial().uppercase(Locale.ROOT)
     }
-    return Build.SERIAL.toUpperCase(Locale.ROOT)
+    return Build.SERIAL.uppercase(Locale.ROOT)
 }
 
 /**
@@ -40,7 +41,11 @@ fun Context.serialNo() : String {
  */
 fun Context.checkPermissions(permissions: Array<String>): Boolean {
     for (permission in permissions) {
-        if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {
+        if (ContextCompat.checkSelfPermission(
+                this,
+                permission
+            ) != PackageManager.PERMISSION_GRANTED
+        ) {
             return false
         }
     }

+ 238 - 31
app/src/main/java/com/grkj/iscs/modbus/DockBean.kt

@@ -4,6 +4,8 @@ import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_CARD
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_FINGERPRINT
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_LOCK
+import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_SWITCH
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_COLLECT
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_ELEC_LOCK_BOARD
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
@@ -20,9 +22,12 @@ import com.grkj.iscs.util.log.LogUtil
 class DockBean(
     var addr: Byte,
     var type: Byte?,
+    var isWorking: Boolean = true,
     var deviceList: MutableList<DeviceBean>
 ) {
-
+    /**
+     * 转换状态
+     */
     fun parseStatus(byteArray: ByteArray): DockBean? {
         if (byteArray.isEmpty()) {
             return null
@@ -37,8 +42,16 @@ class DockBean(
                     val isRightCharging = (byteArray[3].toInt() shr 1) and 0x1 == 1
                     LogUtil.i("钥匙刷新状态 : $leftHasKey - $isLeftCharging - $rightHasKey - $isRightCharging")
                     if (getKeyList().isEmpty()) {
-                        deviceList.add(KeyBean(0, leftHasKey, true, isLeftCharging, null, null))
-                        deviceList.add(KeyBean(1, rightHasKey, false, isRightCharging, null, null))
+                        deviceList.add(
+                            KeyBean(
+                                0, leftHasKey, true, isLeftCharging, false, null, null
+                            )
+                        )
+                        deviceList.add(
+                            KeyBean(
+                                1, rightHasKey, false, isRightCharging, false, null, null
+                            )
+                        )
                         return null
                     }
 
@@ -76,7 +89,7 @@ class DockBean(
                         }
                     }
 
-                    return DockBean(addr, it, changeList)
+                    return DockBean(addr, it, true, changeList)
                 }
 
                 DOCK_TYPE_LOCK -> {
@@ -89,7 +102,7 @@ class DockBean(
 
                     if (getLockList().isEmpty()) {
                         for (i in 0 until tempList.size) {
-                            deviceList.add(LockBean(i, tempList[i], null))
+                            deviceList.add(LockBean(i, tempList[i], false, null))
                         }
                     }
 
@@ -108,7 +121,7 @@ class DockBean(
                     }
 
                     LogUtil.i("锁具刷新状态 : $changeList")
-                    return DockBean(addr, it, changeList)
+                    return DockBean(addr, it, true, changeList)
                 }
 
                 DOCK_TYPE_ELEC_LOCK_BOARD -> {
@@ -124,7 +137,7 @@ class DockBean(
                     }
                     if (getLockList().isEmpty()) {
                         for (i in 0 until tempList.size) {
-                            deviceList.add(LockBean(i, tempList[i], null))
+                            deviceList.add(LockBean(i, tempList[i], false, null))
                         }
                     }
 
@@ -132,7 +145,11 @@ class DockBean(
                     val isKeyCharging = (byteArray[3].toInt() shr 1) and 0x1 == 1
 
                     if (getKeyList().isEmpty()) {
-                        deviceList.add(KeyBean(4, isKeyExist, true, isKeyCharging, null, null))
+                        deviceList.add(
+                            KeyBean(
+                                4, isKeyExist, true, isKeyCharging, false, null, null
+                            )
+                        )
                     }
 
                     val isCardExist = (byteArray[3].toInt() shr 4) and 0x1 == 1
@@ -183,7 +200,13 @@ class DockBean(
                         changeList.add(getFingerPrintList()[0])
                     }
                     LogUtil.i("便携式刷新状态 : $changeList")
-                    return DockBean(addr, it, changeList)
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                DOCK_TYPE_COLLECT -> {
+                    val working = (byteArray[4].toInt() shr 0) and 0x1 == 1
+                    LogUtil.i("开关量采集板是否工作 : $working")
+                    return DockBean(addr, it, working, mutableListOf())
                 }
 
                 else -> return null
@@ -191,41 +214,210 @@ class DockBean(
         } ?: return null
     }
 
+    /**
+     * 转换锁仓状态
+     */
+    fun parseLockStatus(byteArray: ByteArray): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        type?.let {
+            // 因为都是一个寄存器返回的,所以一定能得到2个钥匙的状态或者10把锁具的状态
+            when (it) {
+                DOCK_TYPE_KEY -> {
+                    val leftKeyLockEnabled = (byteArray[4].toInt() shr 0) and 0x1 == 1
+                    val rightKeyLockEnabled = (byteArray[4].toInt() shr 4) and 0x1 == 1
+                    LogUtil.i("钥匙锁仓状态 : $leftKeyLockEnabled - $rightKeyLockEnabled")
+                    if (getKeyList().isEmpty()) {
+                        return null
+                    }
+                    val changeList = mutableListOf<DeviceBean>()
+                    getKeyList().forEach { keyBean ->
+                        if (keyBean.isLeft) {
+                            keyBean.lockEnabled = leftKeyLockEnabled
+                        } else {
+                            keyBean.lockEnabled = rightKeyLockEnabled
+                        }
+                    }
+
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                DOCK_TYPE_LOCK -> {
+                    val tempList = mutableListOf<Boolean>()
+                    for (i in 0..7) {
+                        tempList.add((byteArray[4].toInt() shr i) and 0x1 == 1)
+                    }
+
+                    if (getLockList().isEmpty()) {
+                        return null
+                    }
+
+                    val changeList = mutableListOf<DeviceBean>()
+                    for (i in 0 until 8) {
+                        getLockList()[i].lockEnabled = tempList[i]
+                    }
+
+                    LogUtil.i("锁具刷新状态 : $changeList")
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                DOCK_TYPE_ELEC_LOCK_BOARD -> {
+                    val tempList = mutableListOf<Boolean>()
+                    for (i in 0..2) {
+                        tempList.add((byteArray[4].toInt() shr i) and 0x1 == 1)
+                    }
+                    if (getLockList().isEmpty()) {
+                        for (i in 0 until tempList.size) {
+                            deviceList.add(LockBean(i, tempList[i], tempList[i], null))
+                        }
+                        return null
+                    }
+
+                    val changeList = mutableListOf<DeviceBean>()
+                    for (i in 0 until getLockList().size) {
+                        getLockList()[i].lockEnabled = tempList[i]
+                    }
+
+                    LogUtil.i("电磁锁具刷新状态 : $changeList")
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                DOCK_TYPE_PORTABLE -> {
+                    // TODO 便携式底座更新
+                    val tempList = mutableListOf<Boolean>()
+                    for (i in 0..2) {
+                        tempList.add((byteArray[4].toInt() shr i) and 0x1 == 1)
+                    }
+                    val keyLockEnabled = (byteArray[4].toInt() shr 4) and 0x1 == 1
+
+                    val changeList = mutableListOf<DeviceBean>()
+                    // 锁具变化
+                    for (i in 0 until 3) {
+                        getLockList()[i].lockEnabled = tempList[i]
+                    }
+                    // 蓝牙钥匙变化
+                    if (getKeyList().isNotEmpty()) {
+                        getKeyList()[0].lockEnabled = keyLockEnabled
+                    }
+                    LogUtil.i("便携式刷新状态 : $changeList")
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                DOCK_TYPE_COLLECT -> {
+                    for (i in 0..7) {
+                        if ((byteArray[4].toInt() shr i) and 0x1 == 1) {
+                            val switchBoardAddr = byteArrayOf(0x00, (0x20 + i).toByte())
+                            ModBusController.readSwitchStatus(addr, switchBoardAddr) { res ->
+                                val switchStatus: MutableList<Boolean> = mutableListOf()
+                                for (switchIdx in 0..7) {
+                                    switchStatus.add((res[4].toInt() shr switchIdx) and 0x1 == 1)
+                                }
+                                for (switchIdx in 0..7) {
+                                    switchStatus.add((res[3].toInt() shr switchIdx) and 0x1 == 1)
+                                }
+                                for (idx in 0 until switchStatus.size) {
+                                    deviceList.filterIsInstance<SwitchBean>()
+                                        .find { it.switchBoardAddr == switchBoardAddr[1] && it.idx == idx }
+                                        ?.let {
+                                            it.enabled =
+                                                switchStatus[idx]
+                                        } ?: run {
+                                        deviceList.add(
+                                            SwitchBean(
+                                                idx,
+                                                switchBoardAddr[1],
+                                                switchStatus[idx]
+                                            )
+                                        )
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    return DockBean(addr, type, isWorking, deviceList)
+                }
+
+                else -> return null
+            }
+        } ?: return null
+    }
+
+    /**
+     * 获取额外的9,10锁仓数据
+     */
+    fun parseExtraLockStatus(byteArray: ByteArray): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        type?.let {
+            // 因为都是一个寄存器返回的,所以一定能得到2个钥匙的状态或者10把锁具的状态
+            when (it) {
+
+                DOCK_TYPE_LOCK -> {
+                    val tempList = mutableListOf<Boolean>()
+                    for (i in 0..1) {
+                        tempList.add((byteArray[4].toInt() shr i) and 0x1 == 1)
+                    }
+                    if (getLockList().isEmpty()) {
+                        return null
+                    }
+                    val changeList = mutableListOf<DeviceBean>()
+                    for (i in 0..1) {
+                        getLockList()[getLockList().size - 2 + i].lockEnabled = tempList[i]
+                    }
+
+                    LogUtil.i("锁具刷新状态 : $changeList")
+                    return DockBean(addr, it, true, changeList)
+                }
+
+                else -> return null
+            }
+        } ?: return null
+    }
+
+    /**
+     * 获取钥匙列表
+     */
     fun getKeyList(): MutableList<KeyBean> {
         return deviceList.filterIsInstance<KeyBean>().filter { it.type == DEVICE_TYPE_KEY }
             .toMutableList()
     }
 
+    /**
+     * 获取锁列表
+     */
     fun getLockList(): MutableList<LockBean> {
         return deviceList.filterIsInstance<LockBean>().filter { it.type == DEVICE_TYPE_LOCK }
             .toMutableList()
     }
 
+    /**
+     * 获取工卡列表
+     */
     fun getCardList(): MutableList<CardBean> {
         return deviceList.filterIsInstance<CardBean>().filter { it.type == DEVICE_TYPE_CARD }
             .toMutableList()
     }
 
+    /**
+     * 获取指纹列表
+     */
     fun getFingerPrintList(): MutableList<FingerPrintBean> {
         return deviceList.filterIsInstance<FingerPrintBean>()
             .filter { it.type == DEVICE_TYPE_FINGERPRINT }.toMutableList()
     }
 
-    fun getBit(by: Byte): String {
-        val sb = StringBuffer()
-        sb.append((by.toInt() shr 7) and 0x1)
-            .append((by.toInt() shr 6) and 0x1)
-            .append((by.toInt() shr 5) and 0x1)
-            .append((by.toInt() shr 4) and 0x1)
-            .append((by.toInt() shr 3) and 0x1)
-            .append((by.toInt() shr 2) and 0x1)
-            .append((by.toInt() shr 1) and 0x1)
-            .append((by.toInt() shr 0) and 0x1)
-        return sb.toString()
+    /**
+     * 获取开关列表
+     */
+    fun getSwitchList(): MutableList<SwitchBean> {
+        return deviceList.filterIsInstance<SwitchBean>().filter { it.type == DEVICE_TYPE_SWITCH }
+            .toMutableList()
     }
 
     override fun toString(): String {
-        return "DockBean(addr=$addr, type=$type, deviceList=$deviceList)"
+        return "DockBean(addr=$addr, type=$type, isWorking=$isWorking, deviceList=$deviceList)"
     }
 
 
@@ -239,7 +431,9 @@ class DockBean(
     sealed class DeviceBean(
         var type: Int,
         var idx: Int,
-        var isExist: Boolean
+        var isExist: Boolean,
+        var lockEnabled: Boolean = false,
+        var newHardware: Boolean = false
     ) {
         override fun toString(): String {
             return "DeviceBean(type=$type, idx=$idx, isExist=$isExist)"
@@ -255,16 +449,17 @@ class DockBean(
         isExist: Boolean,
         var isLeft: Boolean,
         var isCharging: Boolean,
+        lockEnabled: Boolean = false,
         var rfid: String?,
         var mac: String?,
         var isReady: Boolean = false    // 钥匙是否准备好(连接上且为待机模式)
-    ) : DeviceBean(DEVICE_TYPE_KEY, idx, isExist) {
+    ) : DeviceBean(DEVICE_TYPE_KEY, idx, isExist, lockEnabled) {
         override fun toString(): String {
             return "KeyBean(isLeft=$isLeft, isCharging=$isCharging, rfid=$rfid, mac=$mac, isReady=$isReady, idx=$idx, isExist=$isExist)"
         }
 
         fun clone(): KeyBean {
-            return KeyBean(idx, isExist, isLeft, isCharging, rfid, mac, isReady)
+            return KeyBean(idx, isExist, isLeft, isCharging, lockEnabled, rfid, mac, isReady)
         }
     }
 
@@ -276,14 +471,15 @@ class DockBean(
     class LockBean(
         idx: Int,
         isExist: Boolean,
-        var rfid: String?
-    ) : DeviceBean(DEVICE_TYPE_LOCK, idx, isExist) {
+        lockEnabled: Boolean = false,
+        var rfid: String?,
+    ) : DeviceBean(DEVICE_TYPE_LOCK, idx, isExist, lockEnabled) {
         override fun toString(): String {
             return "LockBean(rfid=$rfid, idx=$idx, isExist=$isExist)"
         }
 
         fun clone(): LockBean {
-            return LockBean(idx, isExist, rfid)
+            return LockBean(idx, isExist, lockEnabled, rfid)
         }
     }
 
@@ -292,14 +488,25 @@ class DockBean(
      */
     class CardBean(
         idx: Int,
-        isExist: Boolean
-    ) : DeviceBean(DEVICE_TYPE_CARD, idx, isExist)
+        isExist: Boolean,
+        lockEnabled: Boolean = false,
+    ) : DeviceBean(DEVICE_TYPE_CARD, idx, isExist, lockEnabled)
 
     /**
      * 指纹
      */
     class FingerPrintBean(
         idx: Int,
-        isExist: Boolean
-    ) : DeviceBean(DEVICE_TYPE_FINGERPRINT, idx, isExist)
+        isExist: Boolean,
+        lockEnabled: Boolean = false,
+    ) : DeviceBean(DEVICE_TYPE_FINGERPRINT, idx, isExist, lockEnabled)
+
+    /**
+     * 开关
+     */
+    class SwitchBean(
+        idx: Int,
+        val switchBoardAddr: Byte,
+        var enabled: Boolean
+    ) : DeviceBean(DEVICE_TYPE_SWITCH, idx, true)
 }

+ 64 - 0
app/src/main/java/com/grkj/iscs/modbus/FrameTask.kt

@@ -0,0 +1,64 @@
+package com.grkj.iscs.modbus
+
+import com.grkj.iscs.extentions.crc16
+
+class FrameTask(
+    val req: ByteArray,
+    val done: ((res: ByteArray) -> Unit)?    // 响应回调
+) {
+
+    /**
+     * 是否允许重发
+     */
+    var allowRetransmission: Boolean = true
+
+    /**
+     * 上次发送时间
+     */
+    var lastSent: Long = 0
+
+    /**
+     * 已发送次数
+     */
+    var sentCount = 0
+
+    /**
+     * 最小发送间隔
+     */
+    var minSendInterval: Int = MODBUS_MIN_SEND_INTERVAL
+
+
+    fun waitIfNecessary() {
+        val interval = System.nanoTime() - lastSent
+        if (interval < minSendInterval) {
+            Thread.sleep((minSendInterval - interval) / 1000_000)
+        }
+    }
+
+    fun shouldSend(): Boolean {
+        return if (allowRetransmission) {
+            sentCount < 3
+        } else {
+            sentCount < 1
+        }
+    }
+
+    fun afterSent() {
+        sentCount++
+        lastSent = System.nanoTime()
+    }
+
+    /**
+     * 判断 res 是否是 frame 的响应
+     */
+    fun match(res: ByteArray): Boolean {
+        // 从机地址 和 功能码 必须相同
+        if (res.size < 5 || req[0] != res[0] || req[1] != res[1]) {
+            return false
+        }
+        // 报文2 的 CRC校验得正确
+        val crc16 = res.crc16(0, res.size - 2)
+        return crc16[0] == res[res.size - 2] && crc16[1] == res[res.size - 1]
+    }
+
+}

+ 94 - 0
app/src/main/java/com/grkj/iscs/modbus/MBFrame.kt

@@ -0,0 +1,94 @@
+package com.grkj.iscs.modbus
+
+import com.grkj.iscs.extentions.crc16
+
+/**
+ * ModBus 数据帧
+ */
+class MBFrame(
+    // 类型
+    val type: Byte,
+    // 数据域:D1 和 D2
+    val data: ByteArray
+
+) {
+
+    /**
+     * @param index 从机序号
+     */
+    fun compile(index: Int): ByteArray {
+        val bytes = ByteArray(4 + data.size)
+        // TODO 从机开始地址0x01
+//        bytes[0] = (0x80 + index).toByte()
+        bytes[0] = (1 + index).toByte()
+        bytes[1] = type
+        for (i in data.indices) {
+            bytes[2 + i] = data[i]
+        }
+        val crc16 = bytes.crc16(0, bytes.size - 2)
+        bytes[bytes.size - 2] = crc16[0]
+        bytes[bytes.size - 1] = crc16[1]
+        return bytes
+
+//        val cmd = byteArrayOf((1 + index).toByte()) + byteArrayOf(type) + data
+//        return cmd + cmd.crc16()
+    }
+
+    /**
+     * @param address 从机地址
+     */
+    fun compile(address: Byte): ByteArray {
+        val bytes = ByteArray(4 + data.size)
+        bytes[0] = address
+        bytes[1] = type
+        for (i in data.indices) {
+            bytes[2 + i] = data[i]
+        }
+        val crc16 = bytes.crc16(0, bytes.size - 2)
+        bytes[bytes.size - 2] = crc16[0]
+        bytes[bytes.size - 1] = crc16[1]
+        return bytes
+    }
+
+    companion object {
+        /**
+         * 读取设备类型
+         */
+        val READ_DEVICE_TYPE = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x00, 0x00, 0x01)
+        )
+
+        /**
+         * 读钥匙/锁具/便携式底座/开关量采集板状态
+         */
+        val READ_STATUS = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x10, 0x00, 0x01)
+        )
+
+        /**
+         * 读卡扣状态(钥匙、锁的0-7、便携式的锁和钥匙)
+         */
+        val READ_BUCKLE_STATUS = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x11, 0x00, 0x01)
+        )
+
+        /**
+         * 读卡扣状态(锁的9、10)
+         */
+        val READ_LOCK_BUCKLE_EXTRA_STATUS = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x12, 0x00, 0x01)
+        )
+
+        /**
+         * 读取电子锁控灯光状态
+         */
+        val READ_ELE_LOCK_LIGHT_STATUS = MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, 0x15, 0x00, 0x01)
+        )
+    }
+}

+ 142 - 0
app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt

@@ -0,0 +1,142 @@
+package com.grkj.iscs.modbus
+
+/**
+ * 通信指令帮助工具
+ */
+object ModBusCMDHelper {
+    /**
+     * 生成锁具/便携式底座 单个锁具卡扣开关指令
+     */
+    fun generateLockBuckleCmd(isOpen: Boolean, lockIndex: Int): MBFrame {
+        var str = ""
+        val idx = lockIndex - (lockIndex / 8) * 8
+        for (i in 7 downTo 0) {
+            str += if (i == idx) {
+                "1"
+            } else {
+                "0"
+            }
+        }
+
+        // 第三位是 是否响应,第四位是操作哪个,操作默认全是0或者1,使用第三位响应来进行操作
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(
+                0x00,
+                if (lockIndex in 0..7) 0x11 else 0x12,
+                str.toInt(2).toByte(),
+                if (isOpen) 0x00 else 0xFF.toByte()
+            )
+        )
+    }
+
+    /**
+     * 生成多个锁具卡扣开关指令(只能针对同一个底座,不能跨底座)
+     */
+    fun generateLockBuckleCmd(
+        isOpen: Boolean,
+        lockIndexList: MutableList<Int>
+    ): MutableList<MBFrame> {
+        var str = ""
+        val rstList = mutableListOf<MBFrame>()
+        val smallIdxList = lockIndexList.filter { it in 0..7 }
+        val bigIdxList = lockIndexList.filter { it in 8..15 }
+        if (smallIdxList.isNotEmpty()) {
+            for (i in 7 downTo 0) {
+                str += if (smallIdxList.contains(i)) {
+                    "1"
+                } else {
+                    "0"
+                }
+            }
+            rstList.add(
+                MBFrame(
+                    FRAME_TYPE_WRITE,
+                    byteArrayOf(
+                        0x00,
+                        0x11,
+                        str.toInt(2).toByte(),
+                        if (isOpen) 0x00 else 0xFF.toByte()
+                    )
+                )
+            )
+        }
+        if (bigIdxList.isNotEmpty()) {
+            str = ""
+            for (i in 7 downTo 0) {
+                str += if (bigIdxList.contains(i + 8)) {
+                    "1"
+                } else {
+                    "0"
+                }
+            }
+            rstList.add(
+                MBFrame(
+                    FRAME_TYPE_WRITE,
+                    byteArrayOf(
+                        0x00,
+                        0x12,
+                        str.toInt(2).toByte(),
+                        if (isOpen) 0x00 else 0xFF.toByte()
+                    )
+                )
+            )
+        }
+        return rstList
+    }
+
+
+    /**
+     * 生成钥匙底座灯光指令
+     *
+     * @param leftAction、rightAction 0:保持当前状态 1:点亮 2:熄灭 默认0
+     */
+    @Deprecated("已取消")
+    fun generateKeyLightCmd(leftAction: Int = 0, rightAction: Int = 0): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            // TODO 第三个是高位,第四个是低位
+            byteArrayOf(0x00, 0x15, rightAction.toByte(), leftAction.toByte())
+        )
+    }
+
+    /**
+     * 操作钥匙/便携式底座钥匙卡扣,一次只操作一个卡扣
+     *
+     * @param isOpen true:开操作 false:关操作
+     * @param index 0:左 1:右 便携式底座钥匙传0
+     */
+    fun generateKeyBuckleCmd(isOpen: Boolean, index: Int): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(
+                0x00,
+                0x11,
+                if (index == 1) 0b00010000.toByte() else 0b0000001,
+                if (isOpen) 0x00 else 0xFF.toByte()
+            )
+        )
+    }
+
+    /**
+     * 生成RFID 读指令
+     *
+     * @param idx 锁具底座锁具:0-9  钥匙底座钥匙:0(left)-1(right)  便携式底座锁具:0-2  便携式底座钥匙:4   便携式底座卡:8
+     */
+    fun generateRfidCmd(idx: Int): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(0x00, (0x20 + idx * 4).toByte(), 0x00, 0x04)
+        )
+    }
+
+    /**
+     * 生成读取开关采集板的指令
+     */
+    fun generateSwitchBoardStatusCmd(addr: ByteArray): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_READ,
+            byteArrayOf(addr[0], addr[1], 0x00, 0x01)
+        )
+    }
+}

+ 13 - 0
app/src/main/java/com/grkj/iscs/modbus/ModBusConstants.kt

@@ -0,0 +1,13 @@
+package com.grkj.iscs.modbus
+
+/**
+ * modbus 最小发送间隔(150豪秒)
+ */
+// TODO 超时的可能也是用的这个,看情况是否增加到500
+const val MODBUS_MIN_SEND_INTERVAL = 500_000_000
+
+const val FRAME_TYPE_READ: Byte = 0x03
+const val FRAME_TYPE_WRITE: Byte = 0x06
+const val FRAME_TYPE_SCANNER: Byte = 0x43
+const val FRAME_TYPE_WRITE_MULTI: Byte = 0x10
+const val FRAME_TYPE_WRITE_FILE: Byte = 0x15

+ 333 - 72
app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt

@@ -3,28 +3,37 @@ package com.grkj.iscs.modbus
 import android.content.Context
 import com.clj.fastble.BleManager
 import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.BusinessManager.CAN_RETURN
 import com.grkj.iscs.R
+import com.grkj.iscs.extentions.crc16
 import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.toHexStrings
+import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_CARD
+import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_FINGERPRINT
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_LOCK
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_COLLECT
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_ELEC_LOCK_BOARD
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_PORTABLE
+import com.grkj.iscs.util.CommonUtils
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
-import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicInteger
 import java.util.stream.Collectors
+import kotlin.random.Random
 
 
 /**
  * ModBus 主控板控制器
  */
 object ModBusController {
-
+    /**
+     * 是否初始化完成
+     */
     var isInitReady = false
 
     /**
@@ -32,43 +41,50 @@ object ModBusController {
      */
     var dockList: MutableList<DockBean> = mutableListOf()
 
+    /**
+     * 监听状态类型
+     */
     private const val LISTENER_TYPE_STATUS = 3
 
     // 主控板管理器
     var modBusManager: ModBusManager? = null
-    private var slaveCount: Int = 0
 
+    /**
+     * 监听器
+     */
     private val listeners = ArrayList<StatusListener>()
 
     // 是否中断读取状态
     private var interruptReadStatus: ArrayList<Boolean> = ArrayList()
 
+    /**
+     * 重复时间
+     */
     // TODO 临时改成5s
     const val REPEAT_FREQUENCY = 800L
 
-    class StatusListener(
-        val key: Any,
-        val listener: (Any) -> Unit,
-        val type: Int
-    )
-
+    /**
+     * 中断读取
+     */
     fun interruptReadTrashBinStatus(interrupt: Boolean) {
         interruptReadStatus.clear()
         interruptReadStatus.add(interrupt)
     }
 
+    /**
+     * 启动通信引擎
+     */
     @ExperimentalUnsignedTypes
     fun start(ctx: Context) {
         modBusManager?.stop()
         PortManager.openCtrlBord(ctx)
             ?.let { pm ->
-                return@let ModBusManager(slaveCount, pm, true)
+                return@let ModBusManager(pm, true)
             }
             // 间隔 1 秒读一遍桶的状态
             ?.repeatSendToAll(MBFrame.READ_STATUS, {
                 interruptReadStatus
             }, { res ->
-//                // Logger.d("ModbusController", "res: ${res.map { it.toHexString() }}")
                 LogUtil.i("****************************************************************************")
                 // 过滤非空的数据,重置slaveCount
                 // 不再使用slaveCount,改用地址池
@@ -84,10 +100,16 @@ object ModBusController {
             ?.start()
     }
 
+    /**
+     * 注册监听器
+     */
     fun registerStatusListener(key: Any, listener: (Any) -> Unit) {
         listeners.add(StatusListener(key, listener, LISTENER_TYPE_STATUS))
     }
 
+    /**
+     * 取消注册监听器
+     */
     fun unregisterListener(key: Any) {
         val it = listeners.iterator()
         while (it.hasNext()) {
@@ -97,10 +119,16 @@ object ModBusController {
         }
     }
 
+    /**
+     * 停止引擎
+     */
     fun stop() {
         modBusManager?.stop()
     }
 
+    /**
+     * 引擎是否运行中
+     */
     fun isRunning(): Boolean? {
         return modBusManager?.isRunning()
     }
@@ -121,6 +149,7 @@ object ModBusController {
                     DOCK_TYPE_LOCK -> "锁具底座"
                     DOCK_TYPE_ELEC_LOCK_BOARD -> "电磁锁控制板"
                     DOCK_TYPE_PORTABLE -> "便携式底座"
+                    DOCK_TYPE_COLLECT -> "开关量采集板"
                     else -> "未知"
                 }
                 LogUtil.i("initDevicesStatus 设备(${bytes[0].toInt()})类型:$type")
@@ -159,6 +188,9 @@ object ModBusController {
                         val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
                         LogUtil.i("初始化锁具 RFID : $rfid")
                         updateLockRfid(dockBean.addr, idx, rfid)
+                        NetApi.getLockInfo(rfid) {
+                            updateLockNewHardware(dockBean.addr, idx, it == null)
+                        }
                     }
                 }
                 controlLockBuckle(false, dockBean.addr, hasLockIdxList)
@@ -189,6 +221,7 @@ object ModBusController {
                             // 蓝牙准备操作
                             NetApi.getKeyInfo(rfid) {
                                 LogUtil.i("getKeyInfo : $rfid - ${it?.macAddress}")
+                                updateKeyNewHardware(dockBean.addr, isLeft, it == null)
                                 if (it != null && !it.macAddress.isNullOrEmpty()) {
                                     // 更新mac
                                     updateKeyMac(dockBean.addr, key.isLeft, it.macAddress)
@@ -216,7 +249,7 @@ object ModBusController {
     }
 
     /**
-     * 新状态
+     * 获取最新状态
      */
     fun updateStatus(byteArray: ByteArray): DockBean? {
         if (byteArray.isEmpty()) {
@@ -226,6 +259,184 @@ object ModBusController {
         return dockB?.parseStatus(byteArray)
     }
 
+    /**
+     * 更新所有锁仓状态
+     */
+    fun updateAllBuckleStatus(done: () -> Unit) {
+        val remaining = AtomicInteger(2)
+        modBusManager?.sendToAll(MBFrame.READ_BUCKLE_STATUS) { res ->
+            LogUtil.i("****************************************************************************")
+            // 过滤非空的数据,重置slaveCount
+            // 不再使用slaveCount,改用地址池
+            lockBuckleStatus(res)
+            // 每完成一个就把计数减一,减到 0 就触发 done()
+            if (remaining.decrementAndGet() == 0) {
+                done()
+            }
+        }
+        modBusManager?.sendToAll(MBFrame.READ_LOCK_BUCKLE_EXTRA_STATUS) { res ->
+            LogUtil.i("****************************************************************************")
+            // 过滤非空的数据,重置slaveCount
+            // 不再使用slaveCount,改用地址池
+            lockBuckleExtraStatus(res)
+            // 每完成一个就把计数减一,减到 0 就触发 done()
+            if (remaining.decrementAndGet() == 0) {
+                done()
+            }
+        }
+    }
+
+    /**
+     * 第9,10锁位卡扣状态
+     */
+    private fun lockBuckleExtraStatus(res: Any) {
+        LogUtil.i("硬件状态:${(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>()
+
+            modBusManager?.mSlaveAddressList?.forEach { itDock ->
+                if (res.none { it.isNotEmpty() && it[0] == itDock }) {
+                    addressList.add("0x${String.format("%02X", itDock)}")
+                }
+            }
+            tipStr += addressList
+            ToastUtils.tip(tipStr)
+        }
+        res.forEach { bytes ->
+            val dockBean = updateExtraLockStatus(bytes) ?: return@forEach
+            if (!CAN_RETURN) {
+                return@forEach
+            }
+            when (dockBean.type) {
+                DOCK_TYPE_LOCK -> {
+                    dockBean.getLockList().filter { it.idx > 7 }.forEach { lockBean ->
+                        updateLockStatus(
+                            dockBean.addr,
+                            lockBean.idx,
+                            lockBean.lockEnabled
+                        )
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 第1-8锁位卡扣状态和钥匙
+     */
+    private fun lockBuckleStatus(res: Any) {
+        LogUtil.i("硬件状态:${(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>()
+
+            modBusManager?.mSlaveAddressList?.forEach { itDock ->
+                if (res.none { it.isNotEmpty() && it[0] == itDock }) {
+                    addressList.add("0x${String.format("%02X", itDock)}")
+                }
+            }
+            tipStr += addressList
+            ToastUtils.tip(tipStr)
+        }
+
+        res.forEach { bytes ->
+            val dockBean = updateLockStatus(bytes) ?: return@forEach
+            if (!CAN_RETURN) {
+                return@forEach
+            }
+            when (dockBean.type) {
+                DOCK_TYPE_KEY -> {
+                    dockBean.getKeyList().forEach { keyBean ->
+                        updateKeyLockStatus(
+                            dockBean.addr,
+                            keyBean.isLeft,
+                            keyBean.lockEnabled
+                        )
+                        //todo 更新锁仓状态
+                    }
+                }
+
+                DOCK_TYPE_LOCK -> {
+                    dockBean.getLockList().forEach { lockBean ->
+                        updateLockStatus(
+                            dockBean.addr,
+                            lockBean.idx,
+                            lockBean.lockEnabled
+                        )
+                    }
+                }
+
+                DOCK_TYPE_ELEC_LOCK_BOARD -> {
+                    // TODO 占位
+                }
+
+                DOCK_TYPE_PORTABLE -> {
+                    // TODO 便携式待完善
+                    dockBean.deviceList.forEach { deviceBean ->
+                        if (deviceBean.isExist) {
+                            when (deviceBean.type) {
+                                DEVICE_TYPE_KEY -> {
+                                    updateKeyLockStatus(
+                                        dockBean.addr,
+                                        true,
+                                        deviceBean.lockEnabled
+                                    )
+                                }
+
+                                DEVICE_TYPE_LOCK -> {
+                                    updateLockStatus(
+                                        dockBean.addr,
+                                        deviceBean.idx,
+                                        deviceBean.lockEnabled
+                                    )
+                                }
+
+                                DEVICE_TYPE_CARD -> {
+                                    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")
+                                    }
+                                }
+
+                                DEVICE_TYPE_FINGERPRINT -> {
+
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 获取第1-8的锁仓数据和左右钥匙数据
+     */
+    private fun updateLockStatus(byteArray: ByteArray): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        val dockB = dockList.find { it.addr == byteArray[0] }
+        return dockB?.parseLockStatus(byteArray)
+    }
+
+    /**
+     * 获取额外的9,10锁仓数据
+     */
+    private fun updateExtraLockStatus(byteArray: ByteArray): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        val dockB = dockList.find { it.addr == byteArray[0] && it.type == DOCK_TYPE_LOCK }
+        return dockB?.parseExtraLockStatus(byteArray)
+    }
+
     /**
      * 读取设备类型
      */
@@ -243,7 +454,7 @@ object ModBusController {
         dock?.let {
             it.type = type
         } ?: let {
-            dockList.add(DockBean(idx, type, mutableListOf()))
+            dockList.add(DockBean(idx, type, true, mutableListOf()))
         }
     }
 
@@ -272,7 +483,7 @@ object ModBusController {
     }
 
     /**
-     * 开/关锁具卡扣
+     * 开/关锁具卡扣
      */
     fun controlLockBuckle(
         isOpen: Boolean,
@@ -281,7 +492,7 @@ object ModBusController {
         done: ((res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
-            modBusManager?.generateLockBuckleCmd(isOpen, lockIdx)?.let { cmd ->
+            ModBusCMDHelper.generateLockBuckleCmd(isOpen, lockIdx)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) { res ->
                     done?.invoke(res)
                 }
@@ -289,6 +500,9 @@ object ModBusController {
         }
     }
 
+    /**
+     * 开/关锁具卡扣 多
+     */
     fun controlLockBuckle(
         isOpen: Boolean,
         slaveAddress: Byte?,
@@ -296,7 +510,7 @@ object ModBusController {
         done: ((res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
-            modBusManager?.generateLockBuckleCmd(isOpen, lockIdxList)?.let { cmdList ->
+            ModBusCMDHelper.generateLockBuckleCmd(isOpen, lockIdxList)?.let { cmdList ->
                 cmdList.forEach { cmd ->
                     modBusManager?.sendTo(it, cmd) { res ->
                         done?.invoke(res)
@@ -315,7 +529,7 @@ object ModBusController {
         done: ((isLeft: Boolean, res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
-            modBusManager?.generateRfidCmd(idx)?.let { cmd ->
+            ModBusCMDHelper.generateRfidCmd(idx)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) {
                     done?.invoke(idx == 0, it)
                 }
@@ -328,7 +542,7 @@ object ModBusController {
      */
     fun readLockRfid(slaveAddress: Byte?, lockIdx: Int, done: ((res: ByteArray) -> Unit)? = null) {
         slaveAddress?.let {
-            modBusManager?.generateRfidCmd(lockIdx)?.let { cmd ->
+            ModBusCMDHelper.generateRfidCmd(lockIdx)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) { res ->
                     done?.invoke(res)
                 }
@@ -341,7 +555,7 @@ object ModBusController {
      */
     fun readPortalCaseCardRfid(slaveAddress: Byte?, done: ((res: ByteArray) -> Unit)? = null) {
         slaveAddress?.let {
-            modBusManager?.generateRfidCmd(8)?.let { cmd ->
+            ModBusCMDHelper.generateRfidCmd(8)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) { res ->
                     done?.invoke(res)
                 }
@@ -357,6 +571,14 @@ object ModBusController {
             ?.find { it.isLeft == isLeft }?.rfid = rfid
     }
 
+    /**
+     * 更新钥匙锁仓状态
+     */
+    private fun updateKeyLockStatus(slaveAddress: Byte, isLeft: Boolean, lockEnabled: Boolean) {
+        dockList.find { it.addr == slaveAddress }?.getKeyList()
+            ?.find { it.isLeft == isLeft }?.lockEnabled = lockEnabled
+    }
+
     /**
      * 更新钥匙MAC
      */
@@ -366,11 +588,11 @@ object ModBusController {
     }
 
     /**
-     * 通过RFID更新对应的Mac
+     * 更新钥匙时候为新设备
      */
-    fun updateKeyMacByRfid(rfid: String, mac: String) {
-        dockList.find { it.type == DOCK_TYPE_KEY }?.getKeyList()?.find { it.rfid == rfid }?.mac =
-            mac
+    fun updateKeyNewHardware(slaveAddress: Byte, isLeft: Boolean, newHardware: Boolean) {
+        dockList.find { it.addr == slaveAddress }?.getKeyList()
+            ?.find { it.isLeft == isLeft }?.newHardware = newHardware
     }
 
     /**
@@ -382,46 +604,19 @@ object ModBusController {
     }
 
     /**
-     * 设备是否存在,加入deviceType防止有重复的但是不同类型的
-     *
-     * @param deviceType {@link [com.grkj.iscs.model.bo.DeviceTakeUpdateBO]<class>#[deviceType]}
+     * 更新锁是不是新设备,就是后台没有数据的
      */
-    fun isDeviceExist(rfid: String, deviceType: Int): Boolean {
-        return when (deviceType) {
-            DEVICE_TYPE_KEY -> {
-                dockList.find { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
-                    ?.getKeyList()?.find { it.rfid == rfid } != null
-            }
-
-            DEVICE_TYPE_LOCK -> {
-                dockList.find { it.type == DOCK_TYPE_LOCK || it.type == DOCK_TYPE_PORTABLE }
-                    ?.getLockList()?.find { it.rfid == rfid } != null
-            }
-
-            else -> {
-                false
-            }
-        }
+    fun updateLockNewHardware(slaveAddress: Byte, lockIdx: Int, newHardware: Boolean) {
+        dockList.find { it.addr == slaveAddress }?.getLockList()
+            ?.find { it.idx == lockIdx }?.newHardware = newHardware
     }
 
     /**
-     * 操作钥匙灯
-     *
-     * @param leftAction、rightAction 0:保持当前状态 1:点亮 2:熄灭 默认0
+     * 更新锁仓状态
      */
-    fun controlKeyLight(
-        slaveAddress: Byte?,
-        leftAction: Int = 0,
-        rightAction: Int = 0,
-        done: ((res: ByteArray) -> Unit)? = null
-    ) {
-        slaveAddress?.let {
-            modBusManager?.generateKeyLightCmd(leftAction, rightAction)?.let { cmd ->
-                modBusManager?.sendTo(it, cmd) {
-                    done?.invoke(it)
-                }
-            }
-        }
+    fun updateLockStatus(slaveAddress: Byte, lockIdx: Int, lockEnabled: Boolean) {
+        dockList.find { it.addr == slaveAddress }?.getLockList()
+            ?.find { it.idx == lockIdx }?.lockEnabled = lockEnabled
     }
 
     /**
@@ -448,7 +643,7 @@ object ModBusController {
         done: ((res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
-            modBusManager?.generateKeyBuckleCmd(isOpen, if (isLeft) 0 else 1)?.let { cmd ->
+            ModBusCMDHelper.generateKeyBuckleCmd(isOpen, if (isLeft) 0 else 1)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) { res ->
                     done?.invoke(res)
                 }
@@ -472,25 +667,44 @@ object ModBusController {
             .flatMap { it.getKeyList() }.find { it.mac == mac }
     }
 
+    /**
+     * 根据底座获取钥匙
+     */
     fun getKeyByDock(dockAddr: Byte?, isLeft: Boolean): DockBean.KeyBean? {
         dockAddr ?: return null
         return dockList.find { it.addr == dockAddr }?.getKeyList()?.find { it.isLeft == isLeft }
     }
 
     /**
-     * 根据RFID找锁具
+     * 钥匙是否存在
      */
-    fun getLockByRfid(rfid: String): DockBean.LockBean? {
-        return dockList.filter { it.type == DOCK_TYPE_LOCK || it.type == DOCK_TYPE_PORTABLE }
-            .flatMap { it.getLockList() }.find { it.rfid == rfid }
-    }
-
     fun isKeyExist(dockAddr: Byte?, isLeft: Boolean): Boolean {
         dockAddr ?: return false
         return dockList.find { it.addr == dockAddr }?.getKeyList()
             ?.find { it.isLeft == isLeft && it.isExist } != null
     }
 
+    /**
+     * 获取钥匙锁仓的锁定状态
+     */
+    fun getKeyBuckleLockEnabled(dockAddr: Byte?, isLeft: Boolean): Boolean {
+        dockAddr ?: return false
+        return dockList.find { it.addr == dockAddr }?.getKeyList()
+            ?.find { it.isLeft == isLeft }?.lockEnabled == true
+    }
+
+    /**
+     * 获取挂锁锁仓的锁定状态
+     */
+    fun getLockBuckleLockEnabled(dockAddr: Byte?, lockIdx: Int): Boolean {
+        dockAddr ?: return false
+        return dockList.find { it.addr == dockAddr }?.getLockList()
+            ?.find { it.idx == lockIdx }?.lockEnabled == true
+    }
+
+    /**
+     * 获取挂锁是否存在
+     */
     fun isLockExist(dockAddr: Byte?, lockIdx: Int): Boolean {
         dockAddr ?: return false
         return dockList.find { it.addr == dockAddr }?.getLockList()
@@ -567,13 +781,6 @@ object ModBusController {
         }
     }
 
-    fun getDockByLockNfc(nfc: String): DockBean? {
-        return dockList.find {
-            (it.type == DOCK_TYPE_LOCK || it.type == DOCK_TYPE_PORTABLE)
-                    && it.getLockList().any { it.rfid == nfc }
-        }
-    }
-
     /**
      * 根据类型获取底座列表
      */
@@ -581,12 +788,18 @@ object ModBusController {
         return dockList.filter { it.type == type }
     }
 
+    /**
+     * 根据底座类型获取钥匙列表
+     */
     fun getKeyByDockType(type: Byte): MutableList<DockBean.KeyBean>? {
         return dockList.find { it.type == type }?.let {
             it.getKeyList()
         }
     }
 
+    /**
+     * 全部锁扣的开关
+     */
     fun controlAllLockBuckles(isOpen: Boolean) {
         dockList.filter { it.type == DOCK_TYPE_LOCK || it.type == DOCK_TYPE_PORTABLE }
             .forEach { dockBean ->
@@ -598,6 +811,9 @@ object ModBusController {
             }
     }
 
+    /**
+     * 打印全部底座信息
+     */
     fun printDockInfo() {
         LogUtil.i("当前底座列表 : $dockList")
         dockList.forEach { dockBean ->
@@ -626,6 +842,9 @@ object ModBusController {
         }
     }
 
+    /**
+     * 更新设备类型
+     */
     fun updateDeviceType() {
         LogUtil.i("____________________________________")
         readDeviceType { res ->
@@ -722,4 +941,46 @@ object ModBusController {
         }
         return map
     }
+
+    /**
+     * 获取开关量数据
+     */
+    fun getSwitchData(): MutableList<DockBean.SwitchBean> {
+        return dockList.filter { it.type == DOCK_TYPE_COLLECT }.map { it.getSwitchList() }.flatten()
+            .toMutableList()
+    }
+
+    /**
+     * 读取开关采集板数据
+     */
+    fun readSwitchStatus(
+        slaveAddress: Byte?, addr: ByteArray, done: ((res: ByteArray) -> Unit)? = null
+    ) {
+        slaveAddress?.let {
+            ModBusCMDHelper.generateSwitchBoardStatusCmd(addr)?.let { cmd ->
+                //todo 模拟数据
+                val switchData = Random.nextBytes(2)
+                val switchDataFrame = ByteArray(5)
+                switchDataFrame[0] = slaveAddress
+                switchDataFrame[1] = FRAME_TYPE_READ
+                switchDataFrame[2] = 0x02
+                switchDataFrame[3] = switchData[0]
+                switchDataFrame[4] = switchData[1]
+                val switchDataFrameCRC = switchDataFrame.crc16(0, 4)
+                switchDataFrame[5] = switchDataFrameCRC[0]
+                switchDataFrame[6] = switchDataFrameCRC[1]
+                done?.invoke(
+                    byteArrayOf(
+                        slaveAddress,
+                        FRAME_TYPE_READ,
+                        switchData[0],
+                        switchData[1],
+                    ).crc16()
+                )
+//                modBusManager?.sendTo(it, cmd) { res ->
+//                    done?.invoke(res)
+//                }
+            }
+        }
+    }
 }

+ 125 - 426
app/src/main/java/com/grkj/iscs/modbus/ModBusManager.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.modbus
 
+import android.util.Log
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs.MyApplication
@@ -7,523 +8,221 @@ import com.grkj.iscs.extentions.crc16
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.SPUtils
-import com.grkj.iscs.util.jvmSeconds
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.fragment.DockTestFragment
-import java.util.concurrent.LinkedBlockingQueue
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.Channel
+import kotlin.random.Random
 
 /**
- * modbus 最小发送间隔(150豪秒)
- */
-// TODO 超时的可能也是用的这个,看情况是否增加到500
-//const val MODBUS_MIN_SEND_INTERVAL = 150_000_000
-const val MODBUS_MIN_SEND_INTERVAL = 500_000_000
-
-/**
- * 最大从机数量
- */
-const val MODBUS_MAX_SLAVE_COUNT = 16
-
-/**
- * 从机状态:未变化
- */
-const val MODBUS_SLAVE_STATUS__NO_CHANGE = -1
-
-/**
- * 从机状态:已经满溢
- */
-const val MODBUS_SLAVE_STATUS__FULL = 0b00100000
-
-
-/**
- * ModBus 协议管理器
+ * ModBus 协议管理器(协程版)
+ * 逻辑与原版一致:按顺序从队列取任务,发送请求,并在回调中匹配响应。
  */
 class ModBusManager(
-    // 从机数量
-    var slaveCount: Int,
-    // 串口管理器
-    val portManager: PortManager,
+    // 底层串口管理器
+    val portManager: PortManager?,
     // 是否输出详细信息
     val verbose: Boolean = false
 ) {
-
     @Volatile
     private var running = true
 
-    /**
-     * 正在发送的任务
-     */
+    /** 正在发送的任务 */
     @Volatile
     private var sending: FrameTask? = null
 
-    /**
-     * 等待发送队列
-     */
-    private val pendings = LinkedBlockingQueue<FrameTask>()
+    /** 等待发送队列 */
+    private val pendings = Channel<FrameTask>(Channel.UNLIMITED)
 
-    /**
-     * 线程锁
-     */
+    /** 锁保护 sending */
     private val lock = Any()
 
+    private var job: Job? = null
 
-    private var thread: Thread? = null
-
+    /** 从机地址池 */
     var mSlaveAddressList = mutableListOf<Byte>()
 
-
     init {
-        portManager.listen { res ->
-            if (verbose) {
-                LogUtil.i("接收:${res.toHexStrings()}")
-            }
+        // 串口监听,回调在单独线程中执行
+        portManager?.listen { res ->
+            if (verbose) LogUtil.i("接收:${res.toHexStrings()}")
             synchronized(lock) {
                 sending?.run {
                     if (match(res) && running) {
-                        done?.let { it(res) }
+                        done?.invoke(res)
                         sending = null
                     } else {
-                        LogUtil.w("响应: ${res.toHexStrings()}未匹配, running:${running}")
+                        LogUtil.w("响应: ${res.toHexStrings()} 未匹配, running: $running")
                     }
                 }
             }
         }
         // 初始化地址池
-        val dockConfigJson = SPUtils.getDockConfig(MyApplication.instance?.applicationContext!!)
-        if (!dockConfigJson.isNullOrEmpty()) {
-            val tempList: MutableList<DockTestFragment.DockTestBean> =
-                Gson().fromJson(
-                    dockConfigJson,
-                    object : TypeToken<MutableList<DockTestFragment.DockTestBean>>() {}.type
-                )
-            mSlaveAddressList.addAll(tempList.map { it.address })
-        }
+        val dockConfig = SPUtils.getDockConfig(MyApplication.instance?.applicationContext!!)
+        LogUtil.i("基座配置: ${dockConfig}")
+        dockConfig
+            ?.takeIf { it.isNotEmpty() }
+            ?.let { json ->
+                val type = object : TypeToken<List<DockTestFragment.DockTestBean>>() {}.type
+                val list: List<DockTestFragment.DockTestBean> = Gson().fromJson(json, type)
+                mSlaveAddressList.addAll(list.map { it.address })
+            }
     }
 
     /**
-     * 发送队列的消息
+     * 从队列中取任务并发送
      */
-    private fun takePendingToSend() {
+    private suspend fun takePendingToSend() {
         if (sending == null) {
-            sending = pendings.take()
-        }
-        if (!running) {
-            return
-        }
-        sending?.run {
-            waitIfNecessary()
+            sending = pendings.receive()
         }
+        if (!running) return
+        sending?.waitIfNecessary()
         synchronized(lock) {
             sending?.run {
                 if (shouldSend()) {
-                    if (portManager.send(req)) {
+                    if (portManager?.send(req) == true) {
                         afterSent()
-                        if (verbose) {
-                            LogUtil.i("发送:${req.toHexStrings()}")
-                        }
+                        if (verbose) LogUtil.i("发送:${req.toHexStrings()}")
                     } else {
-//                        Tip.toast("无法与主控板通讯")
                         LogUtil.w("无法与主控板通讯")
                     }
                 } else {
                     LogUtil.i("未响应: ${req.toHexStrings()}")
-                    // 放弃处理,回调空数据
-                    done?.let { it(byteArrayOf()) }
+                    done?.invoke(byteArrayOf())
                     sending = null
-//                    onFrameTimeout()
                 }
             }
         }
     }
 
-    private var timeouts = 0
-    private var lastTimeout = 0L
-
-    private fun onFrameTimeout() {
-        val now = jvmSeconds()
-        if (now - lastTimeout > 10) {
-            timeouts = 0
-        }
-        // 如果连续超时达到 15 次,则重建 Modbus 连接
-        if (++timeouts > 15) {
-//            EventBus.getDefault().post(ConfigEvent())
-        }
-        lastTimeout = now
-    }
-
     /**
-     * 循环发送给所有从机
-     * @param frame 发送报文
-     * @param listener 每轮发送完后的数据监听
-     * @param delayMills 每轮发送的间隔
+     * 开始通信(协程替代线程)
      */
-    fun repeatSendToAll(
-        frame: MBFrame,
-        interrupt: (() -> List<Boolean>)? = null,
-        listener: (res: List<ByteArray>) -> Unit,
-        delayMills: Long
-    ): ModBusManager {
-        val keep = interrupt?.invoke()?.run { !this[0] } ?: false
-        if (keep) {
-            sendToAll(frame) {
-                if (running) {
-                    listener(it)
-                    Executor.delayOnIO({
-                        if (running) {
-                            repeatSendToAll(frame, interrupt, listener, delayMills)
-                        }
-                    }, delayMills)
+    fun start() {
+        job = CoroutineScope(Dispatchers.IO).launch {
+            while (running) {
+                try {
+                    takePendingToSend()
+                } catch (_: CancellationException) {
+                    break
                 }
             }
-        } else {
-            Executor.delayOnIO({
-                if (running) {
-                    repeatSendToAll(frame, interrupt, listener, delayMills)
-                }
-            }, delayMills)
         }
-        return this
     }
 
-
     /**
-     * 发送给所有从机
-     * @param frame 发送报文
-     * @param done 所有从机都发送完成后的回调
+     * 停止运行并关闭串口
      */
-    fun sendToAll(frame: MBFrame, done: ((res: List<ByteArray>) -> Unit)? = null) {
-//        if (slaveCount == 0) {
-        if (mSlaveAddressList.size == 0) {
-            done?.let { it(listOf()) }
-            return
-        }
-        sendUp(0, frame, done, ArrayList())
+    fun stop() {
+        running = false
+        job?.cancel()
+        portManager?.close()
     }
 
-    private fun sendUp(
-        index: Int,
-        frame: MBFrame,
-        done: ((res: List<ByteArray>) -> Unit)?,
-        resList: ArrayList<ByteArray>
-    ) {
-        sendTo(mSlaveAddressList[index], frame) { res ->
-            resList.add(res)
-//            if (index >= slaveCount - 1) {
-            if (index >= mSlaveAddressList.size - 1) {
-                // 已经发送完
-                if (running) {
-                    done?.let { it(resList) }
-                }
-            } else {
-                // 发送给下一个从机
-                sendUp(index + 1, frame, done, resList)
-            }
-        }
-    }
+    /**
+     * 是否运行
+     */
+    fun isRunning(): Boolean = running
 
     /**
-     * 发送给序号为 index 的从机
-     * @param slaveAddress 从机地址
-     * @param frame 发送报文
-     * @param done 完成回调
+     * 提交单个请求任务
      */
     fun sendTo(
         slaveAddress: Byte,
         frame: MBFrame,
         allowRetransmission: Boolean = true,
         minSendIntervalNanoSeconds: Int = MODBUS_MIN_SEND_INTERVAL,
-        done: ((res: ByteArray) -> Unit)? = null
+        done: ((ByteArray) -> Unit)? = null
     ) {
-//        if (slaveCount <= 0) {
-        if (mSlaveAddressList.size <= 0) {
-            LogUtil.i("sendTo($slaveAddress), 地址池大小为0, 返回空数据")
+        if (mSlaveAddressList.isEmpty()) {
+            LogUtil.i("sendTo($slaveAddress), 地址池为空, 返回空数据")
             done?.invoke(byteArrayOf())
             return
         }
-//        if (index < 0 || index >= slaveCount) {
-//            throw IllegalArgumentException("index [${index}] out of bound [${slaveCount}]")
-//        }
-        if (mSlaveAddressList.none { it == slaveAddress }) {
-            throw IllegalArgumentException("slaveAddress [${slaveAddress}] is not configed")
-        }
-
-//        val task = FrameTask(frame.compile(index), done)
-        val task = FrameTask(frame.compile(slaveAddress), done)
-        task.allowRetransmission = allowRetransmission
-        task.minSendInterval = minSendIntervalNanoSeconds
-        pendings.add(task)
-    }
-
-    fun start() {
-        thread = Thread {
-            while (running) {
-                try {
-                    takePendingToSend()
-                } catch (e: InterruptedException) {
-                }
-            }
+        if (slaveAddress !in mSlaveAddressList) throw IllegalArgumentException("slaveAddress[$slaveAddress] 未配置")
+        val task = FrameTask(frame.compile(slaveAddress), done).apply {
+            this.allowRetransmission = allowRetransmission
+            this.minSendInterval = minSendIntervalNanoSeconds
         }
-        thread?.isDaemon = true
-        thread?.start()
-    }
-
-    fun isRunning(): Boolean {
-        return running
-    }
-
-    fun stop() {
-        running = false
-        thread?.interrupt()
-        portManager.close()
+        pendings.trySend(task)
     }
 
     /**
-     * 生成锁具/便携式底座 单个锁具卡扣开关指令
+     * 按顺序发送给所有从机,汇总结果后回调
      */
-    fun generateLockBuckleCmd(isOpen: Boolean, lockIndex: Int): MBFrame {
-        var str = ""
-        val idx = lockIndex - (lockIndex / 8) * 8
-        for (i in 7 downTo 0) {
-            str += if (i == idx) {
-                "1"
-            } else {
-                "0"
-            }
+    fun sendToAll(
+        frame: MBFrame,
+        done: ((List<ByteArray>) -> Unit)? = null
+    ) {
+        if (mSlaveAddressList.isEmpty()) {
+            done?.invoke(emptyList()); return
         }
-
-        // 第三位是 是否响应,第四位是操作哪个,操作默认全是0或者1,使用第三位响应来进行操作
-        return MBFrame(
-            FRAME_TYPE_WRITE,
-            byteArrayOf(0x00,
-                if (lockIndex in 0..7) 0x11 else 0x12,
-                str.toInt(2).toByte(),
-                if (isOpen) 0x00 else 0xFF.toByte()
-            )
-        )
+        sendUp(0, frame, done, mutableListOf())
     }
 
-    /**
-     * 生成多个锁具卡扣开关指令(只能针对同一个底座,不能跨底座)
-     */
-    fun generateLockBuckleCmd(isOpen: Boolean, lockIndexList: MutableList<Int>): MutableList<MBFrame> {
-        var str = ""
-        val rstList = mutableListOf<MBFrame>()
-        val smallIdxList = lockIndexList.filter { it in 0..7 }
-        val bigIdxList = lockIndexList.filter { it in 8..15 }
-        if (smallIdxList.isNotEmpty()) {
-            for (i in 7 downTo 0) {
-                str += if (smallIdxList.contains(i)) {
-                    "1"
-                } else {
-                    "0"
-                }
-            }
-            rstList.add(MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x11, str.toInt(2).toByte(), if (isOpen) 0x00 else 0xFF.toByte())))
-        }
-        if (bigIdxList.isNotEmpty()) {
-            str = ""
-            for (i in 7 downTo 0) {
-                str += if (bigIdxList.contains(i + 8)) {
-                    "1"
-                } else {
-                    "0"
+    private fun sendUp(
+        index: Int,
+        frame: MBFrame,
+        done: ((List<ByteArray>) -> Unit)?,
+        results: MutableList<ByteArray>
+    ) {
+        sendTo(mSlaveAddressList[index], frame) { res ->
+            results.add(res)
+            if (index == mSlaveAddressList.lastIndex) {
+                //todo 模拟开关级联板的数量
+                if (frame.type == FRAME_TYPE_READ && frame.data[0] == 0x00.toByte() && frame.data[0] == 0x11.toByte()) {
+                    val switchBoardSizeData = Random.nextBytes(1)
+                    val switchBoardSizeDataFrame = ByteArray(6)
+                    switchBoardSizeDataFrame[0] = 0xA1.toByte()
+                    switchBoardSizeDataFrame[1] = FRAME_TYPE_READ
+                    switchBoardSizeDataFrame[2] = 0x02
+                    switchBoardSizeDataFrame[3] = 0x00
+                    switchBoardSizeDataFrame[4] = switchBoardSizeData[0]
+                    val switchBoardSizeDataCRC = switchBoardSizeDataFrame.crc16(0, 3)
+                    switchBoardSizeDataFrame[4] = switchBoardSizeDataCRC[0]
+                    switchBoardSizeDataFrame[5] = switchBoardSizeDataCRC[1]
+                    results.add(switchBoardSizeDataFrame)
                 }
-            }
-            rstList.add(MBFrame(FRAME_TYPE_WRITE, byteArrayOf(0x00, 0x12, str.toInt(2).toByte(), if (isOpen) 0x00 else 0xFF.toByte())))
+                if (running) done?.invoke(results)
+            } else sendUp(index + 1, frame, done, results)
         }
-        return rstList
-    }
-
-
-
-    /**
-     * 生成钥匙底座灯光指令
-     *
-     * @param leftAction、rightAction 0:保持当前状态 1:点亮 2:熄灭 默认0
-     */
-    @Deprecated("已取消")
-    fun generateKeyLightCmd(leftAction: Int = 0, rightAction: Int = 0): MBFrame {
-        return MBFrame(
-            FRAME_TYPE_WRITE,
-            // TODO 第三个是高位,第四个是低位
-            byteArrayOf(0x00, 0x15, rightAction.toByte(), leftAction.toByte())
-        )
     }
 
     /**
-     * 操作钥匙/便携式底座钥匙卡扣,一次只操作一个卡扣
-     *
-     * @param isOpen true:开操作 false:关操作
-     * @param index 0:左 1:右 便携式底座钥匙传0
-     */
-    fun generateKeyBuckleCmd(isOpen: Boolean, index: Int): MBFrame {
-        return MBFrame(
-            FRAME_TYPE_WRITE,
-            byteArrayOf(0x00, 0x11, if (index == 1) 0b00010000.toByte() else 0b0000001, if (isOpen) 0x00 else 0xFF.toByte())
-        )
-    }
-
-    /**
-     * 生成RFID 读指令
-     *
-     * @param idx 锁具底座锁具:0-9  钥匙底座钥匙:0(left)-1(right)  便携式底座锁具:0-2  便携式底座钥匙:4   便携式底座卡:8
-     */
-    fun generateRfidCmd(idx: Int): MBFrame {
-        return MBFrame(
-            FRAME_TYPE_READ,
-            byteArrayOf(0x00, (0x20 + idx * 4).toByte(), 0x00, 0x04)
-        )
-    }
-}
-
-
-class FrameTask(
-    val req: ByteArray,
-    val done: ((res: ByteArray) -> Unit)?    // 响应回调
-) {
-
-    /**
-     * 是否允许重发
-     */
-    var allowRetransmission: Boolean = true
-
-    /**
-     * 上次发送时间
-     */
-    var lastSent: Long = 0
-
-    /**
-     * 已发送次数
-     */
-    var sentCount = 0
-
-    /**
-     * 最小发送间隔
+     * 循环发送给所有从机
      */
-    var minSendInterval: Int = MODBUS_MIN_SEND_INTERVAL
-
-
-    fun waitIfNecessary() {
-        val interval = System.nanoTime() - lastSent
-        if (interval < minSendInterval) {
-            Thread.sleep((minSendInterval - interval) / 1000_000)
-        }
-    }
-
-    fun shouldSend(): Boolean {
-        return if (allowRetransmission) {
-            sentCount < 3
+    fun repeatSendToAll(
+        frame: MBFrame,
+        interrupt: (() -> List<Boolean>)? = null,
+        listener: (List<ByteArray>) -> Unit,
+        delayMills: Long
+    ): ModBusManager {
+        val keep = interrupt?.invoke()?.run { !this[0] } ?: false
+        if (keep) {
+            sendToAll(frame) {
+                if (running) {
+                    listener(it)
+                    Executor.delayOnIO({
+                        if (running) repeatSendToAll(
+                            frame,
+                            interrupt,
+                            listener,
+                            delayMills
+                        )
+                    }, delayMills)
+                }
+            }
         } else {
-            sentCount < 1
-        }
-    }
-
-    fun afterSent() {
-        sentCount++
-        lastSent = System.nanoTime()
-    }
-
-    /**
-     * 判断 res 是否是 frame 的响应
-     */
-    fun match(res: ByteArray): Boolean {
-        // 从机地址 和 功能码 必须相同
-        if (res.size < 5 || req[0] != res[0] || req[1] != res[1]) {
-            return false
+            Executor.delayOnIO({
+                if (running) repeatSendToAll(
+                    frame,
+                    interrupt,
+                    listener,
+                    delayMills
+                )
+            }, delayMills)
         }
-        // 报文2 的 CRC校验得正确
-        val crc16 = res.crc16(0, res.size - 2)
-        return crc16[0] == res[res.size - 2] && crc16[1] == res[res.size - 1]
+        return this
     }
-
 }
-
-private const val FRAME_TYPE_READ: Byte = 0x03
-private const val FRAME_TYPE_WRITE: Byte = 0x06
-private const val FRAME_TYPE_SCANNER: Byte = 0x43
-const val FRAME_TYPE_WRITE_MULTI: Byte = 0x10
-const val FRAME_TYPE_WRITE_FILE: Byte = 0x15
-
-/**
- * ModBus 数据帧
- */
-class MBFrame(
-
-    // 类型
-    val type: Byte,
-    // 数据域:D1 和 D2
-    val data: ByteArray
-
-) {
-
-    /**
-     * @param index 从机序号
-     */
-    fun compile(index: Int): ByteArray {
-        val bytes = ByteArray(4 + data.size)
-        // TODO 从机开始地址0x01
-//        bytes[0] = (0x80 + index).toByte()
-        bytes[0] = (1 + index).toByte()
-        bytes[1] = type
-        for (i in data.indices) {
-            bytes[2 + i] = data[i]
-        }
-        val crc16 = bytes.crc16(0, bytes.size - 2)
-        bytes[bytes.size - 2] = crc16[0]
-        bytes[bytes.size - 1] = crc16[1]
-        return bytes
-
-//        val cmd = byteArrayOf((1 + index).toByte()) + byteArrayOf(type) + data
-//        return cmd + cmd.crc16()
-    }
-
-    /**
-     * @param address 从机地址
-     */
-    fun compile(address: Byte): ByteArray {
-        val bytes = ByteArray(4 + data.size)
-        bytes[0] = address
-        bytes[1] = type
-        for (i in data.indices) {
-            bytes[2 + i] = data[i]
-        }
-        val crc16 = bytes.crc16(0, bytes.size - 2)
-        bytes[bytes.size - 2] = crc16[0]
-        bytes[bytes.size - 1] = crc16[1]
-        return bytes
-    }
-
-    companion object {
-        /**
-         * 读取设备类型
-         */
-        val READ_DEVICE_TYPE = MBFrame(
-            FRAME_TYPE_READ,
-            byteArrayOf(0x00, 0x00, 0x00, 0x01)
-        )
-
-        /**
-         * 读钥匙/锁具/便携式底座状态
-         */
-        val READ_STATUS = MBFrame(
-            FRAME_TYPE_READ,
-            byteArrayOf(0x00, 0x10, 0x00, 0x01)
-        )
-
-        /**
-         * 读卡扣状态(钥匙、锁的0-7、便携式的锁和钥匙)
-         */
-        val READ_BUCKLE_STATUS = MBFrame(
-            FRAME_TYPE_READ,
-            byteArrayOf(0x00, 0x11, 0x00, 0x01)
-        )
-
-        /**
-         * 读卡扣状态(锁的9、10)
-         */
-        val READ_LOCK_BUCKLE_EXTRA_STATUS = MBFrame(
-            FRAME_TYPE_READ,
-            byteArrayOf(0x00, 0x12, 0x00, 0x01)
-        )
-    }
-}

+ 12 - 7
app/src/main/java/com/grkj/iscs/modbus/PortManager.kt

@@ -3,6 +3,7 @@ package com.grkj.iscs.modbus
 import android.content.Context
 import androidx.annotation.WorkerThread
 import com.epton.sdk.SerialPort
+import com.google.android.datatransport.runtime.dagger.Reusable
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
@@ -27,7 +28,7 @@ class PortManager private constructor(
     /**
      * 向串口发送数据
      */
-    fun send(data: ByteArray) : Boolean {
+    fun send(data: ByteArray): Boolean {
         try {
             output.write(data)
             output.flush()
@@ -38,7 +39,7 @@ class PortManager private constructor(
         }
     }
 
-    var t : Thread ? = null
+    var t: Thread? = null
 
     /**
      * 监听串口数据
@@ -75,7 +76,8 @@ class PortManager private constructor(
         try {
             input.close()
             output.close()
-        } catch (e: Exception) { }
+        } catch (e: Exception) {
+        }
     }
 
     companion object {
@@ -88,7 +90,7 @@ class PortManager private constructor(
          * @param bps 波特率,正整数
          */
         @WorkerThread
-        fun open(port: Int, bps: Int, usb: Boolean) : PortManager? {
+        fun open(port: Int, bps: Int, usb: Boolean): PortManager? {
             var blocked = true
             val thread = Thread.currentThread()
             Executor.delayOnMain(500) {
@@ -99,7 +101,7 @@ class PortManager private constructor(
                 }
             }
             try {
-                val file = File(if(usb) "/dev/ttyUSB${port}" else "/dev/ttyS${port}")
+                val file = File(if (usb) "/dev/ttyUSB${port}" else "/dev/ttyS${port}")
                 LogUtil.i("连接 port file")
                 SerialPort(file, bps, 0).run {
                     blocked = false
@@ -124,7 +126,10 @@ class PortManager private constructor(
          * @param bps 波特率,正整数
          */
         @WorkerThread
-        fun open(port: String?, bps: Int) : PortManager? {
+        fun open(port: String?, bps: Int): PortManager? {
+            if (port == null) {
+                return null
+            }
             var blocked = true
             val thread = Thread.currentThread()
             Executor.delayOnMain(500) {
@@ -155,7 +160,7 @@ class PortManager private constructor(
          * 打开主控板
          */
         @WorkerThread
-        fun openCtrlBord(ctx: Context) : PortManager? {
+        fun openCtrlBord(ctx: Context): PortManager? {
             // TODO 端口号待定:大屏调试设备-1,小屏调试设备-0
 //            val port = 4
 //            val bps = 115200

+ 10 - 0
app/src/main/java/com/grkj/iscs/modbus/StatusListener.kt

@@ -0,0 +1,10 @@
+package com.grkj.iscs.modbus
+
+/**
+ * 状态监听器
+ */
+class StatusListener(
+    val key: Any,
+    val listener: (Any) -> Unit,
+    val type: Int
+)

+ 1 - 0
app/src/main/java/com/grkj/iscs/model/DeviceConst.kt

@@ -17,4 +17,5 @@ object DeviceConst {
     const val DEVICE_TYPE_CARD = 2          // 卡
     const val DEVICE_TYPE_FINGERPRINT = 3   // 指纹
     const val DEVICE_TYPE_PORTAL_CASE = 4   // 便携柜
+    const val DEVICE_TYPE_SWITCH = 5        // 开关
 }

+ 29 - 0
app/src/main/java/com/grkj/iscs/model/ISCSDatabase.kt

@@ -0,0 +1,29 @@
+package com.grkj.iscs.model
+
+import androidx.room.Database
+import androidx.room.Room
+import androidx.room.RoomDatabase
+import com.grkj.iscs.MyApplication
+
+/**
+ * 本地数据库
+ */
+@Database(version = ISCSMigrations.VERSION)
+abstract class ISCSDatabase : RoomDatabase() {
+    companion object {
+        /**
+         * 单例
+         */
+        @JvmStatic
+        val instance: ISCSDatabase by lazy {
+            Room.databaseBuilder(
+                MyApplication.instance?.applicationContext!!,
+                ISCSDatabase::class.java,
+                "iscs_database"
+            )
+                // 如需在主线程查询,可取消下行注释(不推荐):
+                // .allowMainThreadQueries()
+                .build()
+        }
+    }
+}

+ 11 - 0
app/src/main/java/com/grkj/iscs/model/ISCSMigrations.kt

@@ -0,0 +1,11 @@
+package com.grkj.iscs.model
+
+/**
+ * 数据库升级、版本相关信息
+ */
+object ISCSMigrations {
+    /**
+     * 版本号
+     */
+    const val VERSION = 1
+}

+ 20 - 3
app/src/main/java/com/grkj/iscs/model/UrlConsts.kt

@@ -1,11 +1,12 @@
 package com.grkj.iscs.model
 
 object UrlConsts {
-//    const val BASE_URL = "http://192.168.28.82:9190"  // 本地
+    //    const val BASE_URL = "http://192.168.28.82:9190"  // 本地
 //    const val BASE_URL = "http://192.168.28.97:9190"    // 车
 //    const val BASE_URL = "http://36.133.174.236:9190"    // 外
     const val BASE_URL = "http://192.168.0.10:9190"    // 外
-//    const val BASE_URL = "http://120.27.232.27:9190"    // 外
+
+    //    const val BASE_URL = "http://120.27.232.27:9190"    // 外
     const val WEB_SOCKET = "ws://192.168.1.127:9090/websocket/iot/127"
 
     const val AUTOCODE_TICKET_NUMBER = "JOB_TICKET_CODE"
@@ -233,7 +234,8 @@ object UrlConsts {
     /**
      * 删除用户特征(指纹、面部)
      */
-    const val REMOVE_USER_CHARACTERISTIC = "/system/user/characteristic/removeSysUserCharacteristicByRecordIds"
+    const val REMOVE_USER_CHARACTERISTIC =
+        "/system/user/characteristic/removeSysUserCharacteristicByRecordIds"
 
     /**
      * 新增异常记录
@@ -274,4 +276,19 @@ object UrlConsts {
      * 解锁取钥匙前检查
      */
     const val CHECK_BEFORE_UNLOCKING = "/iscs/hardware-api/checkBeforeToUnlock"
+
+    /**
+     * 批量更新硬件状态
+     */
+    const val UPDATE_HARDWARE_ES_STATUS = "/iscs/hardware-api/updateHardwareEsStatus"
+
+    /**
+     * 重合点位数据解锁
+     */
+    const val UPDATE_COINCIDE_TO_UNLOCK = "/iscs/hardware-api/updateCoincideToUnLock"
+
+    /**
+     * 查询锁控机位-仓位-分页
+     */
+    const val GET_IS_LOCK_CABINET_SLOTS_PAGE = "/dev-api/iscs/slots/getIsLockCabinetSlotsPage"
 }

+ 41 - 0
app/src/main/java/com/grkj/iscs/model/vo/hardware/CabinetSlotsRespVo.kt

@@ -0,0 +1,41 @@
+package com.grkj.iscs.model.vo.hardware
+
+/**
+ * 锁柜-仓位返回实体
+ */
+data class CabinetSlotsRespVo(
+    val countId: String,
+    val current: Int,
+    val maxLimit: Int,
+    val optimizeCountSql: Boolean,
+    val orders: List<Order>,
+    val pages: Int,
+    val records: List<CabinetSlotsRecord>,
+    val searchCount: Boolean,
+    val size: Int,
+    val total: Int
+)
+
+data class CabinetSlotsRecord(
+    val cabinetId: Int,
+    val col: String,
+    val createBy: String,
+    val createTime: String,
+    val delFlag: String,
+    val isOccupied: String,
+    val occupiedBy: Int,
+    val paramMap: Map<String, String>,
+    val remark: String,
+    val row: String,
+    val slotCode: String,
+    val slotId: Long,
+    val slotType: String,
+    val status: String,
+    val updateBy: String,
+    val updateTime: String
+)
+
+data class Order(
+    val asc: Boolean,
+    val column: String
+)

+ 28 - 0
app/src/main/java/com/grkj/iscs/model/vo/hardware/UpdateHardwareEsStatusReqVO.kt

@@ -0,0 +1,28 @@
+package com.grkj.iscs.model.vo.hardware
+
+/**
+ * 工卡异常数据
+ */
+data class JobCardExDTO(
+    val cardNfc: String,
+    val exRemark: String,
+    val exStatus: String
+)
+
+/**
+ * 钥匙异常数据
+ */
+data class KeyExDTO(
+    val keyNfc: String,
+    val exRemark: String,
+    val exStatus: String
+)
+
+/**
+ * 挂锁异常数据
+ */
+data class LockExDTO(
+    val lockNfc: String,
+    val exRemark: String,
+    val exStatus: String
+)

+ 2 - 0
app/src/main/java/com/grkj/iscs/util/BitmapUtil.kt

@@ -8,6 +8,8 @@ import android.graphics.ImageFormat
 import android.graphics.Rect
 import android.graphics.YuvImage
 import android.os.Environment
+import androidx.annotation.DrawableRes
+import androidx.core.content.ContextCompat
 import com.bumptech.glide.Glide
 import com.bumptech.glide.load.DataSource
 import com.bumptech.glide.load.engine.GlideException

+ 204 - 35
app/src/main/java/com/grkj/iscs/util/NetApi.kt

@@ -10,6 +10,10 @@ import com.grkj.iscs.model.vo.characteristic.CharacteristicPageRespVO
 import com.grkj.iscs.model.vo.dept.DeptListRespVO
 import com.grkj.iscs.model.vo.dict.CommonDictRespVO
 import com.grkj.iscs.model.vo.finger.LoginCharacteristicRespVO
+import com.grkj.iscs.model.vo.hardware.CabinetSlotsRespVo
+import com.grkj.iscs.model.vo.hardware.JobCardExDTO
+import com.grkj.iscs.model.vo.hardware.KeyExDTO
+import com.grkj.iscs.model.vo.hardware.LockExDTO
 import com.grkj.iscs.model.vo.key.KeyInfoRespVO
 import com.grkj.iscs.model.vo.lock.LockInfoRespVO
 import com.grkj.iscs.model.vo.lock.LockTakeUpdateReqVO
@@ -60,7 +64,8 @@ object NetApi {
                 } ?: run {
                     callBack.invoke(false)
                 }
-            }, isGet = false, isAuth = false)
+            }, isGet = false, isAuth = false
+        )
     }
 
     /**
@@ -96,13 +101,20 @@ object NetApi {
                 } ?: run {
                     callBack.invoke(false)
                 }
-            }, isGet = false, isAuth = false)
+            }, isGet = false, isAuth = false
+        )
     }
 
     /**
      * 获取SOP分页
      */
-    fun getSopPage(pages: Int, size: Int, machineryId: Long, sopType: Int, callBack: (SopPageRespVO?) -> Unit) {
+    fun getSopPage(
+        pages: Int,
+        size: Int,
+        machineryId: Long,
+        sopType: Int,
+        callBack: (SopPageRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.SOP_PAGE,
             false,
@@ -116,7 +128,8 @@ object NetApi {
                 res?.let {
                     callBack.invoke(getRefBean(it))
                 }
-            }, isGet = true, isAuth = true)
+            }, isGet = true, isAuth = true
+        )
     }
 
     /**
@@ -127,12 +140,13 @@ object NetApi {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.TICKET_TYPE,
             false,
-            mapOf<String,String>(),
+            mapOf<String, String>(),
             { res, _, _ ->
                 res?.let {
                     callBack.invoke(getRefBean(it))
                 }
-            }, isGet = true, isAuth = true)
+            }, isGet = true, isAuth = true
+        )
     }
 
     /**
@@ -141,14 +155,15 @@ object NetApi {
     @Deprecated("不使用")
     fun getAutoCode(type: String, callBack: (String?) -> Unit) {
         NetHttpManager.getInstance().doRequestNet(
-            UrlConsts.AUTO_CODE + "/" +type,
+            UrlConsts.AUTO_CODE + "/" + type,
             false,
             mapOf<String, String>(),
             { res, _, _ ->
                 res?.let {
                     callBack.invoke(it.toString())
                 }
-            }, isGet = true, isAuth = true)
+            }, isGet = true, isAuth = true
+        )
     }
 
     /**
@@ -166,7 +181,8 @@ object NetApi {
                 res?.let {
                     callBack.invoke(getRefBean(it))
                 }
-            }, isGet = true, isAuth = true)
+            }, isGet = true, isAuth = true
+        )
     }
 
     /**
@@ -183,7 +199,8 @@ object NetApi {
         unitId: Long? = null,
         callBack: (UserListRespVO?) -> Unit
     ) {
-        val map: MutableMap<String, Any> = mutableMapOf("pageNum" to pageNum, "pageSize" to pageSize)
+        val map: MutableMap<String, Any> =
+            mutableMapOf("pageNum" to pageNum, "pageSize" to pageSize)
         workstationId?.let {
             map["workstationId"] = it
         }
@@ -201,7 +218,8 @@ object NetApi {
                 res?.let {
                     callBack.invoke(it.toBean(UserListRespVO::class.java))
                 }
-            }, isGet = true, isAuth = true)
+            }, isGet = true, isAuth = true
+        )
     }
 
     /**
@@ -248,7 +266,13 @@ object NetApi {
      * 获取工作票分页
      */
     @Deprecated("不使用")
-    fun getTicketPage(pageNum: Int, pageSize: Int, userId: Long, ticketStatus: Int?, callBack: (TicketPageRespVO?) -> Unit) {
+    fun getTicketPage(
+        pageNum: Int,
+        pageSize: Int,
+        userId: Long,
+        ticketStatus: Int?,
+        callBack: (TicketPageRespVO?) -> Unit
+    ) {
         val map = mutableMapOf(
             "pageNum" to pageNum,
             "pageSize" to pageSize,
@@ -301,7 +325,7 @@ object NetApi {
                 }
             }, isGet = true, isAuth = true
         )
-   }
+    }
 
     /**
      * 通过nfc编号获取key信息
@@ -387,7 +411,12 @@ object NetApi {
     /**
      * 挂锁上锁时更新数据,更新挂锁和哪个隔离点进行了绑定
      */
-    fun updateLockPoint(ticketId: Long, lockNfc: String, pointNfc: String, callBack: (Boolean?) -> Unit) {
+    fun updateLockPoint(
+        ticketId: Long,
+        lockNfc: String,
+        pointNfc: String,
+        callBack: (Boolean?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.LOCK_POINT_UPDATE,
             false,
@@ -423,7 +452,12 @@ object NetApi {
     /**
      * 取出钥匙
      */
-    fun updateKeyTake(ticketId: Long, keyNfc: String, serialNumber: String, callBack: (Boolean) -> Unit) {
+    fun updateKeyTake(
+        ticketId: Long,
+        keyNfc: String,
+        serialNumber: String,
+        callBack: (Boolean) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.KEY_TAKE_UPDATE,
             false,
@@ -441,7 +475,13 @@ object NetApi {
     /**
      * 归还钥匙
      */
-    fun updateKeyReturn(ticketId: Long, keyNfc: String, serialNumber: String, retryCount: Int = 3, callBack: (Boolean) -> Unit) {
+    fun updateKeyReturn(
+        ticketId: Long,
+        keyNfc: String,
+        serialNumber: String,
+        retryCount: Int = 3,
+        callBack: (Boolean, String) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.KEY_RETURN_UPDATE,
             false,
@@ -450,16 +490,22 @@ object NetApi {
                 "keyNfc" to keyNfc,
                 "serialNumber" to serialNumber
             ),
-            { res, _, _ ->
+            { res, msg, _ ->
                 res?.let {
-                    callBack(true)
+                    callBack(true, msg ?: "")
                 } ?: let {
                     if (retryCount > 0) {
                         Executor.delayOnIO(500) {
-                            updateKeyReturn(ticketId, keyNfc, serialNumber, retryCount - 1, callBack)
+                            updateKeyReturn(
+                                ticketId,
+                                keyNfc,
+                                serialNumber,
+                                retryCount - 1,
+                                callBack
+                            )
                         }
                     } else {
-                        callBack.invoke(false)
+                        callBack.invoke(false, msg ?: "")
                     }
                 }
                 // TODO isAuth需要配置
@@ -487,7 +533,11 @@ object NetApi {
     /**
      * 批量更新作业票下隔离点的上锁解锁状况
      */
-    fun updateLockPointBatch(list: MutableList<LockPointUpdateReqVO>, retryCount: Int = 3, callBack: (Boolean) -> Unit) {
+    fun updateLockPointBatch(
+        list: MutableList<LockPointUpdateReqVO>,
+        retryCount: Int = 3,
+        callBack: (Boolean) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.LOCK_POINT_UPDATE_BATCH,
             false,
@@ -514,7 +564,12 @@ object NetApi {
      * 根据角色获取人员列表
      */
     @Deprecated("不使用")
-    fun getRoleList(pageNum: Int, pageSize: Int, roleKey: String, callBack: (RoleListRespVO?) -> Unit) {
+    fun getRoleList(
+        pageNum: Int,
+        pageSize: Int,
+        roleKey: String,
+        callBack: (RoleListRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.ROLE_LIST,
             false,
@@ -534,7 +589,11 @@ object NetApi {
     /**
      * 正在进行中的作业票列表
      */
-    fun getWorkstationTicketList(pages: Int, size: Int, callBack: (MutableList<WorkstationTicketListRespVO>?) -> Unit) {
+    fun getWorkstationTicketList(
+        pages: Int,
+        size: Int,
+        callBack: (MutableList<WorkstationTicketListRespVO>?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.WORKSTATION_TICKET_LIST,
             false,
@@ -553,7 +612,12 @@ object NetApi {
     /**
      * 获取工艺分页
      */
-    fun getMachineryPage(pages: Int, size: Int, workstationId: Long, callBack: (MachineryPageRespVO?) -> Unit) {
+    fun getMachineryPage(
+        pages: Int,
+        size: Int,
+        workstationId: Long,
+        callBack: (MachineryPageRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.MACHINERY_PAGE,
             false,
@@ -623,7 +687,7 @@ object NetApi {
                 }
             }, isGet = true, isAuth = true
         )
-   }
+    }
 
     /**
      * 取消作业票
@@ -668,7 +732,11 @@ object NetApi {
     /**
      * 新增/更新作业票人员
      */
-    fun updateTicketUser(ticketId: Long, userList: MutableList<TicketUserReqVO>, callBack: (Boolean) -> Unit) {
+    fun updateTicketUser(
+        ticketId: Long,
+        userList: MutableList<TicketUserReqVO>,
+        callBack: (Boolean) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.UPDATE_TICKET_USER,
             false,
@@ -728,7 +796,12 @@ object NetApi {
     /**
      * 共锁人上锁/解锁
      */
-    fun updateColockerStatus(ticketId: Long, cardNfc: String, jobStatus: String, callBack: (Boolean) -> Unit) {
+    fun updateColockerStatus(
+        ticketId: Long,
+        cardNfc: String,
+        jobStatus: String,
+        callBack: (Boolean) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.UPDATE_COLOCKER_STATUS,
             false,
@@ -788,7 +861,12 @@ object NetApi {
      *
      * @param mapId 机柜固定传1,物资柜固定传4
      */
-    fun getMapPointPage(pages: Int, size: Int, mapId: Long, callBack: (MapPointPageRespVO?) -> Unit) {
+    fun getMapPointPage(
+        pages: Int,
+        size: Int,
+        mapId: Long,
+        callBack: (MapPointPageRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.MAP_POINT_PAGE,
             false,
@@ -808,7 +886,11 @@ object NetApi {
     /**
      * 新增指纹录入-指纹图片转成dat存储
      */
-    fun insertFinger(userName: String, fileList: MutableList<FileStreamReqParam>?, callBack: (Boolean) -> Unit) {
+    fun insertFinger(
+        userName: String,
+        fileList: MutableList<FileStreamReqParam>?,
+        callBack: (Boolean) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.INSERT_FINGER,
             false,
@@ -835,14 +917,15 @@ object NetApi {
             mapOf<String, String>(),
             { res, _, _ ->
                 res?.let {
-                    val resp: LoginCharacteristicRespVO? = it.toBean(LoginCharacteristicRespVO::class.java)
+                    val resp: LoginCharacteristicRespVO? =
+                        it.toBean(LoginCharacteristicRespVO::class.java)
                     Token(resp?.token!!, 0).saveToSp(MyApplication.instance!!.applicationContext)
                     callBack.invoke(true)
                 } ?: run {
                     callBack.invoke(false)
                 }
             }, isGet = false, isAuth = false, fileList = fileList
-       )
+        )
     }
 
     /**
@@ -850,7 +933,13 @@ object NetApi {
      *
      * @param type 1-指纹,2-面部
      */
-    fun getUserCharacteristicPage(pages: Int, size: Int, type: String, userId: Long, callBack: (CharacteristicPageRespVO?) -> Unit) {
+    fun getUserCharacteristicPage(
+        pages: Int,
+        size: Int,
+        type: String,
+        userId: Long,
+        callBack: (CharacteristicPageRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.USER_CHARACTERISTIC_PAGE,
             false,
@@ -908,7 +997,9 @@ object NetApi {
             "exceptionType" to exceptionType,
             "raiser" to raiser,
             "sourceName" to sourceName,
-            "raiseTime" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Calendar.getInstance().time)
+            "raiseTime" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(
+                Calendar.getInstance().time
+            )
         )
         exceptionDescription?.let {
             map["exceptionDescription"] = it
@@ -973,7 +1064,8 @@ object NetApi {
             mapOf<String, String>(),
             { res, _, _ ->
                 res?.let {
-                    val resp: LoginCharacteristicRespVO? = it.toBean(LoginCharacteristicRespVO::class.java)
+                    val resp: LoginCharacteristicRespVO? =
+                        it.toBean(LoginCharacteristicRespVO::class.java)
                     Token(resp?.token!!, 0).saveToSp(MyApplication.instance!!.applicationContext)
                     callBack.invoke(true)
                 } ?: run {
@@ -986,7 +1078,11 @@ object NetApi {
     /**
      * 查询基础数据-分页
      */
-    fun getSystemAttributePage(pages: Int, size: Int, callBack: (SystemAttributePageRespVO?) -> Unit) {
+    fun getSystemAttributePage(
+        pages: Int,
+        size: Int,
+        callBack: (SystemAttributePageRespVO?) -> Unit
+    ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.SYSTEM_ATTRIBUTE_PAGE,
             false,
@@ -1021,4 +1117,77 @@ object NetApi {
             }, isGet = true, isAuth = true
         )
     }
+
+    /**
+     * 批量更新硬件状态
+     */
+    fun updateHardwareEsStatus(
+        jobCardExDTOList: List<JobCardExDTO>,
+        keyExDTOList: List<KeyExDTO>,
+        lockExDTOList: List<LockExDTO>,
+        callBack: (Boolean) -> Unit
+    ) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.UPDATE_HARDWARE_ES_STATUS,
+            false,
+            mapOf(
+                "jobCardExDTOList" to jobCardExDTOList,
+                "keyExDTOList" to keyExDTOList,
+                "lockExDTOList" to lockExDTOList
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 重合点位数据解锁
+     */
+    fun updateCoincideToUnLock(
+        noUnlockTicketPointsVOSet: List<TicketDetailRespVO.JobTicketPointsVO>,
+        callBack: (Boolean) -> Unit
+    ) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.UPDATE_COINCIDE_TO_UNLOCK,
+            false,
+            mapOf(
+                "noUnlockTicketPointsVOSet" to noUnlockTicketPointsVOSet
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 获取锁柜机柜-仓位-分页
+     */
+    fun getIsLockCabinetSlotsPage(
+        pages: Int,
+        size: Int,
+        callBack: (CabinetSlotsRespVo?) -> Unit
+    ) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_LOCK_CABINET_SLOTS_PAGE,
+            false,
+            mapOf(
+                "pages" to pages,
+                "size" to size
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack(getRefBean(it))
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
 }

+ 1 - 34
app/src/main/java/com/grkj/iscs/util/NetHttpManager.kt

@@ -80,10 +80,6 @@ class NetHttpManager {
                     itPreChain.proceed()
                     return@addSerialPreprocessor
                 }
-//                requestTokenAndRefreshIfExpired {
-//                    itPreChain.task.addHeader("Authorization", it)
-//                    itPreChain.proceed()
-//                }
                 itPreChain.task.addHeader("Authorization", Token.fromSp(context!!)?.token)
                 itPreChain.proceed()
             }
@@ -119,30 +115,6 @@ class NetHttpManager {
             ToastUtils.tip(context!!.resources.getString(R.string.please_login))
             return
         }
-//        myHttp.async(LOGIN_CARD)
-//            .skipPreproc()
-//            .addBodyPara("cardNfc", loginUser.cardNfc)
-//            .nextOnIO()
-//            .setOnResponse {
-//                exceptionCount = 0
-//                if (!it.isSuccessful) {
-//                    callback(null)
-//                    return@setOnResponse
-//                }
-//                try {
-//                    val newToken = it.body.toBean(Token::class.java)
-//                    newToken.saveToSp(context!!)
-//                    callback(newToken.token)
-//                } catch (e: Exception) {
-//                    callback(null)
-//                    return@setOnResponse
-//                }
-//            }
-//            .setOnException {
-//                evictHttpConnectPool(it)
-//                callback(null)
-//            }
-//            .post()
         callback(null)
     }
 
@@ -188,10 +160,8 @@ class NetHttpManager {
                             }
                             val bobyStr = it.body.toString()
                             callback(
-                                null, if (bobyStr.isEmpty()) {
+                                null, bobyStr.ifEmpty {
                                     it.toString()
-                                } else {
-                                    bobyStr
                                 }, it.status
                             )
                         }
@@ -229,9 +199,6 @@ class NetHttpManager {
                 mapperCallBack.invoke(it)
             }
         }
-//            .setOnResMapper {
-//                mapperCallBack?.invoke(it)
-//            }
         if (isGet) {
             httpTask.addUrlPara(bodyParas)
             fileList?.forEach {

+ 23 - 16
app/src/main/java/com/grkj/iscs/util/SPUtils.kt

@@ -43,6 +43,8 @@ object SPUtils {
     private const val KEY_SYSTEM_ATTRIBUTE = "system_attribute"
     private const val KEY_BASE_URL = "base_url"
 
+    private const val KEY_TICKET_TAKE_LOCK_EXCEPTION = "ticket_take_lock_exception"
+
     fun getLoginUser(context: Context): LoginUserBO? {
         val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
         if (sp.getLong(KEY_LOGIN_USER_USER_ID, -1) == -1L) {
@@ -231,22 +233,6 @@ object SPUtils {
         return list.find { it.sysAttrKey == key }?.sysAttrValue
     }
 
-    fun saveBaseUrl(context: Context, url: String) {
-        val sp = context.getSharedPreferences(SP_URL, Context.MODE_PRIVATE)
-        val edit = sp.edit()
-        edit.putString(KEY_BASE_URL, url)
-        edit.commit()
-    }
-
-    fun getBaseUrl(context: Context): String {
-        val sp = context.getSharedPreferences(SP_URL, Context.MODE_PRIVATE)
-        val spUrl = sp.getString(KEY_BASE_URL, null)
-        if (spUrl.isNullOrEmpty()) {
-            return UrlConsts.BASE_URL
-        }
-        return spUrl
-    }
-
     /**
      * 取出钥匙
      */
@@ -267,4 +253,25 @@ object SPUtils {
     fun returnKey(ticketId: Long) {
         MMKV.defaultMMKV().remove("${ticketId}_TakeKey")
     }
+
+    /**
+     * 保存作业获取锁异常状态
+     */
+    fun saveTicketTakeLockException(ticketId: Long) {
+        "${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}".saveMMKVData(true)
+    }
+
+    /**
+     * 获取作业获取锁异常状态
+     */
+    fun getTicketTakeLockException(ticketId: Long): Boolean {
+        return "${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}".getMMKVData(false)
+    }
+
+    /**
+     * 重置作业获取锁异常状态
+     */
+    fun resetTicketTakeLockException(ticketId: Long) {
+        MMKV.defaultMMKV().remove("${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}")
+    }
 }

+ 2 - 0
app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt

@@ -30,6 +30,7 @@ import com.grkj.iscs.view.fragment.DockTestFragment
 import com.grkj.iscs.view.fragment.ExceptionReportFragment
 import com.grkj.iscs.view.fragment.JobManagementFragment
 import com.grkj.iscs.view.fragment.SettingFragment
+import com.grkj.iscs.view.fragment.SwitchStatusFragment
 import com.grkj.iscs.view.fragment.SystemSettingFragment
 import com.grkj.iscs.view.iview.IHomeView
 import com.grkj.iscs.view.presenter.HomePresenter
@@ -73,6 +74,7 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
 //        mMenuList.add(Menu(getString(R.string.system_setting), R.mipmap.menu_icon_sys_setting, SystemSettingFragment()))
 
         mMenuList.add(Menu(getString(R.string.device_status), R.mipmap.menu_icon_device_status, DeviceStatusFragment()))
+        mMenuList.add(Menu(getString(R.string.switch_status), R.mipmap.menu_icon_device_status, SwitchStatusFragment()))
         mMenuList.add(Menu(getString(R.string.exception_report), R.mipmap.exception_handle, ExceptionReportFragment()))
         mMenuList.add(Menu(getString(R.string.settings), R.mipmap.settings, SettingFragment()))
 

+ 2 - 0
app/src/main/java/com/grkj/iscs/view/dialog/LoginDialog.kt

@@ -119,6 +119,8 @@ class LoginDialog(
                 }
             }
             return true // 消费 HID 事件
+        } else if (isHidInput) {
+            return true
         }
 
         // 非 HID 情况,正常让 EditText 或 Activity 处理

+ 151 - 16
app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt

@@ -1,7 +1,11 @@
 package com.grkj.iscs.view.fragment
 
 import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
+import com.grkj.iscs.BusinessManager
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDeviceStatusBinding
 import com.grkj.iscs.extentions.setSelected
@@ -9,19 +13,25 @@ import com.grkj.iscs.extentions.setVisibleWithHolder
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
+import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
 import com.grkj.iscs.view.iview.IDeviceStatusView
 import com.grkj.iscs.view.presenter.DeviceStatusPresenter
+import com.sik.sikcore.thread.ThreadUtils
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.MultiItemTypeAdapter
 import com.zhy.adapter.recyclerview.base.ItemViewDelegate
 import com.zhy.adapter.recyclerview.base.ViewHolder
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
 
 /**
  * 硬件状态页
  */
-class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPresenter, FragmentDeviceStatusBinding>() {
+class DeviceStatusFragment :
+    BaseMvpFragment<IDeviceStatusView, DeviceStatusPresenter, FragmentDeviceStatusBinding>() {
     private var mRowList = mutableListOf<DockStatusBO>()
 
     override val viewBinding: FragmentDeviceStatusBinding
@@ -31,8 +41,14 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         presenter?.initData(mRowList)
 
         val adapter = MultiItemTypeAdapter(requireContext(), mRowList)
-        adapter.addItemViewDelegate(KeyDockItemDelegate(presenter))
-        adapter.addItemViewDelegate(LockDockItemDelegate(presenter, requireContext()))
+        adapter.addItemViewDelegate(KeyDockItemDelegate(requireContext(), presenter))
+        adapter.addItemViewDelegate(
+            LockDockItemDelegate(
+                requireContext(),
+                presenter,
+                requireContext()
+            )
+        )
         adapter.addItemViewDelegate(EmptyItemDelegate())
         mBinding?.rvDock?.adapter = adapter
     }
@@ -40,6 +56,17 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
     override fun onResume() {
         super.onResume()
         mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+        fun refreshBuckleStatus() {
+            ThreadUtils.runOnIODelayed(1000) {
+                BusinessManager.updateAllBuckleStatus {
+                    ThreadUtils.runOnMain {
+                        mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+                        refreshBuckleStatus()
+                    }
+                }
+            }
+        }
+        refreshBuckleStatus()
     }
 
     override fun initPresenter(): DeviceStatusPresenter {
@@ -51,7 +78,13 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         val dockList: MutableList<DockTestBean>
     )
 
-    class KeyDockItemDelegate(var presenter: DeviceStatusPresenter?): ItemViewDelegate<DockStatusBO> {
+    class KeyDockItemDelegate(var context: Context, var presenter: DeviceStatusPresenter?) :
+        ItemViewDelegate<DockStatusBO> {
+        private val statusCloseTintColor =
+            ContextCompat.getColor(context, R.color.common_status_red)
+        private val statusOpenTintColor =
+            ContextCompat.getColor(context, R.color.common_status_green)
+
         override fun getItemViewLayoutId(): Int {
             return R.layout.item_rv_key_dock_status
         }
@@ -59,14 +92,30 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         override fun convert(holder: ViewHolder?, row: DockStatusBO, position: Int) {
             holder?.setVisibleWithHolder(R.id.ll_left, row.dockList.any { it.column == "1" })
             holder?.setVisibleWithHolder(R.id.ll_right, row.dockList.any { it.column == "2" })
-            holder?.setSelected(R.id.iv_key_1, ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, true))
-            holder?.setSelected(R.id.iv_key_2, ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, false))
-            holder?.setSelected(R.id.iv_key_3, ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, true))
-            holder?.setSelected(R.id.iv_key_4, ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, false))
-            val status1 = presenter?.getKeyStatus(row.dockList.find { it.column == "1" }?.address, true)
-            val status2 = presenter?.getKeyStatus(row.dockList.find { it.column == "1" }?.address, false)
-            val status3 = presenter?.getKeyStatus(row.dockList.find { it.column == "2" }?.address, true)
-            val status4 = presenter?.getKeyStatus(row.dockList.find { it.column == "2" }?.address, false)
+            holder?.setSelected(
+                R.id.iv_key_1,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, true)
+            )
+            holder?.setSelected(
+                R.id.iv_key_2,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, false)
+            )
+            holder?.setSelected(
+                R.id.iv_key_3,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, true)
+            )
+            holder?.setSelected(
+                R.id.iv_key_4,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, false)
+            )
+            val status1 =
+                presenter?.getKeyStatus(row.dockList.find { it.column == "1" }?.address, true)
+            val status2 =
+                presenter?.getKeyStatus(row.dockList.find { it.column == "1" }?.address, false)
+            val status3 =
+                presenter?.getKeyStatus(row.dockList.find { it.column == "2" }?.address, true)
+            val status4 =
+                presenter?.getKeyStatus(row.dockList.find { it.column == "2" }?.address, false)
             holder?.setSelected(R.id.rl_status_1, status1?.first == true)
             holder?.setSelected(R.id.rl_status_2, status2?.first == true)
             holder?.setSelected(R.id.rl_status_3, status3?.first == true)
@@ -79,6 +128,38 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
             holder?.setVisibleWithHolder(R.id.tv_repair_2, status2?.first == false)
             holder?.setVisibleWithHolder(R.id.tv_repair_3, status3?.first == false)
             holder?.setVisibleWithHolder(R.id.tv_repair_4, status4?.first == false)
+            holder?.getView<View>(R.id.v_buckle_status_1)?.backgroundTintList =
+                if (presenter?.getKeyBuckleLockEnabled(
+                        row.dockList.find { it.column == "1" }?.address,
+                        true
+                    ) == true
+                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+                    statusOpenTintColor
+                )
+            holder?.getView<View>(R.id.v_buckle_status_2)?.backgroundTintList =
+                if (presenter?.getKeyBuckleLockEnabled(
+                        row.dockList.find { it.column == "1" }?.address,
+                        false
+                    ) == true
+                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+                    statusOpenTintColor
+                )
+            holder?.getView<View>(R.id.v_buckle_status_3)?.backgroundTintList =
+                if (presenter?.getKeyBuckleLockEnabled(
+                        row.dockList.find { it.column == "2" }?.address,
+                        true
+                    ) == true
+                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+                    statusOpenTintColor
+                )
+            holder?.getView<View>(R.id.v_buckle_status_4)?.backgroundTintList =
+                if (presenter?.getKeyBuckleLockEnabled(
+                        row.dockList.find { it.column == "2" }?.address,
+                        false
+                    ) == true
+                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+                    statusOpenTintColor
+                )
             holder?.setOnClickListener(R.id.tv_repair_1) {
                 presenter?.repairKey(row.dockList.find { it.column == "1" }?.address, true)
             }
@@ -91,6 +172,22 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
             holder?.setOnClickListener(R.id.tv_repair_4) {
                 presenter?.repairKey(row.dockList.find { it.column == "2" }?.address, false)
             }
+            holder?.setOnLongClickListener(R.id.iv_key_1) {
+                ToastUtils.tip("钥匙1上报异常")
+                true
+            }
+            holder?.setOnLongClickListener(R.id.iv_key_2) {
+                ToastUtils.tip("钥匙2上报异常")
+                true
+            }
+            holder?.setOnLongClickListener(R.id.iv_key_3) {
+                ToastUtils.tip("钥匙3上报异常")
+                true
+            }
+            holder?.setOnLongClickListener(R.id.iv_key_4) {
+                ToastUtils.tip("钥匙4上报异常")
+                true
+            }
         }
 
         override fun isForViewType(item: DockStatusBO?, position: Int): Boolean {
@@ -98,7 +195,18 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         }
     }
 
-    class LockDockItemDelegate(var presenter: DeviceStatusPresenter?, var ctx: Context): ItemViewDelegate<DockStatusBO> {
+    class LockDockItemDelegate(
+        var context: Context,
+        var presenter: DeviceStatusPresenter?,
+        var ctx: Context
+    ) : ItemViewDelegate<DockStatusBO> {
+        private val statusCloseTintColor =
+            ContextCompat.getColor(context, R.color.common_status_red)
+        private val statusOpenTintColor =
+            ContextCompat.getColor(context, R.color.common_status_green)
+        private val statusNotLightTintColor =
+            ContextCompat.getColor(context, R.color.common_status_not_light)
+
         override fun getItemViewLayoutId(): Int {
             return R.layout.item_rv_lock_dock_status
         }
@@ -106,9 +214,36 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         override fun convert(holder: ViewHolder?, row: DockStatusBO, position: Int) {
             val rv = holder?.getView<RecyclerView>(R.id.rv_root)
             rv?.adapter = object :
-                CommonAdapter<Int>(ctx, R.layout.item_rv_lock_dock_child_status, row.dockList[0].deviceList) {
+                CommonAdapter<Int>(
+                    ctx,
+                    R.layout.item_rv_lock_dock_child_status,
+                    row.dockList[0].deviceList
+                ) {
                 override fun convert(holder: ViewHolder?, lockIdx: Int, position: Int) {
-                    holder?.setSelected(R.id.root, ModBusController.isLockExist(row.dockList[0].address, lockIdx))
+                    holder?.setSelected(
+                        R.id.root,
+                        ModBusController.isLockExist(row.dockList[0].address, lockIdx)
+                    )
+                    holder?.convertView?.setOnLongClickListener {
+                        ToastUtils.tip("锁${row.dockList[0].row},${row.dockList[0].column},${lockIdx}上报异常")
+                        true
+                    }
+                    ColorStateList.valueOf(statusNotLightTintColor).let {
+                        holder?.getView<View>(R.id.v_buckle_status_close)?.backgroundTintList = it
+                        holder?.getView<View>(R.id.v_buckle_status_open)?.backgroundTintList = it
+                    }
+                    (presenter?.getLockBuckleLockEnabled(
+                        row.dockList[0].address,
+                        lockIdx
+                    ) == true).let {
+                        if (it) {
+                            holder?.getView<View>(R.id.v_buckle_status_close)?.backgroundTintList =
+                                ColorStateList.valueOf(statusCloseTintColor)
+                        } else {
+                            holder?.getView<View>(R.id.v_buckle_status_open)?.backgroundTintList =
+                                ColorStateList.valueOf(statusOpenTintColor)
+                        }
+                    }
                 }
             }
         }
@@ -118,7 +253,7 @@ class DeviceStatusFragment : BaseMvpFragment<IDeviceStatusView, DeviceStatusPres
         }
     }
 
-    class EmptyItemDelegate: ItemViewDelegate<DockStatusBO> {
+    class EmptyItemDelegate : ItemViewDelegate<DockStatusBO> {
         override fun getItemViewLayoutId(): Int {
             return R.layout.item_rv_empty_dock_status
         }

+ 45 - 8
app/src/main/java/com/grkj/iscs/view/fragment/JobProgressFragment.kt

@@ -34,6 +34,7 @@ class JobProgressFragment(val goBack: () -> Unit) :
     private val mUserList = mutableListOf<TicketDetailMonitorRespVO.IsJobTicketUser>()
     private var mTicketDetail: TicketDetailRespVO? = null
     private var mStep = 0
+    private var mCurrentStepId: Long? = null
     private var mPageChangeBO: PageChangeBO? = null
     private var mTipDialog: TipDialog? = null
     private lateinit var observer: Observer<MsgEvent>
@@ -121,21 +122,55 @@ class JobProgressFragment(val goBack: () -> Unit) :
             } else if (mStep == 7) {
                 val checkResult = presenter?.checkUnlock(requireContext(), mUserList)
                 if (checkResult?.isEmpty() == true) {
-                    if (SPUtils.isKeyTake(mPageChangeBO?.ticketId!!)) {
+                    //如果所有隔离点都有交叉作业,弹窗提醒确认就结束
+                    if (mTicketDetail?.ticketPointsVOList?.all {
+                            it.pointId in (mTicketDetail?.noUnlockTicketPointsVOSet?.map { it.pointId }
+                                ?: mutableListOf())
+                        } == true) {
                         if (mTipDialog == null) {
                             mTipDialog = TipDialog(requireContext())
                         }
-                        mTipDialog?.setTip(getString(R.string.take_one_more_key_hint))
+                        mTipDialog?.setTip(getString(R.string.all_point_have_other_job_not_finish))
                         mTipDialog?.setType(TipDialog.TYPE_HINT)
                         mTipDialog?.setConfirmListener {
-                            BusinessManager.sendLoadingEventMsg(getString(R.string.system_is_processing))
-                            presenter?.handleUnlockProcess(mPageChangeBO?.ticketId!!)
+                            mTicketDetail?.noUnlockTicketPointsVOSet?.let { noUnlockTicketPointsVOSet ->
+                                presenter?.updateCoincideToUnLock(noUnlockTicketPointsVOSet) {
+                                    mCurrentStepId?.let {
+                                        presenter?.updateStep(it, "1") {
+                                            goBack()
+                                        }
+                                    }
+                                        ?: ToastUtils.tip(getString(R.string.current_step_can_not_be_process))
+                                }
+                            }
                         }
                         mTipDialog?.showCancelCountdown(10)
                     } else {
-                        BusinessManager.sendLoadingEventMsg(getString(R.string.system_is_processing))
-                        presenter?.handleUnlockProcess(mPageChangeBO?.ticketId!!)
-                        SPUtils.takeKey(mPageChangeBO?.ticketId!!)
+                        if (SPUtils.isKeyTake(mPageChangeBO?.ticketId!!)) {
+                            if (mTipDialog == null) {
+                                mTipDialog = TipDialog(requireContext())
+                            }
+                            mTipDialog?.setTip(getString(R.string.take_one_more_key_hint))
+                            mTipDialog?.setType(TipDialog.TYPE_HINT)
+                            mTipDialog?.setConfirmListener {
+                                BusinessManager.sendLoadingEventMsg(getString(R.string.system_is_processing))
+                                if (mTicketDetail?.noUnlockTicketPointsVOSet?.isNotEmpty() == true) {
+                                    mTicketDetail?.noUnlockTicketPointsVOSet?.let { noUnlockTicketPointsVOSet ->
+                                        presenter?.updateCoincideToUnLock(noUnlockTicketPointsVOSet)
+                                    }
+                                }
+                                presenter?.handleUnlockProcess(mPageChangeBO?.ticketId!!)
+                            }
+                            mTipDialog?.showCancelCountdown(10)
+                        } else {
+                            BusinessManager.sendLoadingEventMsg(getString(R.string.system_is_processing))
+                            if (mTicketDetail?.noUnlockTicketPointsVOSet?.isNotEmpty() == true) {
+                                mTicketDetail?.noUnlockTicketPointsVOSet?.let { noUnlockTicketPointsVOSet ->
+                                    presenter?.updateCoincideToUnLock(noUnlockTicketPointsVOSet)
+                                }
+                            }
+                            presenter?.handleUnlockProcess(mPageChangeBO?.ticketId!!)
+                        }
                     }
                 } else {
                     ToastUtils.tip(checkResult)
@@ -230,7 +265,7 @@ class JobProgressFragment(val goBack: () -> Unit) :
                 //刷卡处理共锁的位置调整步骤主要在这块
                 if (presenter?.canHandleColockerLock(
                         requireContext(),
-                        mStep
+                        mStep, mTicketDetail
                     ) == true
                 ) {
                     if (presenter?.canHandleColockerUnlock(
@@ -299,6 +334,8 @@ class JobProgressFragment(val goBack: () -> Unit) :
             it?.filter { it.stepStatus == "1" }?.maxByOrNull { it.stepIndex!! }?.stepIndex?.let {
                 mStep = it
             }
+            mCurrentStepId =
+                it?.filter { it.stepStatus == "0" }?.minByOrNull { it.stepIndex!! }?.stepId
             handleActionBtnVisibility()
             callback?.invoke()
         }

+ 135 - 75
app/src/main/java/com/grkj/iscs/view/fragment/StepFragment.kt

@@ -48,6 +48,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
     private var mTicketDetailData: TicketDetailRespVO? = null
     private lateinit var observer: Observer<MsgEvent>
     private var mCanHandle: Boolean? = null // 是否可以操作,创建人、上锁人至少符合一个才可操作
+    private var mapRatio: Float = 1f
 
     override val viewBinding: FragmentStepBinding
         get() = FragmentStepBinding.inflate(layoutInflater)
@@ -72,10 +73,8 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                             .setCardBackgroundColor(requireContext().getColor(R.color.item_rv_step_bg_done))
                     } else {
                         holder.getView<MaterialCardView>(R.id.cv_step).setCardBackgroundColor(
-                            if (mStep + 1 == step.index)
-                                requireContext().getColor(R.color.item_rv_step_bg_doing)
-                            else
-                                requireContext().getColor(R.color.common_bg_white_10)
+                            if (mStep + 1 == step.index) requireContext().getColor(R.color.item_rv_step_bg_doing)
+                            else requireContext().getColor(R.color.common_bg_white_10)
                         )
                     }
 
@@ -89,8 +88,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                     holder.getView<ImageView>(R.id.iv_status)
                         .setImageResource(if (step.stepDetail?.stepStatus == "1") R.mipmap.step_executed else R.mipmap.step_not_executed)
                     holder.setText(
-                        R.id.tv_status,
-                        if (step.stepDetail?.stepStatus == "1") {
+                        R.id.tv_status, if (step.stepDetail?.stepStatus == "1") {
                             if (step.index == 3) getString(R.string.allocated) else getString(R.string.executed)
                         } else {
                             if (step.index == 3) getString(R.string.not_allocated) else getString(R.string.not_executed)
@@ -145,8 +143,11 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             if (mStep >= 4) {
                 changePage(
                     PageChangeBO(
-                        2, mChangePage?.workstationId, mChangePage?.ticketId,
-                        mChangePage?.machineryId, mChangePage?.machineryName
+                        2,
+                        mChangePage?.workstationId,
+                        mChangePage?.ticketId,
+                        mChangePage?.machineryId,
+                        mChangePage?.machineryName
                     )
                 )
             }
@@ -183,6 +184,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                     }
                 })
                 mBinding?.mapview?.addLayer(stationLayer)
+                stationLayer?.setRatio(mapRatio)
                 mBinding?.mapview?.refresh()
             }
 
@@ -237,8 +239,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
         }
 
         presenter?.getMachineryDetail(
-            pageChangeBO.machineryId!!,
-            {
+            pageChangeBO.machineryId!!, {
                 mMachineryDetail = it
                 Glide.with(this).load(it?.machineryImg).into(mBinding?.ivMachinery!!)
             }) { itList ->
@@ -250,43 +251,73 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             if (mLotoList.isNotEmpty()) {
                 mLotoList[0].mapId?.let { itId ->
                     presenter?.getMapInfo(itId) { itMapInfo ->
-                        itMapInfo?.imageUrl ?: return@getMapInfo
-                        BitmapUtil.loadBitmapFromUrl(requireContext(), itMapInfo.imageUrl) {
-                            if (it == null) {
+                        // 如果没有图 URL,直接返回
+                        val imageUrl = itMapInfo?.imageUrl ?: return@getMapInfo
+
+                        BitmapUtil.loadBitmapFromUrl(requireContext(), imageUrl) { mapBmp ->
+                            if (mapBmp == null) {
                                 LogUtil.e("Map pic is null")
                                 return@loadBitmapFromUrl
                             }
-                            mMapPicWidth = it.width
+
+                            // 清空旧点
                             mStationList.clear()
-                            val location = IntArray(2)
-                            mBinding?.mapview?.getLocationOnScreen(location)
-                            itMapInfo.pointList?.forEach { itPoint ->
-                                val locationX =
-                                    (itPoint.x!!.toFloat() - (itMapInfo.x!!.toFloat() / 50)) * 50 / itMapInfo.width!!.toFloat() * it.width
-                                val locationY =
-                                    (itPoint.y!!.toFloat() + 1 - itMapInfo.y!!.toFloat() / 50) * 50 / itMapInfo.height!!.toFloat() * it.height
+
+                            // 1 格 对应的像素
+                            val cellPx = 50f
+                            // 后端给的“逻辑”子图原始尺寸(像素)
+                            val backendW = itMapInfo.width!!.toFloat()
+                            val backendH = itMapInfo.height!!.toFloat()
+                            // 实际下载回来的 Bitmap 尺寸(像素)
+                            val actualW = mapBmp.width.toFloat()
+                            val actualH = mapBmp.height.toFloat()
+                            // 计算缩放比例
+                            val ratioX = actualW / backendW
+                            val ratioY = actualH / backendH
+                            mapRatio = ratioX
+                            // 子图在全局坐标系里的左上角偏移(像素)
+                            val offsetX = itMapInfo.x!!.toFloat()
+                            val offsetY = itMapInfo.y!!.toFloat()
+                            // 图标请求尺寸:逻辑 45px * 缩放比
+                            val iconReqPx = (45f * ratioX).toInt().coerceAtLeast(1)
+
+                            itMapInfo.pointList?.forEach { pt ->
+                                // 1) 格数 → 全局像素
+                                val globalX = pt.x!!.toFloat() * cellPx
+                                val globalY = pt.y!!.toFloat() * cellPx
+                                // 2) 全局像素 - 子图偏移 = 子图内像素
+                                val localX = globalX - offsetX
+                                val localY = globalY - offsetY
+                                // 3) 再乘缩放比,得到真实 Bitmap 上的像素坐标
+                                val finalX = localX * ratioX
+                                val finalY = localY * ratioY
+                                // 异步加载点位图标,固定请求尺寸
                                 BitmapUtil.loadBitmapFromUrl(
                                     requireContext(),
-                                    itPoint.pointIcon!!,
-                                    reqWidth = 18,
-                                    reqHeight = 18
-                                ) { itBitmap ->
+                                    pt.pointIcon!!,
+                                    reqWidth = iconReqPx,
+                                    reqHeight = iconReqPx
+                                ) { bmpIcon ->
+                                    val icon = bmpIcon ?: BitmapUtil.getResizedBitmapFromMipmap(
+                                        requireContext(),
+                                        R.mipmap.ticket_type_placeholder,
+                                        iconReqPx,
+                                        iconReqPx
+                                    )
+
                                     mStationList.add(
                                         CustomStationLayer.IsolationPoint(
-                                            PointF(locationX, locationY),
-                                            itPoint.entityName!!,
-                                            itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(
-                                                requireContext(),
-                                                R.mipmap.ticket_type_placeholder,
-                                                18,
-                                                18
-                                            ),
-                                            itPoint.entityId!!,
-                                            mMachineryDetail?.pointIdList?.contains(itPoint.entityId) == true
+                                            PointF(finalX, finalY),
+                                            pt.entityName!!,
+                                            icon,
+                                            pt.entityId!!.toLong(),
+                                            mMachineryDetail?.pointIdList?.contains(pt.entityId) == true
                                         )
                                     )
+
+                                    // 全部点都加载完后,设置给 layer 并绘制
                                     if (mStationList.size == itMapInfo.pointList.size) {
-                                        mBinding?.mapview?.loadMap(it)
+                                        mBinding?.mapview?.loadMap(mapBmp)
                                     }
                                 }
                             }
@@ -337,8 +368,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
     private fun updateStep(step: Int) {
         val canContinue =
             presenter?.checkCanContinue(requireContext(), step, mTicketDetailData) ?: ""
-        if (canContinue.isEmpty()
-        ) {
+        if (canContinue.isEmpty()) {
             val str = when (step) {
                 4 -> getString(R.string.action_confirm_shut_down)
                 5 -> getString(R.string.action_confirm_lock)
@@ -347,54 +377,81 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                 8 -> getString(R.string.action_confirm_restore)
                 else -> ""
             }
-            mTipDialog.setTip(str)
-            mTipDialog.setType(TipDialog.TYPE_ALL)
-            mTipDialog.setConfirmListener {
-                BusinessManager.sendLoadingEventMsg(getString(R.string.is_processing_please_wait))
-                when (step) {
-                    4 -> {
-                        presenter?.updateStep(mStepList[2].stepDetail?.stepId!!, "1") {
-                            presenter?.updateStep(mStepList[3].stepDetail?.stepId!!, "1") {
-                                BusinessManager.sendLoadingEventMsg(null, false)
+            if (step == 8 && presenter?.checkCrossJobUnlockData(
+                    requireContext(), mTicketDetailData
+                ) == true
+            ) {
+                mTipDialog.setTip(getString(R.string.all_point_have_other_job_not_finish))
+                mTipDialog.setType(TipDialog.TYPE_HINT)
+                mTipDialog.setConfirmListener {
+                    mTicketDetailData?.noUnlockTicketPointsVOSet?.let {
+                        presenter?.updateCoincideToUnLock(it) {
+                            if (it) {
+                                presenter?.updateStep(
+                                    mStepList.find { it.index == step }?.stepDetail?.stepId!!, "1"
+                                ) {
+                                    mChangePage?.let {
+                                        refreshPage(it)
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+                mTipDialog.showCancelCountdown(10)
+            } else {
+                mTipDialog.setTip(str)
+                mTipDialog.setType(TipDialog.TYPE_ALL)
+                mTipDialog.setConfirmListener {
+                    BusinessManager.sendLoadingEventMsg(getString(R.string.is_processing_please_wait))
+                    when (step) {
+                        4 -> {
+                            presenter?.updateStep(mStepList[2].stepDetail?.stepId!!, "1") {
+                                presenter?.updateStep(mStepList[3].stepDetail?.stepId!!, "1") {
+                                    BusinessManager.sendLoadingEventMsg(null, false)
+                                    refreshPage(mChangePage!!)
+                                    if (presenter?.jumpJobProgressPageCheck(
+                                            requireContext(), step
+                                        ) == true
+                                    ) {
+                                        // 自动跳转
+                                        changePage(
+                                            PageChangeBO(
+                                                2,
+                                                mChangePage?.workstationId,
+                                                mChangePage?.ticketId,
+                                                mChangePage?.machineryId,
+                                                mChangePage?.machineryName
+                                            )
+                                        )
+                                    }
+                                }
+                            }
+                        }
+
+                        else -> {
+                            presenter?.updateStep(
+                                mStepList.find { it.index == step }?.stepDetail?.stepId!!, "1"
+                            ) {
                                 refreshPage(mChangePage!!)
                                 if (presenter?.jumpJobProgressPageCheck(
-                                        requireContext(),
-                                        step
+                                        requireContext(), step
                                     ) == true
                                 ) {
                                     // 自动跳转
                                     changePage(
                                         PageChangeBO(
-                                            2, mChangePage?.workstationId, mChangePage?.ticketId,
-                                            mChangePage?.machineryId, mChangePage?.machineryName
+                                            2,
+                                            mChangePage?.workstationId,
+                                            mChangePage?.ticketId,
+                                            mChangePage?.machineryId,
+                                            mChangePage?.machineryName
                                         )
                                     )
                                 }
                             }
                         }
                     }
-
-                    else -> {
-                        presenter?.updateStep(
-                            mStepList.find { it.index == step }?.stepDetail?.stepId!!,
-                            "1"
-                        ) {
-                            refreshPage(mChangePage!!)
-                            if (presenter?.jumpJobProgressPageCheck(
-                                    requireContext(),
-                                    step
-                                ) == true
-                            ) {
-                                // 自动跳转
-                                changePage(
-                                    PageChangeBO(
-                                        2, mChangePage?.workstationId, mChangePage?.ticketId,
-                                        mChangePage?.machineryId, mChangePage?.machineryName
-                                    )
-                                )
-                            }
-                        }
-                    }
                 }
             }
         } else {
@@ -404,8 +461,11 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                 presenter?.tipToJobProgressPageCheck(requireContext(), mStep) {
                     changePage(
                         PageChangeBO(
-                            2, mChangePage?.workstationId, mChangePage?.ticketId,
-                            mChangePage?.machineryId, mChangePage?.machineryName
+                            2,
+                            mChangePage?.workstationId,
+                            mChangePage?.ticketId,
+                            mChangePage?.machineryId,
+                            mChangePage?.machineryName
                         )
                     )
                 }

+ 20 - 0
app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt

@@ -0,0 +1,20 @@
+package com.grkj.iscs.view.fragment
+
+import com.grkj.iscs.databinding.FragmentSwitchStatusBinding
+import com.grkj.iscs.view.base.BaseMvpFragment
+import com.grkj.iscs.view.iview.ISwitchStatusView
+import com.grkj.iscs.view.presenter.SwitchStatusPresenter
+
+class SwitchStatusFragment :
+    BaseMvpFragment<ISwitchStatusView, SwitchStatusPresenter, FragmentSwitchStatusBinding>() {
+    override fun initPresenter(): SwitchStatusPresenter {
+        return SwitchStatusPresenter()
+    }
+
+    override val viewBinding: FragmentSwitchStatusBinding
+        get() = FragmentSwitchStatusBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        
+    }
+}

+ 6 - 0
app/src/main/java/com/grkj/iscs/view/iview/ISwitchStatusView.kt

@@ -0,0 +1,6 @@
+package com.grkj.iscs.view.iview
+
+import com.grkj.iscs.view.base.IView
+
+interface ISwitchStatusView:IView {
+}

+ 19 - 1
app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt

@@ -26,7 +26,10 @@ class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
         val dockConfigJson = SPUtils.getDockConfig(mContext!!)
         if (!dockConfigJson.isNullOrEmpty()) {
             val tempList: MutableList<DockTestBean> =
-                Gson().fromJson(dockConfigJson, object : TypeToken<MutableList<DockTestBean>>() {}.type)
+                Gson().fromJson(
+                    dockConfigJson,
+                    object : TypeToken<MutableList<DockTestBean>>() {}.type
+                )
             if (tempList.isNotEmpty()) {
                 tempList.forEach { dock ->
                     try {
@@ -78,6 +81,7 @@ class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
                 ToastUtils.tip(R.string.no_key_to_repair)
                 return
             }
+
             1 -> readyRfid(dockAddr, isLeft)
             2 -> getKeyInfo(dockAddr, isLeft, ModBusController.getKeyByDock(dockAddr, isLeft)?.rfid)
             3, 4, 5 -> {
@@ -129,4 +133,18 @@ class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
             }
         }
     }
+
+    /**
+     * 获取钥匙的锁仓状态
+     */
+    fun getKeyBuckleLockEnabled(address: Byte?, isLeft: Boolean): Boolean {
+        return ModBusController.getKeyBuckleLockEnabled(address, isLeft)
+    }
+
+    /**
+     * 获取挂锁的锁仓状态
+     */
+    fun getLockBuckleLockEnabled(address: Byte, lockIdx: Int): Any {
+        return ModBusController.getLockBuckleLockEnabled(address, lockIdx)
+    }
 }

+ 76 - 25
app/src/main/java/com/grkj/iscs/view/presenter/JobProgressPresenter.kt

@@ -82,6 +82,14 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
         }
     }
 
+    fun updateStep(stepId: Long, stepStatus: String, callBack: (Boolean) -> Unit) {
+        NetApi.updateStep(stepId, stepStatus) {
+            Executor.runOnMain {
+                callBack.invoke(it)
+            }
+        }
+    }
+
     /**
      * 检查是否可以继续执行后续操作(例:点击后不拿设备或者无法拿设备,又再次点击按钮)
      *
@@ -96,6 +104,7 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
      * 上锁人上锁流程
      */
     fun handleLockProcess(ticketId: Long) {
+        SPUtils.resetTicketTakeLockException(ticketId)
         NetApi.checkBeforeAction(ticketId, true) {
             if (!it) {
                 LogUtil.w("handleLockProcess check false")
@@ -139,35 +148,55 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
                                 && BusinessManager.mDeviceTakeList.none { it.deviceType == DEVICE_TYPE_KEY && it.ticketId == ticketId }
                     LogUtil.i("needLockCount = $needLockCount , readyLockCount = $readyLockCount, realCount = $realCount, isNeedKey = $isNeedKey")
                     BusinessManager.checkEquipCount(realCount, isNeedKey) { keyPair, lockMap ->
-                        if (keyPair == null) {
-                            Executor.runOnMain {
-                                val dialog = TipDialog(mContext!!)
-                                dialog.setTip(mContext!!.getString(R.string.no_key_available_dialog_tip))
-                                var state = 0
-                                dialog.setConfirmListener {
-                                    state = 1
-                                    handleGiveKey(ticketDetail, null, lockMap)
-                                }
-                                dialog.setOnCancelListener {
-                                    state = 2
-                                    BusinessManager.sendLoadingEventMsg(null, false)
-                                }
-                                dialog.setOnDismissListener {
-                                    if (state == 0) {
+                        if (needLockCount == 0) {
+                            ToastUtils.tip(R.string.all_point_already_locked)
+                            (keyPair?.second?.rfid ?: "VIRTUALKEY").let { keyNfc ->
+                                BusinessManager.handleVirtualKeyGive(ticketId, keyNfc) {
+                                    BusinessManager.handleVirtualKeyReturn(ticketId, keyNfc) {
                                         BusinessManager.sendLoadingEventMsg(null, false)
                                     }
                                 }
-                                dialog.show()
                             }
                         } else {
-                            if (needLockCount == 0) {
-                                ToastUtils.tip(R.string.all_point_already_locked)
-                                keyPair.second?.rfid?.let { keyNfc ->
-                                    BusinessManager.handleVirtualKeyGive(ticketId, keyNfc) {
-                                        BusinessManager.handleVirtualKeyReturn(ticketId, keyNfc) {
+                            if (lockMap.isEmpty()) {
+                                Executor.runOnMain {
+                                    val dialog = TipDialog(mContext!!)
+                                    dialog.setTip(mContext!!.getString(R.string.lock_is_not_enough_stop_issue_ticket))
+                                    var state = 0
+                                    dialog.setConfirmListener {
+                                        state = 1
+                                        BusinessManager.sendLoadingEventMsg(null, false)
+                                    }
+                                    dialog.setOnCancelListener {
+                                        state = 2
+                                        BusinessManager.sendLoadingEventMsg(null, false)
+                                    }
+                                    dialog.setOnDismissListener {
+                                        if (state == 0) {
+                                            BusinessManager.sendLoadingEventMsg(null, false)
+                                        }
+                                    }
+                                    dialog.show()
+                                }
+                            } else if (keyPair == null) {
+                                Executor.runOnMain {
+                                    val dialog = TipDialog(mContext!!)
+                                    dialog.setTip(mContext!!.getString(R.string.no_key_available_dialog_tip))
+                                    var state = 0
+                                    dialog.setConfirmListener {
+                                        state = 1
+                                        handleGiveKey(ticketDetail, null, lockMap)
+                                    }
+                                    dialog.setOnCancelListener {
+                                        state = 2
+                                        BusinessManager.sendLoadingEventMsg(null, false)
+                                    }
+                                    dialog.setOnDismissListener {
+                                        if (state == 0) {
                                             BusinessManager.sendLoadingEventMsg(null, false)
                                         }
                                     }
+                                    dialog.show()
                                 }
                             } else {
                                 handleGiveKey(ticketDetail, keyPair, lockMap)
@@ -272,7 +301,9 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
                             ticketId,
                             keyPair.second?.rfid!!
                         )
-
+                        ticketDetail.noUnlockTicketPointsVOSet?.let { noUnlockTicketPointsVOSet ->
+                            updateCoincideToUnLock(noUnlockTicketPointsVOSet)
+                        }
                         BusinessManager.getCurrentStatus(
                             5,
                             BusinessManager.getBleDeviceByMac(keyPair.second?.mac)!!.bleDevice
@@ -324,7 +355,8 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
         mPointList: MutableList<TicketDetailMonitorRespVO.IsJobTicketPointsVO>,
         mUserList: MutableList<TicketDetailMonitorRespVO.IsJobTicketUser>
     ): Boolean {
-        return IStepMode.getStepMode(context)?.showGoToUnlock(mTicketDetail,mPointList, mUserList) == true
+        return IStepMode.getStepMode(context)
+            ?.showGoToUnlock(mTicketDetail, mPointList, mUserList) == true
     }
 
     /**
@@ -341,8 +373,12 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
     /**
      * 是否共锁人可以上锁
      */
-    fun canHandleColockerLock(context: Context, mStep: Int): Boolean {
-        return IStepMode.getStepMode(context)?.canHandleColockerLock(mStep) == true
+    fun canHandleColockerLock(
+        context: Context,
+        mStep: Int,
+        mTicketDetail: TicketDetailRespVO?
+    ): Boolean {
+        return IStepMode.getStepMode(context)?.canHandleColockerLock(mStep, mTicketDetail) == true
     }
 
     /**
@@ -361,4 +397,19 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
     ): String? {
         return IStepMode.getStepMode(context)?.checkUnlock(mUserList)
     }
+
+
+    /**
+     * 重合点位数据解锁
+     */
+    fun updateCoincideToUnLock(
+        noUnlockTicketPointsVOSet: MutableList<TicketDetailRespVO.JobTicketPointsVO>,
+        callBack: ((Boolean) -> Unit)? = null
+    ) {
+        NetApi.updateCoincideToUnLock(noUnlockTicketPointsVOSet) {
+            Executor.runOnMain {
+                callBack?.invoke(it)
+            }
+        }
+    }
 }

+ 27 - 2
app/src/main/java/com/grkj/iscs/view/presenter/StepPresenter.kt

@@ -116,7 +116,32 @@ class StepPresenter : BasePresenter<IStepView>() {
     /**
      * 检查弹窗是否需要进入进度页面
      */
-    fun tipToJobProgressPageCheck(context: Context,mStep: Int, checked: () -> Unit) {
-        IStepMode.getStepMode(context)?.tipToJobProgressPageCheck(mStep,checked)
+    fun tipToJobProgressPageCheck(context: Context, mStep: Int, checked: () -> Unit) {
+        IStepMode.getStepMode(context)?.tipToJobProgressPageCheck(mStep, checked)
+    }
+
+    /**
+     * 检查当前步骤的解锁数据是否满足继续
+     * 主要是第8步的交叉作业的数据检查
+     */
+    fun checkCrossJobUnlockData(
+        context: Context,
+        mTicketDetailData: TicketDetailRespVO?
+    ): Boolean {
+        return IStepMode.getStepMode(context)?.checkCrossJobUnlockData(mTicketDetailData) == true
+    }
+
+    /**
+     * 重合点位数据解锁
+     */
+    fun updateCoincideToUnLock(
+        noUnlockTicketPointsVOSet: MutableList<TicketDetailRespVO.JobTicketPointsVO>,
+        callBack: (Boolean) -> Unit
+    ) {
+        NetApi.updateCoincideToUnLock(noUnlockTicketPointsVOSet) {
+            Executor.runOnMain {
+                callBack(it)
+            }
+        }
     }
 }

+ 7 - 0
app/src/main/java/com/grkj/iscs/view/presenter/SwitchStatusPresenter.kt

@@ -0,0 +1,7 @@
+package com.grkj.iscs.view.presenter
+
+import com.grkj.iscs.view.base.BasePresenter
+import com.grkj.iscs.view.iview.ISwitchStatusView
+
+class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
+}

+ 7 - 1
app/src/main/java/com/grkj/iscs/view/step_mode/IStepMode.kt

@@ -78,7 +78,7 @@ interface IStepMode {
     /**
      * 是否可以处理共锁人上锁
      */
-    fun canHandleColockerLock(mStep: Int): Boolean
+    fun canHandleColockerLock(mStep: Int, mTicketDetail: TicketDetailRespVO?): Boolean
 
     /**
      * 是否共锁人可以解锁
@@ -105,6 +105,12 @@ interface IStepMode {
      */
     fun getMinColockerSize(): Int
 
+    /**
+     * 检查当前步骤的解锁数据是否满足继续
+     * 主要是第8步的交叉作业的数据检查
+     */
+    fun checkCrossJobUnlockData(mTicketDetailData: TicketDetailRespVO?): Boolean
+
     companion object {
         /**
          * 根据存储的模式获取模式

+ 6 - 2
app/src/main/java/com/grkj/iscs/view/step_mode/StepMode1.kt

@@ -63,7 +63,7 @@ class StepMode1 : IStepMode {
         return mStep >= 6
     }
 
-    override fun canHandleColockerLock(mStep: Int): Boolean {
+    override fun canHandleColockerLock(mStep: Int, mTicketDetail: TicketDetailRespVO?): Boolean {
         return mStep == 6
     }
 
@@ -84,6 +84,10 @@ class StepMode1 : IStepMode {
     }
 
     override fun getMinColockerSize(): Int {
-        return  1
+        return 1
+    }
+
+    override fun checkCrossJobUnlockData(mTicketDetailData: TicketDetailRespVO?): Boolean {
+        return false
     }
 }

+ 15 - 5
app/src/main/java/com/grkj/iscs/view/step_mode/StepMode2.kt

@@ -12,6 +12,9 @@ import com.grkj.iscs.model.vo.user.UserListRespVO
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.log.LogUtil
 
+/**
+ * 玛氏流程
+ */
 class StepMode2 : IStepMode {
     override fun canModifyColocker(step: Int): Boolean {
         return step in 3..7
@@ -74,8 +77,8 @@ class StepMode2 : IStepMode {
         mPointList: MutableList<TicketDetailMonitorRespVO.IsJobTicketPointsVO>,
         mUserList: MutableList<TicketDetailMonitorRespVO.IsJobTicketUser>
     ): Boolean {
-        //交叉作业要检查非不可解锁的区域外的所有区域的锁状态
-        return !mPointList.filter { it.pointId !in (mTicketDetail?.noUnlockTicketPointsVOSet?.map { it.pointId }?: mutableListOf()) }.all { it.pointStatus == "2" } && mUserList.all { it.jobStatus == 5 }
+        //交叉作业要检查所有共锁解除已完成
+        return mUserList.all { it.jobStatus == 5 }
     }
 
     override fun colockerInsideCanAdd(user: UserListRespVO.Row, mStep: Int): Boolean {
@@ -138,8 +141,8 @@ class StepMode2 : IStepMode {
         return (mStep >= 4 && mPointList.all { it.pointStatus == "1" }) || mStep >= 6
     }
 
-    override fun canHandleColockerLock(mStep: Int): Boolean {
-        return mStep == 4 || mStep == 5
+    override fun canHandleColockerLock(mStep: Int, mTicketDetail: TicketDetailRespVO?): Boolean {
+        return (mStep == 4 || mStep == 5) && mTicketDetail?.ticketPointsVOList?.all { it.pointStatus == "1" } == true
     }
 
     override fun canHandleColockerUnlock(mStep: Int): Boolean {
@@ -147,7 +150,7 @@ class StepMode2 : IStepMode {
     }
 
     override fun checkUnlock(mUserList: MutableList<TicketDetailMonitorRespVO.IsJobTicketUser>): String {
-        return if (mUserList.all { it.jobStatus!! >= 5 } == true) {
+        return if (mUserList.all { it.jobStatus!! >= 5 }) {
             ""
         } else {
             ContextCompat.getString(
@@ -170,4 +173,11 @@ class StepMode2 : IStepMode {
     override fun getMinColockerSize(): Int {
         return 1
     }
+
+    override fun checkCrossJobUnlockData(mTicketDetailData: TicketDetailRespVO?): Boolean {
+        return mTicketDetailData?.ticketPointsVOList?.all {
+            it.pointId in (mTicketDetailData.noUnlockTicketPointsVOSet?.map { it.pointId }
+                ?: mutableListOf())
+        } == true
+    }
 }

+ 81 - 46
app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt

@@ -10,15 +10,16 @@ import android.util.Pair
 import android.view.MotionEvent
 import com.grkj.iscs.R
 import com.grkj.iscs.util.BitmapUtil
+import com.grkj.iscs.util.log.LogUtil
 import com.onlylemi.mapview.library.MapView
+import com.onlylemi.mapview.library.MapViewListener
 import com.onlylemi.mapview.library.layer.MapBaseLayer
 import java.util.Collections
 import kotlin.math.cos
 import kotlin.math.sin
 
 class CustomStationLayer @JvmOverloads constructor(
-    mapView: MapView?,
-    private var pointList: List<IsolationPoint> = mutableListOf()
+    mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
     private var listener: MarkIsClickListener? = null
     private var radiusMark = 0f
@@ -28,75 +29,103 @@ class CustomStationLayer @JvmOverloads constructor(
     private var btnIndex: Int = -1
     private var currentZoom = 0f
     private var currentDegree = 0f
-    private lateinit var bgBitmap: Bitmap
-    private lateinit var coverBitmap: Bitmap
+    private var bgBitmap: Bitmap? = null
+    private var coverBitmap: Bitmap? = null
+    private var ratio: Float = 1f
 
     init {
         initLayer()
     }
 
     private fun initLayer() {
-        radiusMark = setValue(4.0f)
-        bgBitmap = BitmapUtil.getResizedBitmapFromMipmap(mapView.context, R.mipmap.red_stroke_bg, 24, 32)
-        coverBitmap = BitmapUtil.getResizedBitmapFromDrawable(mapView.context, R.drawable.map_item_cover_bg, 24, 32)!!
-
+        radiusMark = setValue(6.0f)
         paint = Paint()
         paint.isAntiAlias = true
         paint.style = Paint.Style.FILL_AND_STROKE
     }
 
+    fun setRatio(ratio: Float) {
+        this.ratio = ratio
+        bgBitmap = BitmapUtil.getResizedBitmapFromDrawable(
+            mapView.context,
+            R.drawable.red_stroke_bg,
+            (50 * ratio).toInt(),
+            (78 * ratio).toInt()
+        )!!
+        coverBitmap = BitmapUtil.getResizedBitmapFromDrawable(
+            mapView.context,
+            R.drawable.map_item_cover_bg,
+            (50 * ratio).toInt(),
+            (78 * ratio).toInt()
+        )!!
+    }
+
     override fun onTouch(event: MotionEvent) {
 
     }
 
     override fun draw(
-        canvas: Canvas,
-        currentMatrix: Matrix,
-        currentZoom: Float,
-        currentRotateDegrees: Float
+        canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
     ) {
         this.currentZoom = currentZoom
         currentDegree = 360 - currentRotateDegrees
-        if (isVisible) {
-            canvas.save()
-            synchronized(pointList) {
-                pointList.forEach { point ->
-                    val mark = point.pos
-                    val goal = floatArrayOf(mark.x, mark.y)
-                    currentMatrix.mapPoints(goal)
-
-                    // 文字背景
-                    val width = paint.measureText(point.entityName)
-                    paint.style = Paint.Style.FILL
-                    paint.strokeWidth = 1.0f
-
-                    paint.color = Color.parseColor("#CCFF0000")
-                    paint.textSize = radiusMark
-                    // 先画背景再画文字防止文字被盖住
-                    canvas.drawBitmap(bgBitmap, goal[0] - bgBitmap.width / 2, goal[1] - 2 * radiusMark, paint)
-                    point.icon?.let {
-                        canvas.drawBitmap(it, goal[0] - it.width / 2, goal[1] - 2  * radiusMark, paint)
+
+        if (!isVisible) return
+
+        canvas.save()
+        // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
+        canvas.concat(currentMatrix)
+        pointList.forEach { point ->
+            // point.pos.x/y 已经是「图内像素坐标」
+            val x = point.pos.x
+            val y = point.pos.y
+            // 先画背景(它会被 currentMatrix 自动缩放)
+            bgBitmap?.let {
+                canvas.drawBitmap(
+                    it, x, y, paint
+                )
+                // 再画 icon
+                point.icon?.let { icon ->
+                    canvas.drawBitmap(
+                        icon,
+                        x + (it.width - icon.width) / 2,
+                        y + (it.width - icon.width) / 2,
+                        paint
+                    )
+                }
+                // 然后画文字
+                paint.style = Paint.Style.FILL
+                paint.strokeWidth = 1f
+                paint.color = Color.RED
+                paint.textSize = radiusMark * ratio  // 这里是「图内」的文字大小,后面会跟着缩放
+                val textW = paint.measureText(point.entityName)
+                canvas.drawText(
+                    point.entityName,
+                    x + (it.width - textW) / 2,
+                    y + (it.height - radiusMark / 2 - it.height / 10),
+                    paint
+                )
+
+                // 如果选中,再叠加一个标记
+                if (point.isSelected) {
+                    coverBitmap?.let {
+                        canvas.drawBitmap(
+                            it, x, y, paint
+                        )
                     }
-                    // 一直显示文字
+                    val checkW = paint.measureText("√")
+                    paint.color = Color.WHITE
                     canvas.drawText(
-                        point.entityName,
-                        goal[0] - width / 2,
-                        goal[1] + radiusMark / 3.0f,
+                        "√",
+                        x + (it.width - checkW) / 2,
+                        y + (it.height / 2 + radiusMark / 2),
                         paint
                     )
-                    if (point.isSelected) {
-                        canvas.drawBitmap(coverBitmap, goal[0] - bgBitmap.width / 2, goal[1] - 2 * radiusMark, paint)
-                        val selectTextWidth = paint.measureText("√")
-                        paint.color = Color.parseColor("#FFFFFF")
-                        canvas.drawText("√", goal[0] - selectTextWidth / 2, goal[1] - radiusMark / 2, paint)
-                    }
-
-                    // 定位点,仅调试用,不要显示
-//                    canvas.drawCircle(goal[0], goal[1], 2f, paint)
                 }
             }
-            canvas.restore()
         }
+
+        canvas.restore()
     }
 
     private fun rotatePoint(
@@ -120,5 +149,11 @@ class CustomStationLayer @JvmOverloads constructor(
         fun markIsClick(index: Int, btnIndex: Int)
     }
 
-    data class IsolationPoint(val pos: PointF, val entityName: String, val icon: Bitmap?, val entityId: Long, val isSelected: Boolean)
+    data class IsolationPoint(
+        val pos: PointF,
+        val entityName: String,
+        val icon: Bitmap?,
+        val entityId: Long,
+        val isSelected: Boolean
+    )
 }

+ 8 - 0
app/src/main/res/drawable/common_status_circle.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <size
+        android:width="@dimen/common_status_circle_small"
+        android:height="@dimen/common_status_circle_small" />
+    <solid android:color="@color/common_status_red" />
+</shape>

+ 7 - 0
app/src/main/res/drawable/divider_dock_lock_status.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="line">
+    <size
+        android:width="@dimen/divider_line_width"
+        android:height="20dp" />
+</shape>

+ 21 - 0
app/src/main/res/drawable/dock_has_lock.xml

@@ -0,0 +1,21 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="76dp"
+    android:height="182dp"
+    android:viewportWidth="76"
+    android:viewportHeight="182">
+    <path
+        android:fillColor="#000000"
+        android:pathData="M10,0L66,0A10,10 0,0 1,76 10L76,172A10,10 0,0 1,66 182L10,182A10,10 0,0 1,0 172L0,10A10,10 0,0 1,10 0z" />
+    <path
+        android:fillColor="#D9D9D9"
+        android:pathData=" M28,31
+        H48
+        A10,10 0 0 1 58,41
+        V141
+        A10,10 0 0 1 48,151
+        H28
+        A10,10 0 0 1 18,141
+        V41
+        A10,10 0 0 1 28,31
+        Z" />
+</vector>

+ 2 - 2
app/src/main/res/drawable/dock_lock_selector.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <selector xmlns:android="http://schemas.android.com/apk/res/android">
-    <item android:state_selected="false" android:drawable="@mipmap/dock_no_lock" />
-    <item android:state_selected="true" android:drawable="@mipmap/dock_has_lock" />
+    <item android:state_selected="false" android:drawable="@drawable/dock_no_lock" />
+    <item android:state_selected="true" android:drawable="@drawable/dock_has_lock" />
 </selector>

+ 9 - 0
app/src/main/res/drawable/dock_no_lock.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="76dp"
+    android:height="182dp"
+    android:viewportWidth="76"
+    android:viewportHeight="182">
+  <path
+      android:pathData="M10,0L66,0A10,10 0,0 1,76 10L76,172A10,10 0,0 1,66 182L10,182A10,10 0,0 1,0 172L0,10A10,10 0,0 1,10 0z"
+      android:fillColor="#000000"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/menu_icon_device_input.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="72dp"
+    android:height="91dp"
+    android:viewportWidth="72"
+    android:viewportHeight="91">
+  <path
+      android:pathData="M0.688,76.781V18.617C0.688,11.547 4.867,7.953 11.195,7.562C25.219,6.742 36.859,4.789 47.797,2.328C55.297,0.57 59.438,4.359 59.438,12.133V69.32C59.438,76.156 57.289,79.086 51.742,80.648C36.859,84.75 25.219,85.961 12.133,86.664C4.867,87.093 0.688,83.578 0.688,76.781ZM6.977,75.648C6.977,78.812 9.008,80.531 12.406,80.375C25.375,79.711 36.742,78.461 48.969,75.062C51.977,74.281 53.148,72.679 53.148,69.086V13.226C53.148,9.711 51.039,7.836 47.445,8.656C36.742,11.078 25.375,13.031 11.898,13.734C9.008,13.89 6.977,15.179 6.977,18.461V75.648ZM31.898,90.218C38.461,89.203 45.531,87.836 53.227,85.765C61.117,83.5 64.789,78.422 64.789,69.32V12.133C64.789,10.961 64.711,9.828 64.594,8.812C69.047,10.101 71.313,13.851 71.313,19.906V77.992C71.313,86.156 67.289,90.218 59.242,90.218H31.898ZM15.531,27.562C14.242,27.64 13.266,26.703 13.266,25.453C13.266,24.242 14.008,23.304 15.531,23.187C26.625,22.484 35.531,20.922 44.203,19.008C45.883,18.656 46.859,19.711 46.859,20.922C46.859,22.015 46.391,22.914 44.633,23.304C35.531,25.336 26.625,26.859 15.531,27.562ZM15.531,40.14C14.242,40.218 13.266,39.242 13.266,37.992C13.266,36.82 14.008,35.883 15.531,35.765C26.625,35.062 35.531,33.5 44.203,31.586C45.883,31.234 46.859,32.289 46.859,33.5C46.859,34.593 46.391,35.492 44.633,35.883C35.531,37.875 26.625,39.437 15.531,40.14ZM15.531,52.718C14.242,52.797 13.266,51.82 13.266,50.57C13.266,49.398 14.008,48.461 15.531,48.343C26.625,47.64 35.531,46.078 44.203,44.164C45.883,43.812 46.859,44.867 46.859,46.078C46.859,47.172 46.391,48.07 44.633,48.461C35.531,50.453 26.625,52.015 15.531,52.718ZM15.531,65.258C14.242,65.336 13.266,64.359 13.266,63.109C13.266,61.937 14.008,61 15.531,60.883C21.82,60.531 26,59.945 29.75,59.359C31.508,59.047 32.445,60.179 32.445,61.351C32.445,62.367 31.742,63.383 30.453,63.617C26.234,64.281 21.938,64.906 15.531,65.258Z"
+      android:fillColor="#D7D2D2"/>
+</vector>

+ 21 - 0
app/src/main/res/drawable/red_stroke_bg.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/white" />
+            <size
+                android:width="50dp"
+                android:height="78dp" />
+            <corners android:radius="2.5dp" />
+        </shape>
+    </item>
+    <item>
+        <shape android:shape="rectangle">
+            <stroke
+                android:width="1dp"
+                android:color="@color/common_status_red" />
+            <corners android:radius="2.5dp" />
+        </shape>
+    </item>
+</layer-list>

+ 14 - 0
app/src/main/res/layout/fragment_switch_status.xml

@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:background="@drawable/item_rv_technology_sop_bg_normal"
+    android:padding="@dimen/common_spacing"
+    tools:context=".view.fragment.SwitchStatusFragment">
+
+    <com.onlylemi.mapview.library.MapView
+        android:id="@+id/mapview"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent" />
+</RelativeLayout>

+ 57 - 15
app/src/main/res/layout/item_rv_key_dock_status.xml

@@ -5,7 +5,6 @@
     android:layout_height="wrap_content"
     android:layout_marginHorizontal="@dimen/common_spacing_small"
     android:layout_marginVertical="@dimen/common_spacing_smallest"
-    android:background="@color/white"
     android:orientation="horizontal"
     android:padding="@dimen/common_spacing_small">
 
@@ -13,26 +12,27 @@
         android:id="@+id/ll_left"
         android:layout_width="0dp"
         android:layout_height="wrap_content"
-        android:layout_weight="1"
-        android:orientation="horizontal">
+        android:layout_weight="1">
 
-        <LinearLayout
+        <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"
-            android:gravity="center"
             android:orientation="vertical">
 
             <ImageView
                 android:id="@+id/iv_key_1"
                 android:layout_width="50dp"
                 android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
                 android:background="@drawable/dock_key_selector" />
 
             <RelativeLayout
                 android:id="@+id/rl_status_1"
                 android:layout_width="50dp"
                 android:layout_height="20dp"
+                android:layout_below="@+id/iv_key_1"
+                android:layout_centerHorizontal="true"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
                 android:gravity="center">
@@ -46,16 +46,25 @@
             <TextView
                 android:id="@+id/tv_repair_1"
                 style="@style/CommonTextView"
+                android:layout_below="@+id/rl_status_1"
+                android:layout_alignLeft="@+id/rl_status_1"
+                android:layout_alignRight="@+id/rl_status_1"
                 android:background="@drawable/common_btn_red_bg"
                 android:text="@string/repair_key" />
-        </LinearLayout>
 
-        <LinearLayout
+            <View
+                android:id="@+id/v_buckle_status_1"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_1"
+                android:background="@drawable/common_status_circle" />
+        </RelativeLayout>
+
+        <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"
-            android:gravity="center"
-            android:orientation="vertical">
+            android:gravity="center_horizontal">
 
             <ImageView
                 android:id="@+id/iv_key_2"
@@ -67,6 +76,7 @@
                 android:id="@+id/rl_status_2"
                 android:layout_width="50dp"
                 android:layout_height="20dp"
+                android:layout_below="@+id/iv_key_2"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
                 android:gravity="center">
@@ -80,9 +90,19 @@
             <TextView
                 android:id="@+id/tv_repair_2"
                 style="@style/CommonTextView"
+                android:layout_below="@+id/rl_status_2"
+                android:layout_alignLeft="@+id/rl_status_2"
+                android:layout_alignRight="@+id/rl_status_2"
                 android:background="@drawable/common_btn_red_bg"
                 android:text="@string/repair_key" />
-        </LinearLayout>
+
+            <View
+                android:id="@+id/v_buckle_status_2"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_2"
+                android:background="@drawable/common_status_circle" />
+        </RelativeLayout>
     </LinearLayout>
 
     <LinearLayout
@@ -92,11 +112,11 @@
         android:layout_weight="1"
         android:orientation="horizontal">
 
-        <LinearLayout
+        <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"
-            android:gravity="center"
+            android:gravity="center_horizontal"
             android:orientation="vertical">
 
             <ImageView
@@ -109,6 +129,7 @@
                 android:id="@+id/rl_status_3"
                 android:layout_width="50dp"
                 android:layout_height="20dp"
+                android:layout_below="@+id/iv_key_3"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
                 android:gravity="center">
@@ -122,11 +143,21 @@
             <TextView
                 android:id="@+id/tv_repair_3"
                 style="@style/CommonTextView"
+                android:layout_below="@+id/rl_status_3"
+                android:layout_alignLeft="@+id/rl_status_3"
+                android:layout_alignRight="@+id/rl_status_3"
                 android:background="@drawable/common_btn_red_bg"
                 android:text="@string/repair_key" />
-        </LinearLayout>
 
-        <LinearLayout
+            <View
+                android:id="@+id/v_buckle_status_3"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_3"
+                android:background="@drawable/common_status_circle" />
+        </RelativeLayout>
+
+        <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
             android:layout_weight="1"
@@ -143,6 +174,7 @@
                 android:id="@+id/rl_status_4"
                 android:layout_width="50dp"
                 android:layout_height="20dp"
+                android:layout_below="@+id/iv_key_4"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
                 android:gravity="center">
@@ -156,8 +188,18 @@
             <TextView
                 android:id="@+id/tv_repair_4"
                 style="@style/CommonTextView"
+                android:layout_below="@+id/rl_status_4"
+                android:layout_alignLeft="@+id/rl_status_4"
+                android:layout_alignRight="@+id/rl_status_4"
                 android:background="@drawable/common_btn_red_bg"
                 android:text="@string/repair_key" />
-        </LinearLayout>
+
+            <View
+                android:id="@+id/v_buckle_status_4"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_4"
+                android:background="@drawable/common_status_circle" />
+        </RelativeLayout>
     </LinearLayout>
 </LinearLayout>

+ 34 - 6
app/src/main/res/layout/item_rv_lock_dock_child_status.xml

@@ -1,8 +1,36 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    android:id="@+id/root"
-    android:layout_width="30dp"
-    android:layout_height="90dp"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
     android:layout_marginHorizontal="@dimen/common_spacing_small"
-    android:background="@drawable/dock_lock_selector"
-    android:orientation="horizontal" />
+    android:orientation="horizontal">
+
+    <FrameLayout
+        android:id="@+id/root"
+        android:layout_width="20dp"
+        android:layout_height="70dp"
+        android:background="@drawable/dock_lock_selector" />
+
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginLeft="@dimen/divider_line_margin"
+        android:divider="@drawable/divider_dock_lock_status"
+        android:gravity="center"
+        android:orientation="vertical"
+        android:showDividers="middle">
+
+        <View
+            android:id="@+id/v_buckle_status_close"
+            android:layout_width="@dimen/common_status_circle_medium"
+            android:layout_height="@dimen/common_status_circle_medium"
+            android:background="@drawable/common_status_circle" />
+
+        <View
+            android:id="@+id/v_buckle_status_open"
+            android:layout_width="@dimen/common_status_circle_medium"
+            android:layout_height="@dimen/common_status_circle_medium"
+            android:background="@drawable/common_status_circle" />
+    </LinearLayout>
+</LinearLayout>
+

+ 1 - 1
app/src/main/res/layout/item_rv_lock_dock_status.xml

@@ -5,7 +5,6 @@
     android:layout_height="wrap_content"
     android:layout_marginHorizontal="@dimen/common_spacing_small"
     android:layout_marginVertical="@dimen/common_spacing_smallest"
-    android:background="@color/white"
     android:paddingHorizontal="@dimen/common_spacing_small">
 
     <androidx.recyclerview.widget.RecyclerView
@@ -14,5 +13,6 @@
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_centerHorizontal="true"
+        android:paddingVertical="@dimen/common_rv_tab_padding"
         android:orientation="horizontal" />
 </RelativeLayout>

BIN
app/src/main/res/mipmap/dock_has_lock.png


BIN
app/src/main/res/mipmap/dock_no_lock.png


+ 4 - 0
app/src/main/res/values-en/strings.xml

@@ -331,4 +331,8 @@
     <string name="take_one_more_key_hint">I have already taken out the key, would you like to take out another one</string>
     <string name="current_user_does_not_have_the_authority_to_colock">The current user does not have the authority to colock</string>
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">The current user does not have the authority to unlock colock</string>
+    <string name="all_point_have_other_job_not_finish">all point have other job not finish,do you want to continue?</string>
+    <string name="lock_is_not_enough_stop_issue_ticket">lock is not enough, stop issue ticket</string>
+    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="current_ticket_report_lock_take_exception_tip">current ticket report lock take exception, please return lock</string>
 </resources>

+ 4 - 0
app/src/main/res/values-zh/strings.xml

@@ -331,4 +331,8 @@
     <string name="take_one_more_key_hint">已有钥匙取出,是否再取一个</string>
     <string name="current_user_does_not_have_the_authority_to_colock">当前用户无权共锁</string>
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">当前用户无权解除共锁</string>
+    <string name="all_point_have_other_job_not_finish">所有隔离点存在其他作业未完成,是否继续拆锁恢复?</string>
+    <string name="lock_is_not_enough_stop_issue_ticket">锁具数量不足,停止下发作业票</string>
+    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="current_ticket_report_lock_take_exception_tip">当前作业挂锁上报异常,请归还挂锁</string>
 </resources>

+ 4 - 0
app/src/main/res/values/colors.xml

@@ -16,6 +16,10 @@
     <color name="common_bg_white_80">#CCFFFFFF</color>
     <color name="common_bg_white_90">#E6FFFFFF</color>
 
+    <color name="common_status_red">#FFFF0000</color>
+    <color name="common_status_green">#FF008000</color>
+    <color name="common_status_not_light">#FFD9D9D9</color>
+
     <color name="common_bg_black_30">#4D000000</color>
 
     <color name="common_light_gray">#CCCCCC</color>

+ 3 - 1
app/src/main/res/values/dimens.xml

@@ -13,6 +13,8 @@
     <dimen name="common_spacing_smallest">2dp</dimen>
     <dimen name="common_icon_size">15dp</dimen>
     <dimen name="common_icon_size_small">12dp</dimen>
+    <dimen name="common_status_circle_small">5dp</dimen>
+    <dimen name="common_status_circle_medium">10dp</dimen>
 
     <dimen name="common_btn_width">150dp</dimen>
     <dimen name="common_btn_height">50dp</dimen>
@@ -28,7 +30,7 @@
     <dimen name="switch_radius">13dp</dimen>
     <dimen name="switch_thumb_stroke">2dp</dimen>
     <dimen name="switch_thumb_size">22dp</dimen>
-    
+
     <dimen name="home_navi_height">200dp</dimen>
     <dimen name="home_navi_width">133dp</dimen>
 

+ 4 - 0
app/src/main/res/values/strings.xml

@@ -331,4 +331,8 @@
     <string name="take_one_more_key_hint">已有钥匙取出,是否再取一个</string>
     <string name="current_user_does_not_have_the_authority_to_colock">当前用户无权共锁</string>
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">当前用户无权解除共锁</string>
+    <string name="all_point_have_other_job_not_finish">所有隔离点存在其他作业未完成,是否继续拆锁恢复?</string>
+    <string name="lock_is_not_enough_stop_issue_ticket">锁具数量不足,停止下发作业票</string>
+    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="current_ticket_report_lock_take_exception_tip">当前作业挂锁上报异常,请归还挂锁</string>
 </resources>

+ 1 - 0
build.gradle

@@ -2,4 +2,5 @@
 plugins {
     alias(libs.plugins.android.application) apply false
     alias(libs.plugins.jetbrains.kotlin.android) apply false
+    id("com.google.devtools.ksp") version "2.1.10-1.0.31" apply false
 }

+ 13 - 1
gradle/libs.versions.toml

@@ -1,6 +1,6 @@
 [versions]
 agp = "8.5.2"
-kotlin = "1.9.0"
+kotlin = "2.1.10"
 coreKtx = "1.13.0"
 junit = "4.13.2"
 junitVersion = "1.1.5"
@@ -14,6 +14,8 @@ okhttps = "4.1.0"
 log-interceptor = "3.14.9"
 autosize = "v1.2.1"
 fastble = "2.4.0"
+lifecycle-version = "2.9.0"
+room-version = "2.7.1"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -31,6 +33,16 @@ log-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor"
 autosize = { group = "com.github.JessYanCoding", name = "AndroidAutoSize", version.ref = "autosize" }
 fastble = { group = "com.github.Jasonchenlijian", name = "FastBle", version.ref = "fastble" }
 
+viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle-version" }
+viewmodel-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle-version" }
+viewmodel-savestate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "lifecycle-version" }
+viewmodel-compiler = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "lifecycle-version" }
+
+room-runtime = { group = "androidx.room",name = "room-runtime",version.ref = "room-version" }
+room-compiler = { group = "androidx.room",name = "room-compiler",version.ref = "room-version" }
+room-ktx = { group = "androidx.room",name = "room-ktx",version.ref = "room-version" }
+room-testing = { group = "androidx.room",name = "room-testing",version.ref = "room-version" }
+
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }