Sfoglia il codice sorgente

merge(master) :
- 主分支合并

周文健 5 mesi fa
parent
commit
4ba2053db3
56 ha cambiato i file con 3071 aggiunte e 853 eliminazioni
  1. 241 538
      app/src/main/java/com/grkj/iscs/BusinessManager.kt
  2. 141 35
      app/src/main/java/com/grkj/iscs/ble/BleCmdManager.kt
  3. 482 0
      app/src/main/java/com/grkj/iscs/ble/BleConnectionManager.kt
  4. 12 0
      app/src/main/java/com/grkj/iscs/ble/BleConst.kt
  5. 1 0
      app/src/main/java/com/grkj/iscs/extentions/Context.kt
  6. 53 25
      app/src/main/java/com/grkj/iscs/modbus/DockBean.kt
  7. 19 1
      app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt
  8. 181 97
      app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt
  9. 0 14
      app/src/main/java/com/grkj/iscs/modbus/ModBusManager.kt
  10. 51 0
      app/src/main/java/com/grkj/iscs/model/DictConstants.kt
  11. 44 3
      app/src/main/java/com/grkj/iscs/model/UrlConsts.kt
  12. 4 1
      app/src/main/java/com/grkj/iscs/model/eventmsg/MsgEventConstants.kt
  13. 33 0
      app/src/main/java/com/grkj/iscs/model/vo/cabinet/LockCabinetPageRespVO.kt
  14. 20 20
      app/src/main/java/com/grkj/iscs/model/vo/hardware/CabinetSlotsRespVo.kt
  15. 17 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/JobCardPageRespVO.kt
  16. 16 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/RfidTokenPageRespVO.kt
  17. 10 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/SwitchListReqVO.kt
  18. 10 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/UpdateHardwareEsStatusReqVO.kt
  19. 17 0
      app/src/main/java/com/grkj/iscs/model/vo/key/KeyPageRespVO.kt
  20. 16 0
      app/src/main/java/com/grkj/iscs/model/vo/lock/LockPageRespVO.kt
  21. 13 0
      app/src/main/java/com/grkj/iscs/model/vo/machinery/IsLotoStationPageRespVO.kt
  22. 3 1
      app/src/main/java/com/grkj/iscs/model/vo/map/MapInfoRespVO.kt
  23. 12 0
      app/src/main/java/com/grkj/iscs/model/vo/system/SystemAttributeByKeyRespVO.kt
  24. 261 8
      app/src/main/java/com/grkj/iscs/util/NetApi.kt
  25. 19 0
      app/src/main/java/com/grkj/iscs/util/SPUtils.kt
  26. 6 2
      app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt
  27. 1 0
      app/src/main/java/com/grkj/iscs/view/activity/LoginActivity.kt
  28. 27 0
      app/src/main/java/com/grkj/iscs/view/dialog/CabinetSerialNoDialog.kt
  29. 49 0
      app/src/main/java/com/grkj/iscs/view/dialog/SlotExceptionDialog.kt
  30. 243 20
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt
  31. 45 7
      app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt
  32. 114 3
      app/src/main/java/com/grkj/iscs/view/fragment/ExceptionReportFragment.kt
  33. 1 1
      app/src/main/java/com/grkj/iscs/view/fragment/StepFragment.kt
  34. 117 1
      app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt
  35. 0 14
      app/src/main/java/com/grkj/iscs/view/fragment/WorkshopFragment.kt
  36. 84 5
      app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt
  37. 35 1
      app/src/main/java/com/grkj/iscs/view/presenter/ExceptionReportPresenter.kt
  38. 27 3
      app/src/main/java/com/grkj/iscs/view/presenter/HomePresenter.kt
  39. 28 4
      app/src/main/java/com/grkj/iscs/view/presenter/LoginPresenter.kt
  40. 26 0
      app/src/main/java/com/grkj/iscs/view/presenter/SwitchStatusPresenter.kt
  41. 12 8
      app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt
  42. 221 0
      app/src/main/java/com/grkj/iscs/view/widget/CustomSwitchStationLayer.kt
  43. 37 9
      app/src/main/java/com/grkj/iscs/view/widget/SelectableInput.kt
  44. 59 0
      app/src/main/res/layout/dialog_cabinet_serial_no.xml
  45. 77 0
      app/src/main/res/layout/dialog_slot_exception.xml
  46. 14 2
      app/src/main/res/layout/fragment_exception_report.xml
  47. 7 0
      app/src/main/res/layout/item_rv_dock_test_child.xml
  48. 1 2
      app/src/main/res/layout/item_rv_empty_dock_status.xml
  49. 74 22
      app/src/main/res/layout/item_rv_key_dock_status.xml
  50. 17 2
      app/src/main/res/layout/item_rv_lock_dock_child_status.xml
  51. 5 0
      app/src/main/res/values-en/colors.xml
  52. 20 1
      app/src/main/res/values-en/strings.xml
  53. 5 0
      app/src/main/res/values-zh/colors.xml
  54. 20 1
      app/src/main/res/values-zh/strings.xml
  55. 2 0
      app/src/main/res/values/colors.xml
  56. 21 2
      app/src/main/res/values/strings.xml

File diff suppressed because it is too large
+ 241 - 538
app/src/main/java/com/grkj/iscs/BusinessManager.kt


+ 141 - 35
app/src/main/java/com/grkj/iscs/ble/BleCmdManager.kt

@@ -39,7 +39,7 @@ object BleCmdManager {
     /**
      * 拼接时间戳 + token
      */
-     private fun assembleData(bleBean: BleBean, byteArray: ByteArray): ByteArray? {
+    private fun assembleData(bleBean: BleBean, byteArray: ByteArray): ByteArray? {
         bleBean.token?.let {
             return assembleTimeStamp(byteArray) + it
         } ?: run {
@@ -80,17 +80,26 @@ object BleCmdManager {
 //        }
         when {
             // 获取令牌
-            byteArray.startsWith(BleConst.RSP_GET_TOKEN) -> handleToken(bleBean.bleDevice, byteArray)
+            byteArray.startsWith(BleConst.RSP_GET_TOKEN) -> handleToken(
+                bleBean.bleDevice,
+                byteArray
+            )
             // 工作模式切换
             byteArray.startsWith(BleConst.RSP_SWITCH_MODE) -> handleSwitchModeResult(byteArray)
             // 工作票下发
-            byteArray.startsWith(BleConst.RSP_SEND_WORK_TICKET) -> handleWorkTicketResult(bleBean, byteArray)
+            byteArray.startsWith(BleConst.RSP_SEND_WORK_TICKET) -> handleWorkTicketResult(
+                bleBean,
+                byteArray
+            )
             // 获取设备当前状态
             byteArray.startsWith(BleConst.RSP_CURRENT_STATUS) -> handleCurrentStatus(byteArray)
             // 获取钥匙电量
             byteArray.startsWith(BleConst.RSP_POWER_STATUS) -> handlePowerStatus(byteArray)
             // 传输文件
-            byteArray.startsWith(BleConst.RSP_TRANSFER_FILE) && byteArray[3] == 0x01.toByte() -> handleFileRsp(bleBean, byteArray)
+            byteArray.startsWith(BleConst.RSP_TRANSFER_FILE) && byteArray[3] == 0x01.toByte() -> handleFileRsp(
+                bleBean,
+                byteArray
+            )
             // 获取固件版本号
             byteArray.startsWith(BleConst.RSP_GET_VERSION) -> handleVersion(byteArray)
             // 获取设备工作票完成情况
@@ -106,7 +115,11 @@ object BleCmdManager {
         LogUtil.i("$mac")
         BusinessManager.getBleDeviceByMac(mac)?.bleDevice?.let {
             LogUtil.i("Get token : $mac")
-            BleUtil.instance?.write(it, cmd = assembleTimeStamp(REQ_GET_TOKEN), writeCallback = callback)
+            BleUtil.instance?.write(
+                it,
+                cmd = assembleTimeStamp(REQ_GET_TOKEN),
+                writeCallback = callback
+            )
         }
     }
 
@@ -115,7 +128,11 @@ object BleCmdManager {
      *
      * @param callBack 是否成功
      */
-    fun handleToken(bleDevice: BleDevice, byteArray: ByteArray, callBack: ((Boolean) -> Unit)? = null) {
+    fun handleToken(
+        bleDevice: BleDevice,
+        byteArray: ByteArray,
+        callBack: ((Boolean) -> Unit)? = null
+    ) {
         LogUtil.i("handleToken : ${byteArray.toHexStrings()}")
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
             it.token = byteArrayOf(byteArray[11], byteArray[12], byteArray[13], byteArray[14])
@@ -133,7 +150,11 @@ object BleCmdManager {
      */
     fun switchMode(mode: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_SWITCH_MODE + mode), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_SWITCH_MODE + mode),
+                writeCallback = callback
+            )
         }
     }
 
@@ -152,7 +173,12 @@ object BleCmdManager {
     /**
      * 工作票下发
      */
-    fun sendWorkTicket(json: String, idx: Int = 0, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+    fun sendWorkTicket(
+        json: String,
+        idx: Int = 0,
+        bleDevice: BleDevice,
+        callback: CustomBleWriteCallback?
+    ) {
         LogUtil.i("sendWorkTicket : $idx")
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
             it.ticketSend = json
@@ -167,16 +193,30 @@ object BleCmdManager {
             json.toByteArray().copyOfRange(idx * 128, (idx + 1) * 128)
         }
 //        val jsonInfo = total + idx.toByteArray() + CRC16.crc16(data, 0, data.size - 1).toByteArray() + data.size.toByteArray() + data
-        val jsonInfo = total + idx.toByteArray() + data.crc16(0, data.size) + data.size.toByteArray() + data
-        LogUtil.d("debug1 : ${total.size} : ${idx.toByteArray().size} : ${data.crc16(0, data.size).size} : ${data.size.toByteArray().size} : ${data.size}")
+        val jsonInfo =
+            total + idx.toByteArray() + data.crc16(0, data.size) + data.size.toByteArray() + data
+        LogUtil.d(
+            "debug1 : ${total.size} : ${idx.toByteArray().size} : ${
+                data.crc16(
+                    0,
+                    data.size
+                ).size
+            } : ${data.size.toByteArray().size} : ${data.size}"
+        )
         LogUtil.d("debug2 : ${(jsonInfo.size + 1).toByteArray(1).size} : ${0x02.toByteArray(1).size} : ${jsonInfo.size}")
 
-        val cmd = REQ_SEND_WORK_TICKET + (jsonInfo.size + 1).toByteArray(1) + 0x02.toByteArray(1) + jsonInfo
+        val cmd =
+            REQ_SEND_WORK_TICKET + (jsonInfo.size + 1).toByteArray(1) + 0x02.toByteArray(1) + jsonInfo
 
         LogUtil.d("debug3 : ${cmd.toHexStrings()}")
 
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, writeUUID = WRITE_UUID, cmd = assembleData(it, cmd), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                writeUUID = WRITE_UUID,
+                cmd = assembleData(it, cmd),
+                writeCallback = callback
+            )
         }
     }
 
@@ -186,7 +226,11 @@ object BleCmdManager {
      *
      * @param callBack 是否成功、结果
      */
-    fun handleWorkTicketResult(bleBean: BleBean, byteArray: ByteArray, callBack: ((Boolean, Byte?) -> Unit)? = null) {
+    fun handleWorkTicketResult(
+        bleBean: BleBean,
+        byteArray: ByteArray,
+        callBack: ((Boolean, Byte?) -> Unit)? = null
+    ) {
         LogUtil.i("handleWorkTicketResult : ${byteArray.toHexStrings()}")
         val idx = byteArray[4] + byteArray[5]
         val total = byteArray[6] + byteArray[7]
@@ -208,7 +252,12 @@ object BleCmdManager {
                     if (res == 0x00.toByte()) idx + 1 else idx,
                     bleBean.bleDevice,
                     object : CustomBleWriteCallback() {
-                        override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {}
+                        override fun onWriteSuccess(
+                            current: Int,
+                            total: Int,
+                            justWrite: ByteArray?
+                        ) {
+                        }
 
                         override fun onWriteFailure(exception: BleException?) {}
                     })
@@ -231,7 +280,11 @@ object BleCmdManager {
      */
     fun getCurrentStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_CURRENT_STATUS), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_CURRENT_STATUS),
+                writeCallback = callback
+            )
         }
     }
 
@@ -246,9 +299,11 @@ object BleCmdManager {
             0x01.toByte() -> {
                 LogUtil.i("handleCurrentStatus : 工作模式")
             }
+
             0x02.toByte() -> {
                 LogUtil.i("handleCurrentStatus : 待机模式")
             }
+
             0x03.toByte() -> {
                 LogUtil.i("handleCurrentStatus : 故障状态")
             }
@@ -261,14 +316,22 @@ object BleCmdManager {
      */
     fun getTicketStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_WORK_TICKET_RESULT), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_WORK_TICKET_RESULT),
+                writeCallback = callback
+            )
         }
     }
 
     /**
      * 处理工作票完成情况
      */
-    fun handleTicketStatus(bleDevice: BleDevice, byteArray: ByteArray, callBack: ((String?) -> Unit)? = null) {
+    fun handleTicketStatus(
+        bleDevice: BleDevice,
+        byteArray: ByteArray,
+        callBack: ((String?) -> Unit)? = null
+    ) {
         // TODO 需要有超时重传机制
         LogUtil.i("handleTicketStatus : ${byteArray.toHexStrings()}")
 
@@ -283,16 +346,26 @@ object BleCmdManager {
         }
         // TODO 缺少res处理
         if (idx != total - 1) {
-            getTicketStatusPart(idx.toByteArray(), total.toByteArray(), byteArrayOf(0x00.toByte()), bleDevice, object : CustomBleWriteCallback() {
-                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
-                    LogUtil.i("getTicketStatusPart success")
-                }
-
-                override fun onWriteFailure(exception: BleException?) {
-                    LogUtil.e("getTicketStatusPart fail")
-                    BusinessManager.sendEventMsg(MsgEvent(MSG_EVENT_GET_TICKET_STATUS, GetTicketStatusMsg(false, bleDevice)))
-                }
-            })
+            getTicketStatusPart(
+                idx.toByteArray(),
+                total.toByteArray(),
+                byteArrayOf(0x00.toByte()),
+                bleDevice,
+                object : CustomBleWriteCallback() {
+                    override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                        LogUtil.i("getTicketStatusPart success")
+                    }
+
+                    override fun onWriteFailure(exception: BleException?) {
+                        LogUtil.e("getTicketStatusPart fail")
+                        BusinessManager.sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_GET_TICKET_STATUS,
+                                GetTicketStatusMsg(false, bleDevice)
+                            )
+                        )
+                    }
+                })
         } else {
             BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
                 LogUtil.i("工作票完成接收 : ${String(it.ticketStatus)}")
@@ -308,9 +381,19 @@ object BleCmdManager {
     /**
      * 获取工作票完成情况分包
      */
-    private fun getTicketStatusPart(idx: ByteArray, total: ByteArray, res: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+    private fun getTicketStatusPart(
+        idx: ByteArray,
+        total: ByteArray,
+        res: ByteArray,
+        bleDevice: BleDevice,
+        callback: CustomBleWriteCallback?
+    ) {
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_WORK_TICKET_RESULT_PART + idx + total + res), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_WORK_TICKET_RESULT_PART + idx + total + res),
+                writeCallback = callback
+            )
         }
     }
 
@@ -319,7 +402,11 @@ object BleCmdManager {
      */
     fun getPower(mac: String?, callback: CustomBleWriteCallback?) {
         BusinessManager.getBleDeviceByMac(mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_POWER_STATUS), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_POWER_STATUS),
+                writeCallback = callback
+            )
         }
     }
 
@@ -337,9 +424,11 @@ object BleCmdManager {
             0x01.toByte() -> {
                 LogUtil.i("充电状态:未充电")
             }
+
             0x02.toByte() -> {
                 LogUtil.i("充电状态:充电中")
             }
+
             0x03.toByte() -> {
                 LogUtil.i("充电状态:充满")
             }
@@ -358,7 +447,13 @@ object BleCmdManager {
      * PGSZ:当前包长度(字节)
      * PGDATA:当前包数据
      */
-    fun sendFile(type: Int, file: File, idx: Int = 0, mac: String?, callback: CustomBleWriteCallback?) {
+    fun sendFile(
+        type: Int,
+        file: File,
+        idx: Int = 0,
+        mac: String?,
+        callback: CustomBleWriteCallback?
+    ) {
         Executor.runOnIO {
             LogUtil.i("sendFile : $idx")
 
@@ -383,17 +478,24 @@ object BleCmdManager {
             val pgsz = pgdata.size.toByteArray()
             val pgcrc = pgdata.crc16()
 
-            val fileInfo = byteArrayOf(type.toByte()) + flnm + flsz + flcrc + pgtotal.toByteArray() + idx.toByteArray() + pgcrc + pgsz + pgdata
+            val fileInfo =
+                byteArrayOf(type.toByte()) + flnm + flsz + flcrc + pgtotal.toByteArray() + idx.toByteArray() + pgcrc + pgsz + pgdata
 //        println("______________________________________________________________________________")
 //        println("${file.readBytes().size}")
 //        println("${byteArrayOf(type.toByte()).size} : ${flnm.size} : ${flsz.size} : ${flcrc.size} : ${pgtotal.toByteArray().size} : ${idx.toByteArray().size} : ${pgcrc.size} : ${pgsz.size} : ${pgdata.size}")
 //        println("______________________________________________________________________________")
 //        println("file info size : ${fileInfo.size}")
-            val cmd = REQ_TRANSFER_FILE + (fileInfo.size + 1).toByteArray(1) + 0x01.toByteArray(1) + fileInfo
+            val cmd =
+                REQ_TRANSFER_FILE + (fileInfo.size + 1).toByteArray(1) + 0x01.toByteArray(1) + fileInfo
 //        println("cmd size : ${REQ_TRANSFER_FILE.size} : ${(fileInfo.size + 1).toByteArray(1).size} : ${0x01.toByteArray(1).size} : ${fileInfo.size}")
 
             BusinessManager.getBleDeviceByMac(mac)?.let {
-                BleUtil.instance?.write(it.bleDevice, writeUUID = WRITE_UUID, cmd = assembleData(it, cmd), writeCallback = callback)
+                BleUtil.instance?.write(
+                    it.bleDevice,
+                    writeUUID = WRITE_UUID,
+                    cmd = assembleData(it, cmd),
+                    writeCallback = callback
+                )
             }
             Thread.sleep(50)
             sendFile(type, file, idx + 1, mac, callback)
@@ -420,7 +522,11 @@ object BleCmdManager {
      */
     fun getVersion(mac: String?, callback: CustomBleWriteCallback?) {
         BusinessManager.getBleDeviceByMac(mac)?.let {
-            BleUtil.instance?.write(it.bleDevice, cmd = assembleData(it, REQ_GET_VERSION), writeCallback = callback)
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, REQ_GET_VERSION),
+                writeCallback = callback
+            )
         }
     }
 

+ 482 - 0
app/src/main/java/com/grkj/iscs/ble/BleConnectionManager.kt

@@ -0,0 +1,482 @@
+package com.grkj.iscs.ble
+
+import android.bluetooth.BluetoothGatt
+import androidx.appcompat.app.AppCompatActivity
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.BusinessManager.deviceList
+import com.grkj.iscs.BusinessManager.getBleDeviceByMac
+import com.grkj.iscs.BusinessManager.getCurrentStatus
+import com.grkj.iscs.BusinessManager.isTestMode
+import com.grkj.iscs.BusinessManager.removeExceptionKey
+import com.grkj.iscs.BusinessManager.sendEventMsg
+import com.grkj.iscs.BusinessManager.sendLoadingEventMsg
+import com.grkj.iscs.R
+import com.grkj.iscs.extentions.toHexStrings
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.Constants.PERMISSION_REQUEST_CODE
+import com.grkj.iscs.model.eventmsg.LoadingMsg
+import com.grkj.iscs.model.eventmsg.MsgEvent
+import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_LOADING
+import com.grkj.iscs.util.ActivityUtils
+import com.grkj.iscs.util.CommonUtils
+import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.base.BaseActivity
+import com.sik.sikcore.activity.ActivityTracker
+import com.sik.sikcore.thread.ThreadUtils
+import pub.devrel.easypermissions.AfterPermissionGranted
+import java.util.LinkedList
+
+/**
+ * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
+ * 并增加“最大待机数”功能,超出后断开最旧连接,保证同时在线设备数不超过阈值。
+ */
+object BleConnectionManager {
+    /**
+     * 最大待机连接数,超过则断开最旧设备。
+     * 默认为业务常量 MAX_KEY_STAND_BY,可根据需求调整。
+     */
+    @Volatile
+    var maxStandbyCount: Int = BleConst.MAX_KEY_STAND_BY
+
+    @Volatile
+    var maxConnectCount: Int = BleConst.MAX_KEY_CONNECT_COUNT
+
+
+    // 按连接完成顺序维护待机队列
+    private val standbyQueue = LinkedList<String>()
+
+    // 原有回调管理
+    private val connectListeners = mutableListOf<ConnectListener>()
+    private var isPreparing: Boolean = false
+
+    @Volatile
+    private var currentConnectingMac: String? = null
+
+    /**
+     * 注册连接监听:
+     * - 如果设备已在 deviceList 且拥有 token,立即回调并返回
+     * - 如果 mac 已在待连接队列或正在连接,忽略重复请求
+     * - 否则将 mac 添加到队列并触发连接流程
+     */
+    fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
+        LogUtil.i("registerConnectListener : $mac")
+        // 已连接且已获取 token
+        deviceList.find { it.bleDevice.mac == mac && it.token != null }?.let { bean ->
+            callBack?.invoke(true, bean)
+            return
+        }
+        // 重复注册检查
+        if (connectListeners.any { it.mac == mac } || currentConnectingMac == mac) {
+            LogUtil.w("忽略重复注册 mac: $mac")
+            return
+        }
+        // 加入队列并启动连接
+        fun checkAndConnect() {
+            if (BleManager.getInstance().allConnectedDevice.size < maxConnectCount) {
+                connectListeners.add(ConnectListener(mac, callBack))
+                connectKey()
+            } else {
+                ThreadUtils.runOnIODelayed(500) {
+                    checkAndConnect()
+                }
+            }
+        }
+        checkAndConnect()
+    }
+
+    /**
+     * 连接监听反注册
+     */
+    fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
+        LogUtil.i("unregisterConnectListener : $mac")
+        connectListeners.removeAll { it.mac == mac }
+    }
+
+    /**
+     * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
+     */
+    private fun checkProcess(mac: String?): Boolean {
+        val canProcess = connectListeners.any { it.mac == mac }
+        if (!canProcess) sendLoadingEventMsg(null, false)
+        return canProcess
+    }
+
+    /**
+     * 连接钥匙,单个mac走完prepare再进行下一个
+     */
+    private fun connectKey() {
+        if (connectListeners.isEmpty()) return
+        if (isPreparing || BleManager.getInstance().allConnectedDevice.size >= maxStandbyCount) {
+            Executor.delayOnMain(1000) { connectKey() }
+            return
+        }
+        val listener = connectListeners.first()
+        currentConnectingMac = listener.mac
+        isPreparing = true
+        if (ActivityTracker.getCurrentActivity() == null) {
+            LogUtil.w("Ignore connectKey : ${listener.mac} no current activity")
+            isPreparing = false
+            currentConnectingMac = null
+            return
+        }
+        prepareBle(
+            listener.mac, ActivityUtils.currentActivity() as BaseActivity<*>, false
+        ) { isDone, bleBean ->
+            Executor.runOnMain {
+                isPreparing = false
+                currentConnectingMac = null
+                if (!isDone) {
+                    // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
+                    if (checkProcess(listener.mac)) {
+                        unregisterConnectListener(listener.mac)
+                        Executor.delayOnMain(2000) {
+                            registerConnectListener(
+                                listener.mac,
+                                listener.callBack
+                            )
+                        }
+                    }
+                    return@runOnMain
+                }
+                // 判断是否仍然待连,防止拿走
+                // TODO 暂时只处理准备成功
+                if (connectListeners.contains(listener)) {
+                    listener.callBack?.invoke(true, bleBean)
+                    unregisterConnectListener(listener.mac)
+                }
+//                bleBean?.bleDevice?.mac?.let { mac ->
+//                    addToStandby(mac)
+//                }
+                if (connectListeners.isNotEmpty()) connectKey()
+            }
+        }
+    }
+
+    /**
+     * 添加到待机队列并断开最旧超出连接
+     */
+    private fun addToStandby(mac: String) {
+        synchronized(standbyQueue) {
+            standbyQueue.addLast(mac)
+            while (standbyQueue.size > maxStandbyCount) {
+                val oldMac = standbyQueue.removeFirst()
+                deviceList.find { it.bleDevice.mac == oldMac }?.let { oldBean ->
+                    LogUtil.i("断开最旧待机设备: $oldMac")
+                    BleManager.getInstance().disconnect(oldBean.bleDevice)
+                }
+            }
+        }
+    }
+
+    /**
+     * @param loadingCallBack 是否显示loading、loading文字、流程是否结束
+     * @param prepareDoneCallBack 蓝牙连接是否成功、蓝牙连接对象
+     */
+    private fun prepareBle(
+        mac: String,
+        activity: AppCompatActivity,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(mac)) {
+            LogUtil.e("Prepare is canceled : $mac")
+            return
+        }
+        Executor.runOnMain {
+            CommonUtils.checkBlePermission(activity) {
+                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+            }
+        }
+    }
+
+    @AfterPermissionGranted(PERMISSION_REQUEST_CODE)
+    private fun doScanBle(
+        mac: String,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        LogUtil.i("doScanBle:$mac")
+        if (!checkProcess(mac)) {
+            LogUtil.e("Prepare is canceled : $mac")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "正在扫描设备...", null)
+            )
+        )
+        BleUtil.instance?.scan(object : CustomBleScanCallback() {
+            override fun onPrompt(promptStr: String?) {
+                // 蓝牙未启动重试
+                BleManager.getInstance().enableBluetooth()
+                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+            }
+
+            override fun onScanStarted(success: Boolean) {
+                LogUtil.i("onScanStarted:${success}")
+                if (!success) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                        )
+                    )
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+
+            override fun onScanning(bleDevice: BleDevice?) {
+                LogUtil.i("onScanning:${bleDevice?.mac}")
+                bleDevice?.let {
+                    doConnect(it, isNeedLoading, prepareDoneCallBack)
+                }
+            }
+
+            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                LogUtil.i("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
+                if (isNeedLoading) sendEventMsg(
+                    MsgEvent(
+                        MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                    )
+                )
+                // 没有扫描到
+                if (scanResultList?.none { it.mac == mac } == true) {
+                    LogUtil.w("$mac is not scanned")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+        })
+    }
+
+    /**
+     * 连接蓝牙设备
+     */
+    private fun doConnect(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        LogUtil.i("doConnect : ${bleDevice.mac}")
+        if (!checkProcess(bleDevice.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING,
+                LoadingMsg(true, CommonUtils.getStr(R.string.ble_connecting), null)
+            )
+        )
+        BleManager.getInstance().disconnect(bleDevice)
+        BleUtil.instance?.connectBySelect(
+            bleDevice, object : CustomBleGattCallback() {
+                override fun onPrompt(promptStr: String?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, promptStr, null)
+                        )
+                    )
+                }
+
+                override fun onStartConnect() {}
+
+                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING,
+                            LoadingMsg(false, CommonUtils.getStr(R.string.ble_connect_fail), false)
+                        )
+                    )
+                    LogUtil.e("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
+                ) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                        )
+                    )
+                    LogUtil.i("onConnectSuccess : ${bleDevice?.mac}")
+                    bleDevice?.let {
+                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                        val bleBean = BleBean(it)
+                        deviceList.add(bleBean)
+                        removeExceptionKey(it.mac)
+                        // 设置MTU
+                        Executor.delayOnMain(200) {
+                            if (!checkProcess(bleDevice.mac)) {
+                                LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+                                return@delayOnMain
+                            }
+                            BleUtil.instance?.setMtu(it)
+                        }
+                        // 监听
+                        Executor.delayOnMain(500) {
+                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
+                        }
+                    }
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice?,
+                    gatt: BluetoothGatt?,
+                    status: Int
+                ) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                        )
+                    )
+                    LogUtil.i("onDisConnected : ${device?.mac} - $isActiveDisConnected")
+                    getBleDeviceByMac(device?.mac)?.let {
+                        deviceList.remove(it)
+                    }
+                    bleDevice.mac?.let { itMac ->
+                        unregisterConnectListener(itMac)
+                    }
+                    if (!isActiveDisConnected) {
+                        // 测试模式下不重连
+                        if (isTestMode) {
+                            return
+                        }
+                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
+                        Executor.delayOnMain(300) {
+                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
+                                if (isDone && bleBean != null) {
+                                    Executor.delayOnMain(300) {
+                                        getCurrentStatus(6, bleBean.bleDevice)
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
+                    }
+                }
+            })
+    }
+
+    /**
+     * 监听蓝牙设备
+     */
+    private fun indicate(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "开始监听...", null)
+            )
+        )
+        bleBean?.let {
+            var isIndicateSuccess = false
+            BleUtil.instance?.indicate(
+                it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
+                    override fun onPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onPrompt : $promptStr")
+                    }
+
+                    override fun onConnectPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onConnectPrompt : $promptStr")
+                    }
+
+                    override fun onDisConnectPrompt(promptStr: String?) {
+                        LogUtil.i("indicate onDisConnectPrompt : $promptStr")
+                    }
+
+                    override fun onIndicateSuccess() {
+                        LogUtil.i("onIndicateSuccess")
+                        isIndicateSuccess = true
+                        getToken(bleBean, isNeedLoading, prepareDoneCallBack)
+                    }
+
+                    override fun onIndicateFailure(exception: BleException?) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                            )
+                        )
+                        LogUtil.e("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
+                        Executor.delayOnIO(500) {
+                            if (isIndicateSuccess) {
+                                return@delayOnIO
+                            }
+                            prepareDoneCallBack?.invoke(false, null)
+                        }
+                    }
+
+                    override fun onCharacteristicChanged(data: ByteArray?) {
+                        LogUtil.i("onCharacteristicChanged : ${data?.toHexStrings()}")
+                        data?.let { itData ->
+                            BusinessManager.handleRsp(
+                                it,
+                                itData,
+                                isNeedLoading,
+                                prepareDoneCallBack
+                            )
+                        }
+                    }
+                })
+        }
+    }
+
+    /**
+     * 获取蓝牙钥匙token
+     */
+    private fun getToken(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) sendEventMsg(
+            MsgEvent(
+                MSG_EVENT_LOADING, LoadingMsg(true, "开始获取token...", null)
+            )
+        )
+        bleBean?.let {
+            BleCmdManager.getToken(it.bleDevice.mac, object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取成功", null)
+                        )
+                    )
+                    LogUtil.i("getToken success : ${bleBean.bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    if (isNeedLoading) sendEventMsg(
+                        MsgEvent(
+                            MSG_EVENT_LOADING, LoadingMsg(false, "token获取失败", false)
+                        )
+                    )
+                    LogUtil.e("getToken fail : ${bleBean.bleDevice.mac}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            })
+        }
+    }
+
+
+    // 蓝牙连接准备监听
+    data class ConnectListener(
+        val mac: String,
+        val callBack: ((Boolean, BleBean?) -> Unit)? = null
+    )
+}

+ 12 - 0
app/src/main/java/com/grkj/iscs/ble/BleConst.kt

@@ -5,6 +5,9 @@ package com.grkj.iscs.ble
  */
 object BleConst {
 
+    const val MAX_KEY_STAND_BY: Int = 1
+    const val MAX_KEY_CONNECT_COUNT: Int = 2
+
     const val MTU = 500
 
     const val SERVICE_UUID = "0000FEE7-0000-1000-8000-00805F9B34FB"
@@ -20,43 +23,52 @@ object BleConst {
      */
     // 获取令牌,需增加4字节的时间戳,总长8个字节长度
     val REQ_GET_TOKEN = byteArrayOf(0x01, 0x01, 0x05, 0x00)
+
     // 获取令牌响应,最后4个是token,总长15个字节长度
     val RSP_GET_TOKEN = byteArrayOf(0x01, 0x02, 0x04)
 
     // 设备工作模式切换
     val REQ_SWITCH_MODE = byteArrayOf(0x02, 0x01, 0x02, 0x01)
+
     // 工作模式切换响应
     val RSP_SWITCH_MODE = byteArrayOf(0x02, 0x02, 0x03, 0x01)
 
     // 工作票下发
     val REQ_SEND_WORK_TICKET = byteArrayOf(0x02, 0x01)
+
     // 工作票下发响应
     val RSP_SEND_WORK_TICKET = byteArrayOf(0x02, 0x02, 0x06, 0x02)
 
     // 获取设备当前状态
     val REQ_CURRENT_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x01)
+
     // 获取当前设备响应
     val RSP_CURRENT_STATUS = byteArrayOf(0x03, 0x02, 0x02, 0x01)
 
     // 获取设备工作票完成情况
     val REQ_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x01, 0x01, 0x02)
+
     // 获取设备工作票完成情况响应
     val RSP_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x02)
+
     // 获取设备工作票完成情况分包
     val REQ_WORK_TICKET_RESULT_PART = byteArrayOf(0x03, 0x01, 0x06, 0x02)
 
     // 获取钥匙电量
     val REQ_POWER_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x03)
+
     // 获取钥匙电量响应
     val RSP_POWER_STATUS = byteArrayOf(0x03, 0x02, 0x03, 0x03)
 
     // 传输文件
     val REQ_TRANSFER_FILE = byteArrayOf(0x06, 0x01)
+
     // 传输文件完成响应
     val RSP_TRANSFER_FILE = byteArrayOf(0x06, 0x02)
 
     // 获取固件版本号
     val REQ_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x01, 0x02, 0x01, 0x01)
+
     // 获取固件版本号响应
     val RSP_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x02, 0x03, 0x01)
 }

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

@@ -10,6 +10,7 @@ import androidx.core.app.ActivityCompat
 import androidx.core.content.ContextCompat
 import androidx.lifecycle.Observer
 import com.grkj.iscs.util.NetManager
+import com.sik.sikcore.device.DeviceUtils
 import java.util.Locale
 
 /**

+ 53 - 25
app/src/main/java/com/grkj/iscs/modbus/DockBean.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.modbus
 
+import com.grkj.iscs.BusinessManager
 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
@@ -10,7 +11,10 @@ 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.model.eventmsg.MsgEvent
+import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_SWITCH_COLLECTION_UPDATE
 import com.grkj.iscs.util.log.LogUtil
+import java.util.concurrent.atomic.AtomicInteger
 
 /**
  * RS-485 设备底座 Bean
@@ -304,35 +308,59 @@ class DockBean(
                     return DockBean(addr, it, true, changeList)
                 }
 
+                else -> return null
+            }
+        } ?: return null
+    }
+
+    /**
+     * 转换开关状态
+     */
+    fun parseSwitchStatus(byteArray: ByteArray, done: () -> Unit): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        type?.let {
+            // 因为都是一个寄存器返回的,所以一定能得到2个钥匙的状态或者10把锁具的状态
+            when (it) {
                 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]
-                                            )
+                    val remainTimes = AtomicInteger(0)
+                    for (i in 0 until byteArray[4].toInt()) {
+                        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]
                                         )
-                                    }
+                                    )
                                 }
                             }
+                            remainTimes.addAndGet(1)
+                            if (remainTimes.get() == byteArray[4].toInt()) {
+                                BusinessManager.sendEventMsg(
+                                    MsgEvent(
+                                        MSG_EVENT_SWITCH_COLLECTION_UPDATE,
+                                        null
+                                    )
+                                )
+                                done()
+                            }
                         }
                     }
                     return DockBean(addr, type, isWorking, deviceList)

+ 19 - 1
app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt

@@ -112,12 +112,30 @@ object ModBusCMDHelper {
             byteArrayOf(
                 0x00,
                 0x11,
-                if (index == 1) 0b00010000.toByte() else 0b0000001,
+                if (index == 1) 0b00010000.toByte() else 0b0000001.toByte(),
                 if (isOpen) 0x00 else 0xFF.toByte()
             )
         )
     }
 
+    /**
+     * 操作钥匙/便携式底座钥匙充电,一次只操作一个卡扣
+     *
+     * @param isOpen true:开操作 false:关操作
+     * @param index 0:左 1:右 便携式底座钥匙传0
+     */
+    fun generateKeyBuckleChargeCmd(isOpen: Boolean, index: Int): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(
+                0x00,
+                0x11,
+                if (index == 1) 0b00100000.toByte() else 0b0000010.toByte(),
+                if (isOpen) 0xFF.toByte() else 0x00.toByte()
+            )
+        )
+    }
+
     /**
      * 生成RFID 读指令
      *

+ 181 - 97
app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt

@@ -5,7 +5,6 @@ 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
@@ -17,6 +16,7 @@ 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.model.vo.hardware.CabinetSlotsRecord
 import com.grkj.iscs.util.CommonUtils
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
@@ -24,7 +24,8 @@ import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.stream.Collectors
-import kotlin.random.Random
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 
 /**
@@ -77,10 +78,9 @@ object ModBusController {
     @ExperimentalUnsignedTypes
     fun start(ctx: Context) {
         modBusManager?.stop()
-        PortManager.openCtrlBord(ctx)
-            ?.let { pm ->
-                return@let ModBusManager(pm, true)
-            }
+        PortManager.openCtrlBord(ctx)?.let { pm ->
+            return@let ModBusManager(pm, true)
+        }
             // 间隔 1 秒读一遍桶的状态
             ?.repeatSendToAll(MBFrame.READ_STATUS, {
                 interruptReadStatus
@@ -93,11 +93,9 @@ object ModBusController {
                         l.listener(res)
                     }
                 }
-            }, REPEAT_FREQUENCY)
-            ?.also {
+            }, REPEAT_FREQUENCY)?.also {
                 modBusManager = it
-            }
-            ?.start()
+            }?.start()
     }
 
     /**
@@ -225,13 +223,15 @@ object ModBusController {
                                 if (it != null && !it.macAddress.isNullOrEmpty()) {
                                     // 更新mac
                                     updateKeyMac(dockBean.addr, key.isLeft, it.macAddress)
-                                    BusinessManager.registerConnectListener(it.macAddress) { isDone, bleBean ->
+                                    BusinessManager.registerConnectListener(
+                                        it.macAddress
+                                    ) { isDone, bleBean ->
                                         if (isDone && bleBean?.bleDevice != null) {
                                             Executor.delayOnMain(500) {
                                                 BusinessManager.getCurrentStatus(
-                                                    3,
-                                                    bleBean.bleDevice
+                                                    3, bleBean.bleDevice
                                                 )
+                                                BusinessManager.getBatteryPower(bleBean.bleDevice)
                                             }
                                         }
                                     }
@@ -243,6 +243,11 @@ object ModBusController {
                         controlKeyBuckle(false, key.isLeft, dockBean.addr)
                     } else {
                         controlKeyBuckle(true, key.isLeft, dockBean.addr)
+                        controlKeyCharge(
+                            false,
+                            key.isLeft,
+                            dockBean.addr
+                        )
                     }
                 }
             }
@@ -286,6 +291,20 @@ object ModBusController {
         }
     }
 
+    /**
+     * 更新开关状态
+     */
+    fun updateSwitchStatus(done: () -> Unit) {
+        modBusManager?.mSlaveAddressList?.find { it == (0xA1).toByte() }?.let {
+            modBusManager?.sendTo(it, MBFrame.READ_BUCKLE_STATUS) { res ->
+                LogUtil.i("****************************************************************************")
+                // 过滤非空的数据,重置slaveCount
+                // 不再使用slaveCount,改用地址池
+                switchStatus(res, done)
+            }
+        }
+    }
+
     /**
      * 第9,10锁位卡扣状态
      */
@@ -312,9 +331,7 @@ object ModBusController {
                 DOCK_TYPE_LOCK -> {
                     dockBean.getLockList().filter { it.idx > 7 }.forEach { lockBean ->
                         updateLockStatus(
-                            dockBean.addr,
-                            lockBean.idx,
-                            lockBean.lockEnabled
+                            dockBean.addr, lockBean.idx, lockBean.lockEnabled
                         )
                     }
                 }
@@ -322,6 +339,26 @@ object ModBusController {
         }
     }
 
+    /**
+     * 开关量更新
+     */
+    private fun switchStatus(res: Any, done: () -> Unit) {
+        LogUtil.i("开关板:${(res as ByteArray).toHexStrings()}")
+        if (res.isEmpty()) {
+            var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
+            val addressList = mutableListOf<String>()
+
+            modBusManager?.mSlaveAddressList?.forEach { itDock ->
+                if (res.isNotEmpty() && res == itDock) {
+                    addressList.add("0x${String.format("%02X", itDock)}")
+                }
+            }
+            tipStr += addressList
+            ToastUtils.tip(tipStr)
+        }
+        updateSwitchStatus(res, done)
+    }
+
     /**
      * 第1-8锁位卡扣状态和钥匙
      */
@@ -349,9 +386,7 @@ object ModBusController {
                 DOCK_TYPE_KEY -> {
                     dockBean.getKeyList().forEach { keyBean ->
                         updateKeyLockStatus(
-                            dockBean.addr,
-                            keyBean.isLeft,
-                            keyBean.lockEnabled
+                            dockBean.addr, keyBean.isLeft, keyBean.lockEnabled
                         )
                         //todo 更新锁仓状态
                     }
@@ -360,9 +395,7 @@ object ModBusController {
                 DOCK_TYPE_LOCK -> {
                     dockBean.getLockList().forEach { lockBean ->
                         updateLockStatus(
-                            dockBean.addr,
-                            lockBean.idx,
-                            lockBean.lockEnabled
+                            dockBean.addr, lockBean.idx, lockBean.lockEnabled
                         )
                     }
                 }
@@ -378,17 +411,13 @@ object ModBusController {
                             when (deviceBean.type) {
                                 DEVICE_TYPE_KEY -> {
                                     updateKeyLockStatus(
-                                        dockBean.addr,
-                                        true,
-                                        deviceBean.lockEnabled
+                                        dockBean.addr, true, deviceBean.lockEnabled
                                     )
                                 }
 
                                 DEVICE_TYPE_LOCK -> {
                                     updateLockStatus(
-                                        dockBean.addr,
-                                        deviceBean.idx,
-                                        deviceBean.lockEnabled
+                                        dockBean.addr, deviceBean.idx, deviceBean.lockEnabled
                                     )
                                 }
 
@@ -426,6 +455,17 @@ object ModBusController {
         return dockB?.parseLockStatus(byteArray)
     }
 
+    /**
+     * 更新开关状态
+     */
+    private fun updateSwitchStatus(byteArray: ByteArray, done: () -> Unit): DockBean? {
+        if (byteArray.isEmpty()) {
+            return null
+        }
+        val dockB = dockList.find { it.addr == byteArray[0] }
+        return dockB?.parseSwitchStatus(byteArray, done)
+    }
+
     /**
      * 获取额外的9,10锁仓数据
      */
@@ -486,10 +526,7 @@ object ModBusController {
      * 开/关锁具卡扣 单
      */
     fun controlLockBuckle(
-        isOpen: Boolean,
-        slaveAddress: Byte?,
-        lockIdx: Int,
-        done: ((res: ByteArray) -> Unit)? = null
+        isOpen: Boolean, slaveAddress: Byte?, lockIdx: Int, done: ((res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
             ModBusCMDHelper.generateLockBuckleCmd(isOpen, lockIdx)?.let { cmd ->
@@ -524,9 +561,7 @@ object ModBusController {
      * 读取钥匙RFID
      */
     fun readKeyRfid(
-        slaveAddress: Byte?,
-        idx: Int,
-        done: ((isLeft: Boolean, res: ByteArray) -> Unit)? = null
+        slaveAddress: Byte?, idx: Int, done: ((isLeft: Boolean, res: ByteArray) -> Unit)? = null
     ) {
         slaveAddress?.let {
             ModBusCMDHelper.generateRfidCmd(idx)?.let { cmd ->
@@ -651,6 +686,38 @@ object ModBusController {
         }
     }
 
+    /**
+     * 控制钥匙充电
+     */
+    fun controlKeyCharge(isOpen: Boolean, mac: String, done: ((res: ByteArray) -> Unit)? = null) {
+        val dockBean = getDockByKeyMac(mac)
+        dockBean ?: return
+        val key = getKeyByMac(mac)
+        key ?: return
+        controlKeyCharge(isOpen, key.isLeft, dockBean.addr, done)
+    }
+
+    /**
+     * 开/关钥匙充电
+     *
+     * @param isOpen true:开操作 false:关操作
+     * @param isLeft true:左充电 false:右充电
+     */
+    fun controlKeyCharge(
+        isOpen: Boolean,
+        isLeft: Boolean,
+        slaveAddress: Byte?,
+        done: ((res: ByteArray) -> Unit)? = null
+    ) {
+        slaveAddress?.let {
+            ModBusCMDHelper.generateKeyBuckleChargeCmd(isOpen, if (isLeft) 0 else 1)?.let { cmd ->
+                modBusManager?.sendTo(it, cmd) { res ->
+                    done?.invoke(res)
+                }
+            }
+        }
+    }
+
     /**
      * 根据RFID找钥匙
      */
@@ -776,8 +843,8 @@ object ModBusController {
      */
     fun getDockByKeyMac(mac: String): DockBean? {
         return dockList.find {
-            (it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE)
-                    && it.getKeyList().any { it.mac == mac }
+            (it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE) && it.getKeyList()
+                .any { it.mac == mac }
         }
     }
 
@@ -872,45 +939,67 @@ object ModBusController {
      *
      * @return 底座地址,钥匙
      */
-    fun getOneKey(): Pair<Byte, DockBean.KeyBean?>? {
-        val keyDockList =
-            dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
-        val keyList = keyDockList.flatMap { it.getKeyList() }.filter { it.isExist }
-        LogUtil.i("keyList : $keyList")
+    suspend fun getOneKey(
+        exceptionSlots: List<CabinetSlotsRecord>,
+        exceptionKeysRfid: List<String>,
+        exceptionKeysMac: List<String> = mutableListOf(),
+    ): Pair<Byte, DockBean.KeyBean?>? {
+        // 1. 过滤并准备钥匙列表
+        val slotCols = exceptionSlots.mapNotNull { it.col?.toInt() }
+        val keyDockList = dockList
+            .filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+            .sortedBy { it.addr }
+            .onEach { it.deviceList.sortBy { dev -> dev.idx } }
+
+        val keyList = keyDockList
+            .flatMap { it.deviceList }
+            .filterIsInstance<DockBean.KeyBean>()
+            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }
+            .filter { kb ->
+                !kb.rfid.isNullOrEmpty()
+                        && kb.rfid !in exceptionKeysRfid
+                        && kb.mac !in exceptionKeysMac
+                        && !kb.mac.isNullOrEmpty()
+                        && kb.isExist
+            }
+            .shuffled()
+
         if (keyList.isEmpty()) {
             ToastUtils.tip(R.string.no_available_key)
             return null
         }
 
-        keyList.forEach {
-            LogUtil.i(
-                "keyStatus : ${it.isExist} - ${it.rfid} - ${it.mac} - ${it.isReady} - " +
-                        "${BusinessManager.getBleDeviceByMac(it.mac)?.bleDevice != null} - " +
-                        "${
-                            BleManager.getInstance()
-                                .isConnected(BusinessManager.getBleDeviceByMac(it.mac)?.bleDevice)
-                        } - " +
-                        "${!BusinessManager.mExceptionKeyList.contains(it.rfid)}"
-            )
-        }
-        val key = keyList.filter {
-            it.isExist && it.rfid != null && it.mac != null && it.isReady
-                    && BleManager.getInstance()
-                .isConnected(BusinessManager.getBleDeviceByMac(it.mac)?.bleDevice)
-                    && !BusinessManager.mExceptionKeyList.contains(it.rfid)
-        }
-            .shuffled().firstOrNull()
-        if (key == null) {
-            LogUtil.e("getOneKey : no key match")
-            return null
+        // —— 优先检查已经连接的 ——
+        val already = keyList.firstOrNull { kb ->
+            BleManager.getInstance().isConnected(kb.mac!!)  // mac 一定 non-null
+        }
+        if (already != null) {
+            val addr = keyDockList
+                .firstOrNull { dock ->
+                    dock.getKeyList().any { it.rfid == already.rfid }
+                }?.addr
+            if (addr != null) return addr to already
         }
 
-        val address = keyDockList.find { it.getKeyList().any { it.rfid == key.rfid } }?.addr
-        if (address == null) {
-            LogUtil.e("getOneKey : no dock match")
-            return null
+        // —— 如果没有已连的,再顺序挂起尝试连接 ——
+        for (kb in keyList) {
+            val mac = kb.mac ?: continue
+            val found = suspendCoroutine<DockBean.KeyBean?> { cont ->
+                BusinessManager.registerConnectListener(mac) { isDone, _ ->
+                    if (isDone) cont.resume(kb)
+                }
+            }
+            if (found != null) {
+                val addr = keyDockList
+                    .firstOrNull { it.getKeyList().any { it.rfid == found.rfid } }
+                    ?.addr
+                return if (addr != null) addr to found else null
+            }
         }
-        return Pair(address, key)
+
+        // 一个都没成功
+        LogUtil.e("getOneKey : no key match")
+        return null
     }
 
     /**
@@ -920,25 +1009,37 @@ object ModBusController {
      *
      * @return key: dock地址,value: 锁具RFID列表
      */
-    fun getLocks(needLockCount: Int): MutableMap<Byte, MutableList<DockBean.LockBean>> {
+    fun getLocks(
+        needLockCount: Int,
+        exceptionSlots: MutableList<CabinetSlotsRecord>,
+        exceptionLocks: MutableList<String>
+    ): MutableMap<Byte, MutableList<DockBean.LockBean>> {
         val map = mutableMapOf<Byte, MutableList<DockBean.LockBean>>()
         if (needLockCount == 0) {
             return map
         }
         val lockDockList =
             dockList.filter { it.type == DOCK_TYPE_LOCK || it.type == DOCK_TYPE_PORTABLE }
-
+        lockDockList.sortedBy { it.addr }
         var provideCount = 0
-        for (lockDock in lockDockList) {
+        LogUtil.i("异常锁rfid:${exceptionLocks}")
+        LogUtil.i("异常锁仓位:${exceptionSlots.joinToString(",") { "${it.row},${it.col}" }}")
+        for (lockDockIndex in lockDockList.indices) {
             if (provideCount >= needLockCount) break
 
-            val validLocks = lockDock.getLockList().filter { it.isExist }
+            val validLocks =
+                lockDockList[lockDockIndex].getLockList().filter { it.rfid !in exceptionLocks }
+                    .filter {
+                        it.isExist && it.idx !in exceptionSlots.filter { it.row?.toInt() == (lockDockIndex + 2) }
+                            .map { (it.col?.toInt() ?: 1) - 1 }
+                    }
             val toTake = (needLockCount - provideCount).coerceAtMost(validLocks.size)
             if (toTake > 0) {
-                map[lockDock.addr] = validLocks.take(toTake).toMutableList()
+                map[lockDockList[lockDockIndex].addr] = validLocks.take(toTake).toMutableList()
                 provideCount += toTake
             }
         }
+        LogUtil.i("待取锁:${map}")
         return map
     }
 
@@ -946,8 +1047,10 @@ object ModBusController {
      * 获取开关量数据
      */
     fun getSwitchData(): MutableList<DockBean.SwitchBean> {
-        return dockList.filter { it.type == DOCK_TYPE_COLLECT }.map { it.getSwitchList() }.flatten()
-            .toMutableList()
+        return dockList.filter { it.type == DOCK_TYPE_COLLECT }.sortedBy { it.addr }
+            .flatMap { it.getSwitchList() }.mapIndexed { index, switchBean ->
+                DockBean.SwitchBean(index, switchBean.switchBoardAddr, switchBean.enabled)
+            }.toMutableList()
     }
 
     /**
@@ -958,28 +1061,9 @@ object ModBusController {
     ) {
         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)
-//                }
+                modBusManager?.sendTo(it, cmd) { res ->
+                    done?.invoke(res)
+                }
             }
         }
     }

+ 0 - 14
app/src/main/java/com/grkj/iscs/modbus/ModBusManager.kt

@@ -170,20 +170,6 @@ class ModBusManager(
         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)
-                }
                 if (running) done?.invoke(results)
             } else sendUp(index + 1, frame, done, results)
         }

+ 51 - 0
app/src/main/java/com/grkj/iscs/model/DictConstants.kt

@@ -0,0 +1,51 @@
+package com.grkj.iscs.model
+
+/**
+ * 字典参数
+ */
+object DictConstants {
+    /**
+     * 仓位状态
+     */
+    const val KEY_SLOT_STATUS = "slot_status"
+
+    /**
+     * 仓位是否被占用
+     */
+    const val KEY_IS_OCCUPIED_STATUS = "isOccupied_status"
+
+    /**
+     * 硬件工卡异常原因
+     */
+    const val KEY_JOB_CARD_REASON = "job_card_reason"
+
+    /**
+     * 挂锁异常原因
+     */
+    const val KEY_PAD_LOCK_REASON = "padlock_reason"
+
+    /**
+     * 挂锁状态
+     */
+    const val KEY_PAD_LOCK_STATUS = "padlock_status"
+
+    /**
+     * 钥匙异常原因
+     */
+    const val KEY_KEY_REASON = "key_reason"
+
+    /**
+     * 钥匙状态
+     */
+    const val KEY_KEY_STATUS = "key_status"
+
+    /**
+     * 开关状态
+     */
+    const val KEY_SWITCH_STATUS = "switch_status"
+
+    /**
+     * 锁仓类型
+     */
+    const val KEY_SLOT_TYPE = "slot_type"
+}

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

@@ -4,7 +4,8 @@ object UrlConsts {
     //    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://192.168.0.10:9190"    // 外
+    const val BASE_URL = "http://192.168.1.121:9190"    // 外
 
     //    const val BASE_URL = "http://120.27.232.27:9190"    // 外
     const val WEB_SOCKET = "ws://192.168.1.127:9090/websocket/iot/127"
@@ -29,7 +30,7 @@ object UrlConsts {
     /**
      * 字典前缀
      */
-    private const val DICT_PREFIX = "/system/dict/data/type"
+    const val DICT_PREFIX = "/system/dict/data/type"
 
     /**
      * 查询字典 - 工作票类型
@@ -290,5 +291,45 @@ object UrlConsts {
     /**
      * 查询锁控机位-仓位-分页
      */
-    const val GET_IS_LOCK_CABINET_SLOTS_PAGE = "/dev-api/iscs/slots/getIsLockCabinetSlotsPage"
+    const val GET_IS_LOCK_CABINET_SLOTS_PAGE = "/iscs/slots/getIsLockCabinetSlotsPage"
+
+    /**
+     * 获取锁柜列表
+     */
+    const val GET_IS_LOCK_CABINET_PAGE = "/iscs/cabinet/getIsLockCabinetPage"
+
+    /**
+     * 根据键值获取系统参数
+     */
+    const val GET_IS_SYSTEM_ATTRIBUTE_BY_KEY = "/iscs/attribute/getIsSystemAttributeByKey"
+
+    /**
+     * 更新开关状态
+     */
+    const val UPDATE_SWITCH_LIST = "/iscs/hardware-api/updateSwitchList"
+
+    /**
+     * 获取钥匙列表
+     */
+    const val GET_IS_KEY_PAGE = "/iscs/key/getIsKeyPage"
+
+    /**
+     * 获取锁具列表
+     */
+    const val GET_IS_LOCK_PAGE = "/iscs/lock/getIsLockPage"
+
+    /**
+     * 获取工卡列表
+     */
+    const val GET_IS_JOB_CARD_PAGE = "/iscs/card/getIsJobCardPage"
+
+    /**
+     * 获取RFID标识列表
+     */
+    const val GET_IS_RFID_TOKEN_PAGE = "/iscs/token/getIsRfidTokenPage"
+
+    /**
+     * 获取锁定站列表
+     */
+    const val GET_IS_LOTO_STATION_PAGE = "/iscs/station/getIsLotoStationPage"
 }

+ 4 - 1
app/src/main/java/com/grkj/iscs/model/eventmsg/MsgEventConstants.kt

@@ -4,7 +4,7 @@ object MsgEventConstants {
 
     // ------------------------------ 通用 1-001-000 ------------------------------
     const val MSG_EVENT_LOADING = 1_001_000             // loading消息
-    
+
     // ------------------------------ 设备 1-002-000 ------------------------------
     const val MSG_EVENT_DEVICE_TAKE_UPDATE = 1_002_000  // 设备取出
 
@@ -18,4 +18,7 @@ object MsgEventConstants {
 
     // ------------------------------ 设备异常 1-005-000 ------------------------------
     const val MSG_EVENT_DEVICE_EXCEPTION = 1_005_000    // 钥匙出现异常
+
+    // ------------------------------ 开关量采集更新 1-006-000 ------------------------------
+    const val MSG_EVENT_SWITCH_COLLECTION_UPDATE = 1_006_000 //开关量采集更新
 }

+ 33 - 0
app/src/main/java/com/grkj/iscs/model/vo/cabinet/LockCabinetPageRespVO.kt

@@ -0,0 +1,33 @@
+package com.grkj.iscs.model.vo.cabinet
+
+/**
+ * 锁柜列表实体
+ */
+data class LockCabinetPageRespVO(
+    val pages: Int,
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<LockCabinet> = listOf()
+) {
+    data class LockCabinet(
+        val createBy: String?,
+        val createTime: String?,
+        val updateBy: String?,
+        val updateTime: String?,
+        val remark: String?,
+        val paramMap: Map<String, String>? = mapOf(),
+        val cabinetId: String?,
+        val cabinetCode: String?,
+        val cabinetName: String?,
+        val hardwareId: String?,
+        val workareId: String?,
+        val workstationId: String?,
+        val cabinetIcon: String?,
+        val cabinetPicture: String?,
+        val isOnline: String?,
+        val delFlag: String?,
+        val status: String?,
+        val serialNumber: String?
+    )
+}

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

@@ -7,8 +7,8 @@ data class CabinetSlotsRespVo(
     val countId: String,
     val current: Int,
     val maxLimit: Int,
-    val optimizeCountSql: Boolean,
-    val orders: List<Order>,
+    val optimizeCountSql: Boolean?,
+    val orders: List<Order>?,
     val pages: Int,
     val records: List<CabinetSlotsRecord>,
     val searchCount: Boolean,
@@ -17,25 +17,25 @@ data class CabinetSlotsRespVo(
 )
 
 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
+    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
+    val asc: Boolean?,
+    val column: String?
 )

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

@@ -0,0 +1,17 @@
+package com.grkj.iscs.model.vo.hardware
+
+data class JobCardPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<JobCardPageItem>
+)
+
+data class JobCardPageItem(
+    val cardId: String?,
+    val cardNfc: String?,
+    val nickName: String?,
+    val cardCode: String?,
+    val exStatus: String?,
+    val exRemark: String?
+)

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

@@ -0,0 +1,16 @@
+package com.grkj.iscs.model.vo.hardware
+
+data class RfidTokenPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<RfidTokenPageItem>
+)
+
+data class RfidTokenPageItem(
+    val rfidId: String?,
+    val rfid: String?,
+    val rfidCode: String?,
+    val status: String?,
+    val remark: String?
+)

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

@@ -0,0 +1,10 @@
+package com.grkj.iscs.model.vo.hardware
+
+/**
+ * 开关状态请求实体
+ */
+data class SwitchListReqVO(
+    val pointSerialNumber: String,
+    val switchStatus: String?,
+    val switchLastUpdateTime: String?
+)

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

@@ -25,4 +25,14 @@ data class LockExDTO(
     val lockNfc: String,
     val exRemark: String,
     val exStatus: String
+)
+
+/**
+ * 锁仓异常数据
+ */
+data class SlotExDTO(
+    val col: String,
+    val remark: String,
+    val row: String,
+    val status: String
 )

+ 17 - 0
app/src/main/java/com/grkj/iscs/model/vo/key/KeyPageRespVO.kt

@@ -0,0 +1,17 @@
+package com.grkj.iscs.model.vo.key
+
+data class KeyPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<KeyPageItem>
+)
+
+data class KeyPageItem(
+    val keyId: String?,
+    val keyNfc: String?,
+    val macAddress: String?,
+    val keyName: String?,
+    val exStatus: String?,
+    val exRemark: String?
+)

+ 16 - 0
app/src/main/java/com/grkj/iscs/model/vo/lock/LockPageRespVO.kt

@@ -0,0 +1,16 @@
+package com.grkj.iscs.model.vo.lock
+
+data class LockPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<LockPageItem>
+)
+
+data class LockPageItem(
+    val lockId: String?,
+    val lockNfc: String?,
+    val lockName: String?,
+    val exStatus: String?,
+    val exRemark: String?
+)

+ 13 - 0
app/src/main/java/com/grkj/iscs/model/vo/machinery/IsLotoStationPageRespVO.kt

@@ -0,0 +1,13 @@
+package com.grkj.iscs.model.vo.machinery
+
+data class IsLotoStationPageRespVO(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<IsLotoStationPageItem>
+)
+
+data class IsLotoStationPageItem(
+    val lotoSerialNumber: String?,
+    val mapId: String?
+)

+ 3 - 1
app/src/main/java/com/grkj/iscs/model/vo/map/MapInfoRespVO.kt

@@ -42,6 +42,8 @@ data class MapInfoRespVO(
 
         val pointPicture: String?,
 
-        val pointNfc: String?
+        val pointNfc: String?,
+
+        val pointSerialNumber: String?,
     )
 }

+ 12 - 0
app/src/main/java/com/grkj/iscs/model/vo/system/SystemAttributeByKeyRespVO.kt

@@ -0,0 +1,12 @@
+package com.grkj.iscs.model.vo.system
+
+/**
+ * 系统参数返回
+ */
+data class SystemAttributeByKeyRespVO(
+    val remark: String?,
+    val sysAttrValue: String?,
+    val sysAttrType: String?,
+    val sysAttrName: String?,
+    val sysAttrKey: String?,
+)

+ 261 - 8
app/src/main/java/com/grkj/iscs/util/NetApi.kt

@@ -1,10 +1,12 @@
 package com.grkj.iscs.util
 
+import android.graphics.Bitmap
 import com.grkj.iscs.BusinessManager
 import com.grkj.iscs.MyApplication
 import com.grkj.iscs.model.Token
 import com.grkj.iscs.model.UrlConsts
 import com.grkj.iscs.model.vo.FileStreamReqParam
+import com.grkj.iscs.model.vo.cabinet.LockCabinetPageRespVO
 import com.grkj.iscs.model.vo.card.CardInfoRespVO
 import com.grkj.iscs.model.vo.characteristic.CharacteristicPageRespVO
 import com.grkj.iscs.model.vo.dept.DeptListRespVO
@@ -12,17 +14,25 @@ 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.JobCardPageRespVO
 import com.grkj.iscs.model.vo.hardware.KeyExDTO
 import com.grkj.iscs.model.vo.hardware.LockExDTO
+import com.grkj.iscs.model.vo.hardware.RfidTokenPageRespVO
+import com.grkj.iscs.model.vo.hardware.SlotExDTO
+import com.grkj.iscs.model.vo.hardware.SwitchListReqVO
 import com.grkj.iscs.model.vo.key.KeyInfoRespVO
+import com.grkj.iscs.model.vo.key.KeyPageRespVO
 import com.grkj.iscs.model.vo.lock.LockInfoRespVO
+import com.grkj.iscs.model.vo.lock.LockPageRespVO
 import com.grkj.iscs.model.vo.lock.LockTakeUpdateReqVO
+import com.grkj.iscs.model.vo.machinery.IsLotoStationPageRespVO
 import com.grkj.iscs.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs.model.vo.machinery.MachineryPageRespVO
 import com.grkj.iscs.model.vo.map.MapInfoRespVO
 import com.grkj.iscs.model.vo.map.MapPointPageRespVO
 import com.grkj.iscs.model.vo.sop.SopInfoRespVO
 import com.grkj.iscs.model.vo.sop.SopPageRespVO
+import com.grkj.iscs.model.vo.system.SystemAttributeByKeyRespVO
 import com.grkj.iscs.model.vo.system.SystemAttributePageRespVO
 import com.grkj.iscs.model.vo.ticket.LockPointUpdateReqVO
 import com.grkj.iscs.model.vo.ticket.LotoMapRespVO
@@ -37,9 +47,12 @@ import com.grkj.iscs.model.vo.user.RoleListRespVO
 import com.grkj.iscs.model.vo.user.UserInfoRespVO
 import com.grkj.iscs.model.vo.user.UserListRespVO
 import com.grkj.iscs.util.log.LogUtil
+import kotlinx.coroutines.runBlocking
 import java.text.SimpleDateFormat
 import java.util.Calendar
 import java.util.Locale
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 /**
  * 网络请求
@@ -989,6 +1002,7 @@ object NetApi {
         exceptionType: String,
         raiser: Long,
         sourceName: String,
+        hardwareId: String,
         callBack: (Boolean) -> Unit
     ) {
         val map = mutableMapOf(
@@ -997,6 +1011,8 @@ object NetApi {
             "exceptionType" to exceptionType,
             "raiser" to raiser,
             "sourceName" to sourceName,
+            "parameters" to hardwareId,//设备id
+            "exceptionCategory" to 1,
             "raiseTime" to SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(
                 Calendar.getInstance().time
             )
@@ -1122,9 +1138,10 @@ object NetApi {
      * 批量更新硬件状态
      */
     fun updateHardwareEsStatus(
-        jobCardExDTOList: List<JobCardExDTO>,
-        keyExDTOList: List<KeyExDTO>,
-        lockExDTOList: List<LockExDTO>,
+        jobCardExDTOList: List<JobCardExDTO> = mutableListOf(),
+        keyExDTOList: List<KeyExDTO> = mutableListOf(),
+        lockExDTOList: List<LockExDTO> = mutableListOf(),
+        slotsExDTOList: List<SlotExDTO> = mutableListOf(),
         callBack: (Boolean) -> Unit
     ) {
         NetHttpManager.getInstance().doRequestNet(
@@ -1133,7 +1150,8 @@ object NetApi {
             mapOf(
                 "jobCardExDTOList" to jobCardExDTOList,
                 "keyExDTOList" to keyExDTOList,
-                "lockExDTOList" to lockExDTOList
+                "lockExDTOList" to lockExDTOList,
+                "slotsExDTOList" to slotsExDTOList
             ),
             { res, _, _ ->
                 res?.let {
@@ -1172,16 +1190,15 @@ object NetApi {
      * 获取锁柜机柜-仓位-分页
      */
     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
+                "current" to 1,
+                "size" to 50,
+                "cabinetId" to SPUtils.getCabinetId()
             ),
             { res, _, _ ->
                 res?.let {
@@ -1190,4 +1207,240 @@ object NetApi {
             }, isGet = true, isAuth = true
         )
     }
+
+    /**
+     * 根据key获取系统参数
+     */
+    fun getIsSystemAttributeByKey(
+        key: String,
+        callBack: (SystemAttributeByKeyRespVO?) -> Unit
+    ) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_SYSTEM_ATTRIBUTE_BY_KEY,
+            false,
+            mapOf(
+                "sysAttrKey" to key
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack(getRefBean(it))
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取系统图标
+     */
+    fun getIsSystemAttributeIconByKey(key: String, callBack: (Bitmap?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_SYSTEM_ATTRIBUTE_BY_KEY,
+            false,
+            mapOf(
+                "sysAttrKey" to key
+            ),
+            { res, _, _ ->
+                res?.let {
+                    val bean: SystemAttributeByKeyRespVO? = getRefBean(it)
+                    LogUtil.i("资源信息:${bean}")
+                    bean?.sysAttrValue?.let { url ->
+                        BitmapUtil.loadBitmapFromUrl(
+                            MyApplication.instance?.applicationContext!!,
+                            url,
+                            callback = callBack
+                        )
+                    }
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取字典数据
+     */
+    suspend fun getDictData(dictKey: String): MutableList<CommonDictRespVO>? {
+        return suspendCoroutine { cont ->
+            getDictData("${UrlConsts.DICT_PREFIX}/${dictKey}") {
+                cont.resume(it)
+            }
+        }
+    }
+
+    /**
+     * 异常上报
+     */
+    fun reportException(
+        row: Int,
+        col: Int,
+        slotType: Int,
+        exceptionReason: String,
+        callBack: ((Boolean) -> Unit)? = null
+    ) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_SYSTEM_ATTRIBUTE_BY_KEY,
+            false,
+            mapOf(
+                "cabinetId" to SPUtils.getCabinetId(),
+                "col" to col,
+                "row" to row,
+                "row" to row,
+                "slotType" to slotType,
+                "status" to 1,
+                "remark" to exceptionReason,
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack?.invoke(true)
+                } ?: run {
+                    callBack?.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 获取锁柜列表
+     */
+    fun getIsLockCabinetPage(callBack: (LockCabinetPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_LOCK_CABINET_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 批量更新开关状态
+     */
+    fun updateSwitchList(switchList: List<SwitchListReqVO>, callBack: (Boolean) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.UPDATE_SWITCH_LIST,
+            false,
+            mapOf(
+                "list" to switchList
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 获取钥匙列表
+     */
+    fun getIsKeyPage(callBack: (KeyPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_KEY_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取挂锁列表
+     */
+    fun getIsLockPage(callBack: (LockPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_LOCK_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取工卡列表
+     */
+    fun getIsJobCardPage(callBack: (JobCardPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_JOB_CARD_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取Rfid标识列表
+     */
+    fun getIsRfidTokenPage(callBack: (RfidTokenPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_RFID_TOKEN_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
+
+    /**
+     * 获取Rfid标识列表
+     */
+    fun getIsLotoStationPage(callBack: (IsLotoStationPageRespVO?) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.GET_IS_LOTO_STATION_PAGE,
+            false,
+            mapOf(
+                "current" to 1,
+                "size" to 50
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(getRefBean(it))
+                } ?: run {
+                    callBack.invoke(null)
+                }
+            }, isGet = true, isAuth = true
+        )
+    }
 }

+ 19 - 0
app/src/main/java/com/grkj/iscs/util/SPUtils.kt

@@ -12,6 +12,7 @@ import com.grkj.iscs.model.vo.user.UserInfoRespVO
 import com.grkj.iscs.util.log.LogUtil
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.saveMMKVData
+import com.sik.sikcore.extension.toJson
 import com.tencent.mmkv.MMKV
 
 object SPUtils {
@@ -45,6 +46,10 @@ object SPUtils {
 
     private const val KEY_TICKET_TAKE_LOCK_EXCEPTION = "ticket_take_lock_exception"
 
+    private const val KEY_CABINET_ID = "key_cabinet_id"
+
+    private const val KEY_USERINFO = "key_userinfo"
+
     fun getLoginUser(context: Context): LoginUserBO? {
         val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
         if (sp.getLong(KEY_LOGIN_USER_USER_ID, -1) == -1L) {
@@ -290,4 +295,18 @@ object SPUtils {
     fun resetTicketTakeLockException(ticketId: Long) {
         MMKV.defaultMMKV().remove("${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}")
     }
+
+    /**
+     * 锁柜id
+     */
+    fun getCabinetId(): String {
+        return KEY_CABINET_ID.getMMKVData("2")
+    }
+
+    /**
+     * 保存锁柜id
+     */
+    fun saveCabinetId(cabinetId: String) {
+        KEY_CABINET_ID.saveMMKVData(cabinetId)
+    }
 }

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

@@ -10,6 +10,7 @@ import com.grkj.iscs.R
 import com.grkj.iscs.databinding.ActivityHomeBinding
 import com.grkj.iscs.extentions.toByteArrays
 import com.grkj.iscs.extentions.toHexStrings
+import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.Constants.USER_ROLE_ADMHDWSETTER
 import com.grkj.iscs.model.Constants.USER_ROLE_ADMHDWTESTER
 import com.grkj.iscs.model.Constants.USER_ROLE_COLOCKER
@@ -21,6 +22,7 @@ import com.grkj.iscs.model.eventmsg.DeviceExceptionMsg
 import com.grkj.iscs.model.eventmsg.MsgEvent
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_DEVICE_EXCEPTION
 import com.grkj.iscs.model.vo.user.UserInfoRespVO
+import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.adapter.MenuAdapter
 import com.grkj.iscs.view.base.BaseFragment
@@ -52,9 +54,9 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
 
     override fun initView() {
         presenter?.registerStatusListener()
-
+        presenter?.getAndSaveCabinetId()
         val userInfo = intent.getSerializableExtra("userInfo")
-
+        
         BusinessManager.isTestMode = false
         if (userInfo != null && (userInfo as UserInfoRespVO).roles != null) {
             if (userInfo.roles?.any { it == USER_ROLE_DRAWER || it == USER_ROLE_LOCKER || it == USER_ROLE_COLOCKER || it == USER_ROLE_GUARD } == true) {
@@ -123,6 +125,8 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
         }
 
         BusinessManager.mEventBus.observe(this, observer)
+        ModBusController.updateAllBuckleStatus{}
+        ModBusController.updateSwitchStatus{}
     }
 
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {

+ 1 - 0
app/src/main/java/com/grkj/iscs/view/activity/LoginActivity.kt

@@ -17,6 +17,7 @@ import com.grkj.iscs.util.FingerprintUtil
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BaseMvpActivity
+import com.grkj.iscs.view.dialog.CabinetSerialNoDialog
 import com.grkj.iscs.view.dialog.LoginDialog
 import com.grkj.iscs.view.dialog.UrlConfigDialog
 import com.grkj.iscs.view.iview.ILoginView

+ 27 - 0
app/src/main/java/com/grkj/iscs/view/dialog/CabinetSerialNoDialog.kt

@@ -0,0 +1,27 @@
+package com.grkj.iscs.view.dialog
+
+import android.content.Context
+import com.grkj.iscs.databinding.DialogCabinetSerialNoBinding
+import com.grkj.iscs.extentions.serialNo
+import com.grkj.iscs.view.base.BaseDialog
+
+/**
+ * 锁柜序列号弹框
+ */
+class CabinetSerialNoDialog(ctx: Context) : BaseDialog<DialogCabinetSerialNoBinding>(ctx) {
+
+    override val viewBinding: DialogCabinetSerialNoBinding
+        get() = DialogCabinetSerialNoBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        mBinding?.serialNo?.text = context.serialNo()
+
+        mBinding?.btnConfirm?.setOnClickListener {
+            dismiss()
+        }
+
+        mBinding?.btnCancel?.setOnClickListener {
+            dismiss()
+        }
+    }
+}

+ 49 - 0
app/src/main/java/com/grkj/iscs/view/dialog/SlotExceptionDialog.kt

@@ -0,0 +1,49 @@
+package com.grkj.iscs.view.dialog
+
+import android.content.Context
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.DialogSlotExceptionBinding
+import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.view.base.BaseDialog
+
+/**
+ * 仓位异常弹框
+ */
+class SlotExceptionDialog(
+    ctx: Context,
+    val row: Int,
+    val col: Int,
+    val slotType: Int,
+    val onConfirm: (exceptionReason: String) -> Unit
+) : BaseDialog<DialogSlotExceptionBinding>(ctx) {
+    override val viewBinding: DialogSlotExceptionBinding
+        get() = DialogSlotExceptionBinding.inflate(layoutInflater)
+
+    override fun initView() {
+        mBinding?.hardwareInfo?.text = context.getString(
+            R.string.hardware_info,
+            "${getDeviceTypeStr(slotType)},${context.getString(R.string.number)} 行${row},列${col}"
+        )
+        mBinding?.btnConfirm?.setOnClickListener {
+            val exceptionReason = mBinding?.et?.text?.toString()
+            if (exceptionReason?.isEmpty() == true) {
+                ToastUtils.tip(R.string.please_input_exception_reason)
+                return@setOnClickListener
+            }
+            onConfirm(exceptionReason!!)
+            dismiss()
+        }
+
+        mBinding?.btnCancel?.setOnClickListener {
+            dismiss()
+        }
+    }
+
+    private fun getDeviceTypeStr(slotType: Int): String {
+        return when (slotType) {
+            0 -> context.getString(R.string.hardware_key)
+            1 -> context.getString(R.string.hardware_lock)
+            else -> context.getString(R.string.hardware_unknown)
+        }
+    }
+}

+ 243 - 20
app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt

@@ -3,19 +3,23 @@ package com.grkj.iscs.view.fragment
 import android.content.Context
 import android.content.res.ColorStateList
 import android.view.View
+import android.widget.ImageView
 import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
-import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.MyApplication
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDeviceStatusBinding
 import com.grkj.iscs.extentions.setSelected
 import com.grkj.iscs.extentions.setVisibleWithHolder
 import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.Constants.USER_ROLE_ADMHDWTESTER
 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.SPUtils
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseMvpFragment
+import com.grkj.iscs.view.dialog.SlotExceptionDialog
+import com.grkj.iscs.view.dialog.TipDialog
 import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
 import com.grkj.iscs.view.iview.IDeviceStatusView
 import com.grkj.iscs.view.presenter.DeviceStatusPresenter
@@ -24,6 +28,7 @@ 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.Dispatchers
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.withContext
 
@@ -32,14 +37,74 @@ import kotlinx.coroutines.withContext
  */
 class DeviceStatusFragment :
     BaseMvpFragment<IDeviceStatusView, DeviceStatusPresenter, FragmentDeviceStatusBinding>() {
+    private val tipDialog: TipDialog by lazy { TipDialog(requireContext()) }
     private var mRowList = mutableListOf<DockStatusBO>()
 
     override val viewBinding: FragmentDeviceStatusBinding
         get() = FragmentDeviceStatusBinding.inflate(layoutInflater)
 
     override fun initView() {
+        presenter?.loginUser = SPUtils.getLoginUser(requireContext())
         presenter?.initData(mRowList)
-
+        presenter?.getSlotData {
+            presenter?.getExceptionIcon {
+                ThreadUtils.runOnMain {
+                    mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+                }
+            }
+        }
+        presenter?.mExceptionHintTip = {
+            tipDialog.setType(TipDialog.TYPE_HINT)
+            tipDialog.setTip(it)
+            tipDialog.showCancelCountdown(10)
+        }
+        presenter?.mExceptionRecovery = { row, col, slotType ->
+            tipDialog.setType(TipDialog.TYPE_CONFIRM)
+            tipDialog.setTip(
+                requireContext().getString(
+                    R.string.is_device_recovery_check,
+                    getDeviceTypeStr(slotType),
+                    "${requireContext().getString(R.string.number)} 行${row},列${col}"
+                )
+            )
+            tipDialog.setConfirmListener {
+                presenter?.slotStatus?.find { it.dictLabel == "可用" }?.dictValue?.let { exceptionStatus ->
+                    presenter?.reportException(
+                        row,
+                        col,
+                        exceptionStatus.toInt(),
+                        ""
+                    ) {
+                        if (it) {
+                            ToastUtils.tip(R.string.recovery_success)
+                        } else {
+                            ToastUtils.tip(R.string.recovery_failed)
+                        }
+                        getSlotsStatus()
+                    }
+                }
+            }
+            tipDialog.showCancelCountdown(10)
+        }
+        presenter?.mExceptionReporter = { row, col, slotType ->
+            SlotExceptionDialog(requireContext(), row, col, slotType) { exceptionReason ->
+                presenter?.slotStatus?.find { it.dictLabel == "异常" }?.dictValue?.let { exceptionStatus ->
+                    presenter?.reportException(
+                        row,
+                        col,
+                        exceptionStatus.toInt(),
+                        exceptionReason
+                    ) {
+                        if (it) {
+                            ToastUtils.tip(R.string.report_success)
+                        } else {
+                            ToastUtils.tip(R.string.report_failed)
+                        }
+                        getSlotsStatus()
+                    }
+                }
+            }.show()
+        }
         val adapter = MultiItemTypeAdapter(requireContext(), mRowList)
         adapter.addItemViewDelegate(KeyDockItemDelegate(requireContext(), presenter))
         adapter.addItemViewDelegate(
@@ -55,20 +120,40 @@ class DeviceStatusFragment :
 
     override fun onResume() {
         super.onResume()
-        mBinding?.rvDock?.adapter?.notifyDataSetChanged()
-        fun refreshBuckleStatus() {
-            ThreadUtils.runOnIODelayed(1000) {
-                BusinessManager.updateAllBuckleStatus {
-                    ThreadUtils.runOnMain {
+
+        fun refreshAdapter() {
+            ThreadUtils.runOnIO {
+                if (isResumed) {
+                    withContext(Dispatchers.Main) {
                         mBinding?.rvDock?.adapter?.notifyDataSetChanged()
-                        refreshBuckleStatus()
                     }
+                    delay(1000)
+                    refreshAdapter()
                 }
             }
+
         }
-        refreshBuckleStatus()
+        refreshAdapter()
+        getSlotsStatus()
     }
 
+    private fun getSlotsStatus() {
+        presenter?.getIsLockCabinetSlotsPage {
+            ThreadUtils.runOnMain {
+                mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+            }
+        }
+    }
+
+    private fun getDeviceTypeStr(slotType: Int): String {
+        return when (slotType) {
+            0 -> requireContext().getString(R.string.hardware_key)
+            1 -> requireContext().getString(R.string.hardware_lock)
+            else -> requireContext().getString(R.string.hardware_unknown)
+        }
+    }
+
+
     override fun initPresenter(): DeviceStatusPresenter {
         return DeviceStatusPresenter()
     }
@@ -124,10 +209,10 @@ class DeviceStatusFragment :
             holder?.setText(R.id.tv_status_2, status2?.second)
             holder?.setText(R.id.tv_status_3, status3?.second)
             holder?.setText(R.id.tv_status_4, status4?.second)
-            holder?.setVisibleWithHolder(R.id.tv_repair_1, status1?.first == false)
-            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?.setVisibleWithHolder(R.id.tv_repair_1, status1?.first == false)
+//            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,
@@ -172,20 +257,128 @@ class DeviceStatusFragment :
             holder?.setOnClickListener(R.id.tv_repair_4) {
                 presenter?.repairKey(row.dockList.find { it.column == "2" }?.address, false)
             }
+            presenter?.mCabinetSlotsData?.filter { it.row == row.row.toString() }
+                ?.forEach { cabinetSlotsRecord ->
+                    when (cabinetSlotsRecord.col) {
+                        "1" -> {
+                            if (cabinetSlotsRecord.status == "0") {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_1, false)
+                            } else {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_1, true)
+                                holder?.getView<ImageView>(R.id.iv_key_exception_1)
+                                    ?.setImageBitmap(presenter?.mExceptionIcon)
+                                holder?.setOnClickListener(R.id.iv_key_exception_1) {
+                                    presenter?.exceptionHint(cabinetSlotsRecord.remark ?: "")
+                                }
+                                holder?.setOnLongClickListener(R.id.iv_key_exception_1) {
+                                    presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                                        presenter?.mExceptionRecovery?.invoke(
+                                            row.row,
+                                            1,
+                                            it.toInt()
+                                        )
+                                    }
+                                    true
+                                }
+                            }
+                        }
+
+                        "2" -> {
+                            if (cabinetSlotsRecord.status == "0") {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_2, false)
+                            } else {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_2, true)
+                                holder?.getView<ImageView>(R.id.iv_key_exception_2)
+                                    ?.setImageBitmap(presenter?.mExceptionIcon)
+                                holder?.setOnClickListener(R.id.iv_key_exception_2) {
+                                    presenter?.exceptionHint(cabinetSlotsRecord.remark ?: "")
+                                }
+                                holder?.setOnLongClickListener(R.id.iv_key_exception_2) {
+                                    presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                                        presenter?.mExceptionRecovery?.invoke(
+                                            row.row,
+                                            2,
+                                            it.toInt()
+                                        )
+                                    }
+                                    true
+                                }
+                            }
+                        }
+
+                        "3" -> {
+                            if (cabinetSlotsRecord.status == "0") {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_3, false)
+                            } else {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_3, true)
+                                holder?.getView<ImageView>(R.id.iv_key_exception_3)
+                                    ?.setImageBitmap(presenter?.mExceptionIcon)
+                                holder?.setOnClickListener(R.id.iv_key_exception_3) {
+                                    presenter?.exceptionHint(cabinetSlotsRecord.remark ?: "")
+                                }
+                                holder?.setOnLongClickListener(R.id.iv_key_exception_3) {
+                                    presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                                        presenter?.mExceptionRecovery?.invoke(
+                                            row.row,
+                                            3,
+                                            it.toInt()
+                                        )
+                                    }
+                                    true
+                                }
+                            }
+                        }
+
+                        "4" -> {
+                            if (cabinetSlotsRecord.status == "0") {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_4, false)
+                            } else {
+                                holder?.setVisibleWithHolder(R.id.iv_key_exception_4, true)
+                                holder?.getView<ImageView>(R.id.iv_key_exception_4)
+                                    ?.setImageBitmap(presenter?.mExceptionIcon)
+                                holder?.setOnClickListener(R.id.iv_key_exception_4) {
+                                    presenter?.exceptionHint(cabinetSlotsRecord.remark ?: "")
+                                }
+                                holder?.setOnLongClickListener(R.id.iv_key_exception_4) {
+                                    presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                                        presenter?.mExceptionRecovery?.invoke(
+                                            row.row,
+                                            4,
+                                            it.toInt()
+                                        )
+                                    }
+                                    true
+                                }
+                            }
+                        }
+                    }
+                }
             holder?.setOnLongClickListener(R.id.iv_key_1) {
-                ToastUtils.tip("钥匙1上报异常")
+                presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                    presenter?.mExceptionReporter?.invoke(
+                        row.row,
+                        1,
+                        it.toInt()
+                    )
+                }
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_2) {
-                ToastUtils.tip("钥匙2上报异常")
+                presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                    presenter?.mExceptionReporter?.invoke(row.row, 2, it.toInt())
+                }
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_3) {
-                ToastUtils.tip("钥匙3上报异常")
+                presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                    presenter?.mExceptionReporter?.invoke(row.row, 3, it.toInt())
+                }
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_4) {
-                ToastUtils.tip("钥匙4上报异常")
+                presenter?.slotType?.find { it.dictLabel == "钥匙" }?.dictValue?.let {
+                    presenter?.mExceptionReporter?.invoke(row.row, 4, it.toInt())
+                }
                 true
             }
         }
@@ -225,7 +418,13 @@ class DeviceStatusFragment :
                         ModBusController.isLockExist(row.dockList[0].address, lockIdx)
                     )
                     holder?.convertView?.setOnLongClickListener {
-                        ToastUtils.tip("锁${row.dockList[0].row},${row.dockList[0].column},${lockIdx}上报异常")
+                        presenter?.slotType?.find { it.dictLabel == "锁" }?.dictValue?.let {
+                            presenter?.mExceptionReporter?.invoke(
+                                row.row,
+                                (lockIdx + 1),
+                                it.toInt()
+                            )
+                        }
                         true
                     }
                     ColorStateList.valueOf(statusNotLightTintColor).let {
@@ -244,6 +443,30 @@ class DeviceStatusFragment :
                                 ColorStateList.valueOf(statusOpenTintColor)
                         }
                     }
+
+                    presenter?.mCabinetSlotsData?.filter { it.row == row.row.toString() }
+                        ?.find { it.col == (lockIdx + 1).toString() }?.let { cabinetSlotsRecord ->
+                            if (cabinetSlotsRecord.status == "0") {
+                                holder?.setVisibleWithHolder(R.id.iv_lock_exception, false)
+                            } else {
+                                holder?.setVisibleWithHolder(R.id.iv_lock_exception, true)
+                                holder?.getView<ImageView>(R.id.iv_lock_exception)
+                                    ?.setImageBitmap(presenter?.mExceptionIcon)
+                                holder?.setOnClickListener(R.id.iv_lock_exception) {
+                                    presenter?.exceptionHint(cabinetSlotsRecord.remark ?: "")
+                                }
+                                holder?.setOnLongClickListener(R.id.iv_lock_exception) {
+                                    presenter?.slotType?.find { it.dictLabel == "锁" }?.dictValue?.let {
+                                        presenter?.mExceptionRecovery?.invoke(
+                                            row.row,
+                                            (lockIdx + 1),
+                                            it.toInt()
+                                        )
+                                    }
+                                    true
+                                }
+                            }
+                        }
                 }
             }
         }
@@ -262,7 +485,7 @@ class DeviceStatusFragment :
         }
 
         override fun isForViewType(item: DockStatusBO?, position: Int): Boolean {
-            return item?.dockList?.isEmpty() == true
+            return item?.dockList?.isEmpty() == true || item?.dockList?.none { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_LOCK } == true
         }
     }
 }

+ 45 - 7
app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt

@@ -5,10 +5,13 @@ import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDockTestBinding
+import com.grkj.iscs.extentions.removeLeadingZeros
+import com.grkj.iscs.extentions.toHexStrings
 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.SPUtils
+import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseFragment
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
@@ -28,7 +31,10 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
         val dockConfigJson = SPUtils.getDockConfig(requireActivity())
         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()) {
                 mKeyDockList.addAll(tempList.filter { it.type == DOCK_TYPE_KEY })
                 mLockDockList.addAll(tempList.filter { it.type == DOCK_TYPE_LOCK })
@@ -48,12 +54,18 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
         }
 
         mBinding?.rvKey?.adapter = object :
-            CommonAdapter<DockTestBean>(requireActivity(), R.layout.item_rv_dock_test, mKeyDockList) {
+            CommonAdapter<DockTestBean>(
+                requireActivity(),
+                R.layout.item_rv_dock_test,
+                mKeyDockList
+            ) {
             override fun convert(holder: ViewHolder, dock: DockTestBean, position: Int) {
                 holder.setText(R.id.tv_address, "0x${String.format("%02X", dock.address)}")
-                val rvDevice = holder.getView<androidx.recyclerview.widget.RecyclerView>(R.id.rv_device)
+                val rvDevice =
+                    holder.getView<androidx.recyclerview.widget.RecyclerView>(R.id.rv_device)
                 rvDevice.adapter = object : CommonAdapter<Int>(
-                    requireActivity(), R.layout.item_rv_dock_test_child, dock.deviceList) {
+                    requireActivity(), R.layout.item_rv_dock_test_child, dock.deviceList
+                ) {
                     override fun convert(holder: ViewHolder, deviceIndex: Int, position: Int) {
                         holder.setText(R.id.tv_name, getString(R.string.device_index, deviceIndex))
                         holder.setOnClickListener(R.id.tv_turn_on) {
@@ -62,18 +74,34 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                         holder.setOnClickListener(R.id.tv_turn_off) {
                             ModBusController.controlKeyBuckle(false, deviceIndex == 1, dock.address)
                         }
+                        holder.setOnClickListener(R.id.tv_read) {
+                            ModBusController.readKeyRfid(
+                                dock.address,
+                                if (deviceIndex == 1) 0 else 1
+                            ) { isLeft, res ->
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                    .removeLeadingZeros()
+                                ToastUtils.tip("RFID: ${rfid}")
+                            }
+                        }
                     }
                 }
             }
         }
 
         mBinding?.rvLock?.adapter = object :
-            CommonAdapter<DockTestBean>(requireActivity(), R.layout.item_rv_dock_test, mLockDockList) {
+            CommonAdapter<DockTestBean>(
+                requireActivity(),
+                R.layout.item_rv_dock_test,
+                mLockDockList
+            ) {
             override fun convert(holder: ViewHolder, dock: DockTestBean, position: Int) {
                 holder.setText(R.id.tv_address, "0x${String.format("%02X", dock.address)}")
-                val rvDevice = holder.getView<androidx.recyclerview.widget.RecyclerView>(R.id.rv_device)
+                val rvDevice =
+                    holder.getView<androidx.recyclerview.widget.RecyclerView>(R.id.rv_device)
                 rvDevice.adapter = object : CommonAdapter<Int>(
-                    requireActivity(), R.layout.item_rv_dock_test_child, dock.deviceList) {
+                    requireActivity(), R.layout.item_rv_dock_test_child, dock.deviceList
+                ) {
                     override fun convert(holder: ViewHolder, deviceIndex: Int, position: Int) {
                         holder.setText(R.id.tv_name, getString(R.string.device_index, deviceIndex))
                         holder.setOnClickListener(R.id.tv_turn_on) {
@@ -82,6 +110,16 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                         holder.setOnClickListener(R.id.tv_turn_off) {
                             ModBusController.controlLockBuckle(false, dock.address, deviceIndex - 1)
                         }
+                        holder.setOnClickListener(R.id.tv_read) {
+                            ModBusController.readLockRfid(
+                                dock.address,
+                                deviceIndex - 1
+                            ) { res ->
+                                val rfid = res.copyOfRange(3, 11).toHexStrings(false)
+                                    .removeLeadingZeros()
+                                ToastUtils.tip("RFID: ${rfid}")
+                            }
+                        }
                     }
                 }
             }

+ 114 - 3
app/src/main/java/com/grkj/iscs/view/fragment/ExceptionReportFragment.kt

@@ -1,8 +1,13 @@
 package com.grkj.iscs.view.fragment
 
+import android.view.View
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentExceptionReportBinding
 import com.grkj.iscs.model.vo.dict.CommonDictRespVO
+import com.grkj.iscs.model.vo.hardware.JobCardPageItem
+import com.grkj.iscs.model.vo.hardware.RfidTokenPageItem
+import com.grkj.iscs.model.vo.key.KeyPageItem
+import com.grkj.iscs.model.vo.lock.LockPageItem
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.iview.IExceptionReportView
@@ -19,6 +24,11 @@ class ExceptionReportFragment :
     private val mLevelList = mutableListOf<CommonDictRespVO>()
     private var mTypeIdx = -1   // 选中的类型
     private var mLevelIdx = -1  // 选中的等级
+    private var mSelectDeviceId = ""
+    private var mKeyList: MutableList<KeyPageItem> = mutableListOf()
+    private var mLockList: MutableList<LockPageItem> = mutableListOf()
+    private var mJobCardList: MutableList<JobCardPageItem> = mutableListOf()
+    private var mRfidList: MutableList<RfidTokenPageItem> = mutableListOf()
 
     override val viewBinding: FragmentExceptionReportBinding
         get() = FragmentExceptionReportBinding.inflate(layoutInflater)
@@ -40,13 +50,109 @@ class ExceptionReportFragment :
             }
         }
 
-        mBinding?.siType?.setOnSpinnerSelectListener(object : SelectableInput.OnSpinnerSelectListener {
+        mBinding?.siType?.setOnSpinnerSelectListener(object :
+            SelectableInput.OnSpinnerSelectListener {
             override fun onSelect(str: String?, index: Int) {
+                mSelectDeviceId = ""
                 mTypeIdx = index
+                when (str) {
+                    "钥匙异常" -> {
+                        mBinding?.siDevice?.visibility = View.VISIBLE
+                        mBinding?.siDevice?.setTitle(getString(R.string.exception_report_hardware_key))
+                        presenter?.getKeyPage {
+                            mKeyList.clear()
+                            mKeyList.addAll(it?.records ?: mutableListOf())
+                            if (mKeyList.isEmpty()){
+                                mBinding?.siDevice?.setOptionList(mutableListOf())
+                            }
+                            mBinding?.siDevice?.setOptionList(mKeyList.map { "${it.keyName}(${it.keyNfc})" }
+                                ?.toMutableList() ?: mutableListOf())
+                        }
+                        mBinding?.siDevice?.setOnSpinnerSelectListener(object :
+                            SelectableInput.OnSpinnerSelectListener {
+                            override fun onSelect(str: String?, index: Int) {
+                                mSelectDeviceId =
+                                    mKeyList.find { it.keyNfc?.let { it1 -> str?.contains(it1) } == true }?.keyId
+                                        ?: ""
+                            }
+                        })
+                    }
+
+                    "挂锁异常" -> {
+                        mBinding?.siDevice?.visibility = View.VISIBLE
+                        mBinding?.siDevice?.setTitle(getString(R.string.exception_report_hardware_lock))
+                        presenter?.getLockPage {
+                            mLockList.clear()
+                            mLockList.addAll(it?.records ?: mutableListOf())
+                            if (mLockList.isEmpty()){
+                                mBinding?.siDevice?.setOptionList(mutableListOf())
+                            }
+                            mBinding?.siDevice?.setOptionList(mLockList.map { "${it.lockName}(${it.lockNfc})" }
+                                ?.toMutableList() ?: mutableListOf())
+                        }
+                        mBinding?.siDevice?.setOnSpinnerSelectListener(object :
+                            SelectableInput.OnSpinnerSelectListener {
+                            override fun onSelect(str: String?, index: Int) {
+                                mSelectDeviceId =
+                                    mLockList.find { it.lockNfc?.let { it1 -> str?.contains(it1) } == true }?.lockId
+                                        ?: ""
+                            }
+                        })
+                    }
+
+                    "工卡异常" -> {
+                        mBinding?.siDevice?.visibility = View.VISIBLE
+                        mBinding?.siDevice?.setTitle(getString(R.string.exception_report_hardware_job_card))
+                        presenter?.getJobCardPage {
+                            mJobCardList.clear()
+                            mJobCardList.addAll(it?.records?.filter { it.nickName != null }
+                                ?.toMutableList() ?: mutableListOf())
+                            if (mJobCardList.isEmpty()){
+                                mBinding?.siDevice?.setOptionList(mutableListOf())
+                            }
+                            mBinding?.siDevice?.setOptionList(mJobCardList.map { "${it.cardCode}(${it.cardNfc})" }.toMutableList() ?: mutableListOf())
+                        }
+                        mBinding?.siDevice?.setOnSpinnerSelectListener(object :
+                            SelectableInput.OnSpinnerSelectListener {
+                            override fun onSelect(str: String?, index: Int) {
+                                mSelectDeviceId =
+                                    mJobCardList.find { it.cardNfc?.let { it1 -> str?.contains(it1) } == true }?.cardId
+                                        ?: ""
+                            }
+                        })
+                    }
+
+                    "RFID异常" -> {
+                        mBinding?.siDevice?.visibility = View.VISIBLE
+                        mBinding?.siDevice?.setTitle(getString(R.string.exception_report_hardware_rfid_point))
+                        presenter?.getRfidTokenPage {
+                            mRfidList.clear()
+                            mRfidList.addAll(it?.records ?: mutableListOf())
+                            if (mRfidList.isEmpty()){
+                                mBinding?.siDevice?.setOptionList(mutableListOf())
+                            }
+                            mBinding?.siDevice?.setOptionList(mRfidList.map { "${it.rfidCode}(${it.rfid})" }
+                                ?.toMutableList() ?: mutableListOf())
+                        }
+                        mBinding?.siDevice?.setOnSpinnerSelectListener(object :
+                            SelectableInput.OnSpinnerSelectListener {
+                            override fun onSelect(str: String?, index: Int) {
+                                mSelectDeviceId =
+                                    mRfidList.find { it.rfid?.let { it1 -> str?.contains(it1) } == true }?.rfidId
+                                        ?: ""
+                            }
+                        })
+                    }
+
+                    else -> {
+                        mBinding?.siDevice?.visibility = View.GONE
+                    }
+                }
             }
         })
 
-        mBinding?.siLevel?.setOnSpinnerSelectListener(object : SelectableInput.OnSpinnerSelectListener {
+        mBinding?.siLevel?.setOnSpinnerSelectListener(object :
+            SelectableInput.OnSpinnerSelectListener {
             override fun onSelect(str: String?, index: Int) {
                 mLevelIdx = index
             }
@@ -61,10 +167,15 @@ class ExceptionReportFragment :
                 ToastUtils.tip(R.string.exception_level_tip)
                 return@setOnClickListener
             }
+            if (mBinding?.siDevice?.visibility == View.VISIBLE && mSelectDeviceId.isEmpty()) {
+                ToastUtils.tip(R.string.exception_select_hardware_tip)
+                return@setOnClickListener
+            }
             presenter?.insertException(
                 mBinding?.siDescription?.getText(),
                 mLevelList[mLevelIdx].dictValue!!,
-                mExceptionList[mTypeIdx].dictValue!!
+                mExceptionList[mTypeIdx].dictValue!!,
+                mSelectDeviceId
             ) {
                 ToastUtils.tip(R.string.exception_submit_success_tip)
             }

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

@@ -185,7 +185,6 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                 })
                 mBinding?.mapview?.addLayer(stationLayer)
                 stationLayer?.setRatio(mapRatio)
-                mBinding?.mapview?.refresh()
             }
 
             override fun onMapLoadFail() {
@@ -311,6 +310,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                                             pt.entityName!!,
                                             icon,
                                             pt.entityId!!.toLong(),
+                                            pt.pointSerialNumber,
                                             mMachineryDetail?.pointIdList?.contains(pt.entityId) == true
                                         )
                                     )

+ 117 - 1
app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt

@@ -1,12 +1,24 @@
 package com.grkj.iscs.view.fragment
 
+import android.graphics.PointF
+import com.grkj.iscs.BusinessManager
 import com.grkj.iscs.databinding.FragmentSwitchStatusBinding
+import com.grkj.iscs.util.BitmapUtil
+import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.iview.ISwitchStatusView
 import com.grkj.iscs.view.presenter.SwitchStatusPresenter
+import com.grkj.iscs.view.widget.CustomStationLayer
+import com.grkj.iscs.view.widget.CustomSwitchStationLayer
+import com.onlylemi.mapview.library.MapViewListener
+import com.sik.sikcore.thread.ThreadUtils
 
 class SwitchStatusFragment :
     BaseMvpFragment<ISwitchStatusView, SwitchStatusPresenter, FragmentSwitchStatusBinding>() {
+    private var mapRatio: Float = 1f
+    private var stationLayer: CustomSwitchStationLayer? = null
+    private val mStationList = mutableListOf<CustomSwitchStationLayer.IsolationPoint>()
     override fun initPresenter(): SwitchStatusPresenter {
         return SwitchStatusPresenter()
     }
@@ -15,6 +27,110 @@ class SwitchStatusFragment :
         get() = FragmentSwitchStatusBinding.inflate(layoutInflater)
 
     override fun initView() {
-        
+        initMap()
+    }
+
+    override fun onResume() {
+        super.onResume()
+        presenter?.getMapData {
+            getMap(it)
+        }
+        fun refreshSwitchStatus() {
+            if (!isResumed) {
+                return
+            }
+            ThreadUtils.runOnIODelayed(1000) {
+                BusinessManager.updateSwitchStatus {
+                    refreshSwitchStatus()
+                }
+            }
+        }
+        refreshSwitchStatus()
+    }
+
+    private fun getMap(mapId: String) {
+        presenter?.getMapInfo(mapId.toLong()) { itMapInfo ->
+            // 如果没有图 URL,直接返回
+            val imageUrl = itMapInfo?.imageUrl ?: return@getMapInfo
+
+            BitmapUtil.loadBitmapFromUrl(requireContext(), imageUrl) { mapBmp ->
+                if (mapBmp == null) {
+                    LogUtil.e("Map pic is null")
+                    return@loadBitmapFromUrl
+                }
+
+                // 清空旧点
+                mStationList.clear()
+
+                // 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?.filter { it.x != null && it.y != null }?.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
+                    // 异步加载点位图标,固定请求尺寸
+                    mStationList.add(
+                        CustomSwitchStationLayer.IsolationPoint(
+                            PointF(finalX, finalY),
+                            pt.entityName!!,
+                            null,
+                            pt.entityId!!.toLong(),
+                            pt.pointSerialNumber,
+                            false
+                        )
+                    )
+
+                    // 全部点都加载完后,设置给 layer 并绘制
+                    if (mStationList.size == itMapInfo.pointList.count { it.x != null && it.y != null }) {
+                        mBinding?.mapview?.loadMap(mapBmp)
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 初始化地图
+     */
+    private fun initMap() {
+        mBinding?.mapview?.isScaleAndRotateTogether = false
+        mBinding?.mapview?.setMapViewListener(object : MapViewListener {
+            override fun onMapLoadSuccess() {
+                if (stationLayer != null) {
+                    mBinding?.mapview?.currentRotateDegrees = 0f
+                    return
+                }
+                stationLayer = CustomSwitchStationLayer(mBinding?.mapview, mStationList)
+                mBinding?.mapview?.addLayer(stationLayer)
+                stationLayer?.setRatio(mapRatio)
+                stationLayer?.stopAnimation()
+                stationLayer?.startAnimation()
+            }
+
+            override fun onMapLoadFail() {
+                ToastUtils.tip("onMapLoadFail")
+            }
+        })
     }
 }

+ 0 - 14
app/src/main/java/com/grkj/iscs/view/fragment/WorkshopFragment.kt

@@ -83,20 +83,6 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                         }
                         mPointList.forEach { itPoint ->
                             itPoint.ticketList = it.filter { it.workstationId == itPoint.workstationId }.toMutableList()
-//                            itPoint.ticketList.forEach { itTicket ->
-//                                if (itTicket.ticketType == null) {
-//                                    itTicket.bitmap = BitmapUtil.getResizedBitmapFromMipmap(requireContext(), R.mipmap.ticket_type_placeholder, 60, 60)
-//                                } else {
-//                                    BitmapUtil.loadBitmapFromUrl(
-//                                        requireContext(),
-//                                        SPUtils.getAttributeValue(requireContext(), Constants.getTicketKey(itTicket.ticketType.toInt())),
-//                                        R.mipmap.ticket_type_placeholder,
-//                                        60, 60
-//                                    ) { itBitmap ->
-//                                        itTicket.bitmap = itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(requireContext(), R.mipmap.ticket_type_placeholder, 60, 60)
-//                                    }
-//                                }
-//                            }
                             itPoint.ticketList.take(4).forEachIndexed { index, itTicket ->
                                 if (itPoint.ticketList.size > 3 && index == 3) {
                                     BitmapUtil.loadBitmapFromUrl(

+ 84 - 5
app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt

@@ -1,5 +1,6 @@
 package com.grkj.iscs.view.presenter
 
+import android.graphics.Bitmap
 import com.clj.fastble.BleManager
 import com.google.gson.Gson
 import com.google.gson.reflect.TypeToken
@@ -10,6 +11,12 @@ import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
+import com.grkj.iscs.model.DictConstants
+import com.grkj.iscs.model.bo.LoginUserBO
+import com.grkj.iscs.model.vo.dict.CommonDictRespVO
+import com.grkj.iscs.model.vo.hardware.CabinetSlotsRecord
+import com.grkj.iscs.model.vo.hardware.SlotExDTO
+import com.grkj.iscs.model.vo.user.UserInfoRespVO
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.SPUtils
@@ -19,17 +26,25 @@ import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.fragment.DeviceStatusFragment.DockStatusBO
 import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
 import com.grkj.iscs.view.iview.IDeviceStatusView
+import com.sik.sikcore.thread.ThreadUtils
 
 class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
+    var mCabinetSlotsData: List<CabinetSlotsRecord> = listOf()
+    var mExceptionIcon: Bitmap? = null
+    var mExceptionHintTip: ((String) -> Unit)? = null
+    var mExceptionRecovery: ((row: Int, col: Int, slotType: Int) -> Unit)? = null
+    var mExceptionReporter: ((row: Int, col: Int, slotType: Int) -> Unit)? =
+        null
+    var slotType: MutableList<CommonDictRespVO>? = null
+    var slotStatus: MutableList<CommonDictRespVO>? = null
+    var loginUser: LoginUserBO? = null
 
     fun initData(rowList: MutableList<DockStatusBO>) {
         val dockConfigJson = SPUtils.getDockConfig(mContext!!)
         if (!dockConfigJson.isNullOrEmpty()) {
-            val tempList: MutableList<DockTestBean> =
-                Gson().fromJson(
-                    dockConfigJson,
-                    object : TypeToken<MutableList<DockTestBean>>() {}.type
-                )
+            val tempList: MutableList<DockTestBean> = Gson().fromJson(
+                dockConfigJson, object : TypeToken<MutableList<DockTestBean>>() {}.type
+            )
             if (tempList.isNotEmpty()) {
                 tempList.forEach { dock ->
                     try {
@@ -147,4 +162,68 @@ class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
     fun getLockBuckleLockEnabled(address: Byte, lockIdx: Int): Any {
         return ModBusController.getLockBuckleLockEnabled(address, lockIdx)
     }
+
+    /**
+     * 获取锁柜数据
+     */
+    fun getIsLockCabinetSlotsPage(callback: () -> Unit) {
+        NetApi.getIsLockCabinetSlotsPage {
+            it?.let {
+                mCabinetSlotsData = it.records
+                callback()
+            }
+        }
+    }
+
+    /**
+     * 获取异常的图标
+     */
+    fun getExceptionIcon(callback: () -> Unit) {
+        NetApi.getIsSystemAttributeIconByKey("icon.locker.exception") {
+            mExceptionIcon = it
+            callback()
+        }
+    }
+
+    /**
+     * 异常提醒
+     */
+    fun exceptionHint(hint: String) {
+        mExceptionHintTip?.invoke(hint)
+    }
+
+    /**
+     * 上报异常
+     */
+    fun reportException(
+        row: Int,
+        col: Int,
+        slotStatus: Int,
+        exceptionReason: String,
+        done: (Boolean) -> Unit
+    ) {
+        NetApi.updateHardwareEsStatus(
+            slotsExDTOList = mutableListOf(
+                SlotExDTO(
+                    col.toString(),
+                    exceptionReason,
+                    row.toString(),
+                    slotStatus.toString()
+                )
+            )
+        ) {
+            done(it)
+        }
+    }
+
+    /**
+     * 获取锁仓的字典数据
+     */
+    fun getSlotData(done: () -> Unit) {
+        ThreadUtils.runOnIO {
+            slotStatus = NetApi.getDictData(DictConstants.KEY_SLOT_STATUS)
+            slotType = NetApi.getDictData(DictConstants.KEY_SLOT_TYPE)
+            done()
+        }
+    }
 }

+ 35 - 1
app/src/main/java/com/grkj/iscs/view/presenter/ExceptionReportPresenter.kt

@@ -4,6 +4,10 @@ import com.grkj.iscs.extentions.serialNo
 import com.grkj.iscs.model.Constants.DEVICE_TYPE
 import com.grkj.iscs.model.UrlConsts
 import com.grkj.iscs.model.vo.dict.CommonDictRespVO
+import com.grkj.iscs.model.vo.hardware.JobCardPageRespVO
+import com.grkj.iscs.model.vo.hardware.RfidTokenPageRespVO
+import com.grkj.iscs.model.vo.key.KeyPageRespVO
+import com.grkj.iscs.model.vo.lock.LockPageRespVO
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.SPUtils
@@ -33,6 +37,7 @@ class ExceptionReportPresenter : BasePresenter<IExceptionReportView>() {
         exceptionDescription: String?,
         exceptionLevel: String,
         exceptionType: String,
+        hardwareId: String,
         callBack: (Boolean) -> Unit
     ) {
         NetApi.insertException(
@@ -41,11 +46,40 @@ class ExceptionReportPresenter : BasePresenter<IExceptionReportView>() {
             exceptionLevel,
             exceptionType,
             SPUtils.getLoginUser(mContext!!)?.userId!!,
-            mContext!!.serialNo()
+            mContext!!.serialNo(),
+            hardwareId
         ) {
             Executor.runOnMain {
                 callBack(it)
             }
         }
     }
+
+    /**
+     * 获取钥匙列表
+     */
+    fun getKeyPage(callBack: (KeyPageRespVO?) -> Unit) {
+        NetApi.getIsKeyPage { callBack.invoke(it) }
+    }
+
+    /**
+     * 获取挂锁列表
+     */
+    fun getLockPage(callBack: (LockPageRespVO?) -> Unit) {
+        NetApi.getIsLockPage { callBack.invoke(it) }
+    }
+
+    /**
+     * 获取工卡列表
+     */
+    fun getJobCardPage(callBack: (JobCardPageRespVO?) -> Unit) {
+        NetApi.getIsJobCardPage { callBack.invoke(it) }
+    }
+
+    /**
+     * 获取工卡列表
+     */
+    fun getRfidTokenPage(callBack: (RfidTokenPageRespVO?) -> Unit) {
+        NetApi.getIsRfidTokenPage { callBack.invoke(it) }
+    }
 }

+ 27 - 3
app/src/main/java/com/grkj/iscs/view/presenter/HomePresenter.kt

@@ -1,9 +1,13 @@
 package com.grkj.iscs.view.presenter
 
 import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.MyApplication
 import com.grkj.iscs.R
+import com.grkj.iscs.extentions.serialNo
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.NetApi
+import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.iview.IHomeView
 
@@ -21,11 +25,21 @@ class HomePresenter : BasePresenter<IHomeView>() {
                             Executor.repeatOnMain({
                                 keyBean.mac?.let {
                                     BusinessManager.sendLoadingEventMsg(mContext?.getString(R.string.loading_msg_return_key_start))
-                                    BusinessManager.registerConnectListener(it) { isDone, bleBean ->
+                                    BusinessManager.registerConnectListener(
+                                        it
+                                    ) { isDone, bleBean ->
                                         if (isDone && bleBean != null) {
                                             Executor.delayOnMain(300) {
-                                                BusinessManager.sendLoadingEventMsg(mContext?.getString(R.string.loading_msg_get_ticket_status_start))
-                                                BusinessManager.getCurrentStatus(4, bleBean.bleDevice)
+                                                BusinessManager.sendLoadingEventMsg(
+                                                    mContext?.getString(
+                                                        R.string.loading_msg_get_ticket_status_start
+                                                    )
+                                                )
+                                                BusinessManager.getCurrentStatus(
+                                                    4,
+                                                    bleBean.bleDevice
+                                                )
+                                                BusinessManager.getBatteryPower(bleBean.bleDevice)
                                             }
                                         }
                                     }
@@ -42,4 +56,14 @@ class HomePresenter : BasePresenter<IHomeView>() {
     fun unregisterListener() {
         BusinessManager.unregisterListener(this)
     }
+
+    /**
+     * 根据设备标识获取并保存锁柜id
+     */
+    fun getAndSaveCabinetId() {
+        NetApi.getIsLockCabinetPage {
+            it?.records?.find { it.serialNumber == MyApplication.instance?.serialNo() }
+                ?.let { SPUtils.saveCabinetId(it.cabinetId ?: "") }
+        }
+    }
 }

+ 28 - 4
app/src/main/java/com/grkj/iscs/view/presenter/LoginPresenter.kt

@@ -3,8 +3,10 @@ package com.grkj.iscs.view.presenter
 import android.content.Context
 import android.graphics.Bitmap
 import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.MyApplication
 import com.grkj.iscs.R
 import com.grkj.iscs.extentions.removeLeadingZeros
+import com.grkj.iscs.extentions.serialNo
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.Constants
@@ -22,7 +24,12 @@ import com.grkj.iscs.view.iview.ILoginView
 
 class LoginPresenter : BasePresenter<ILoginView>() {
 
-    fun login(context: Context, account: String, pwd: String, callBack: (Boolean, UserInfoRespVO?) -> Unit) {
+    fun login(
+        context: Context,
+        account: String,
+        pwd: String,
+        callBack: (Boolean, UserInfoRespVO?) -> Unit
+    ) {
         if (account.isEmpty()) {
             ToastUtils.tip(context.getString(R.string.please_input_account))
             return
@@ -45,14 +52,30 @@ class LoginPresenter : BasePresenter<ILoginView>() {
 
     fun fingerprintLogin(bitmap: Bitmap, callBack: (Boolean, UserInfoRespVO?) -> Unit) {
         BusinessManager.sendLoadingEventMsg(mContext?.getString(R.string.doing_login))
-        NetApi.loginByFingerprint(mutableListOf(FileStreamReqParam("file", BitmapUtil.bitmapToByteArray(bitmap), ".bmp"))) {
+        NetApi.loginByFingerprint(
+            mutableListOf(
+                FileStreamReqParam(
+                    "file",
+                    BitmapUtil.bitmapToByteArray(bitmap),
+                    ".bmp"
+                )
+            )
+        ) {
             commonProcess(it, callBack)
         }
     }
 
     fun faceLogin(bitmap: Bitmap, callBack: (Boolean, UserInfoRespVO?) -> Unit) {
         BusinessManager.sendLoadingEventMsg(mContext?.getString(R.string.doing_login))
-        NetApi.loginByFace(mutableListOf(FileStreamReqParam("file", BitmapUtil.bitmapToByteArray(bitmap), ".bmp"))) {
+        NetApi.loginByFace(
+            mutableListOf(
+                FileStreamReqParam(
+                    "file",
+                    BitmapUtil.bitmapToByteArray(bitmap),
+                    ".bmp"
+                )
+            )
+        ) {
             commonProcess(it, callBack)
         }
     }
@@ -92,7 +115,8 @@ class LoginPresenter : BasePresenter<ILoginView>() {
                     DEVICE_TYPE_CARD -> {
                         ModBusController.readPortalCaseCardRfid(dockBean.addr) { res ->
                             if (res.size >= 11) {
-                                val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
+                                val rfid =
+                                    res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
                                 LogUtil.i("卡片RFID : $rfid")
                                 // TODO 跳转页面处理
                             }

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

@@ -1,7 +1,33 @@
 package com.grkj.iscs.view.presenter
 
+import com.grkj.iscs.MyApplication
+import com.grkj.iscs.extentions.serialNo
+import com.grkj.iscs.model.vo.machinery.MachineryDetailRespVO
+import com.grkj.iscs.model.vo.map.MapInfoRespVO
+import com.grkj.iscs.model.vo.ticket.LotoMapRespVO
+import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.iview.ISwitchStatusView
 
 class SwitchStatusPresenter : BasePresenter<ISwitchStatusView>() {
+    /**
+     * 地图id
+     */
+    fun getMapInfo(mapId: Long, callBack: (MapInfoRespVO?) -> Unit) {
+        NetApi.getMapInfo(mapId) {
+            Executor.runOnMain {
+                callBack(it)
+            }
+        }
+    }
+
+    fun getMapData(done: (String) -> Unit) {
+        NetApi.getIsLotoStationPage {
+            done(
+                it?.records?.find { it.lotoSerialNumber == MyApplication.instance?.applicationContext!!.serialNo() }?.mapId
+                    ?: "4"
+            )
+        }
+    }
 }

+ 12 - 8
app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt

@@ -6,32 +6,32 @@ import android.graphics.Color
 import android.graphics.Matrix
 import android.graphics.Paint
 import android.graphics.PointF
+import android.os.SystemClock
 import android.util.Pair
 import android.view.MotionEvent
+import androidx.core.content.ContextCompat
+import com.grkj.iscs.MyApplication
 import com.grkj.iscs.R
+import com.grkj.iscs.modbus.ModBusController
 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()
 ) : MapBaseLayer(mapView) {
+
     private var listener: MarkIsClickListener? = null
     private var radiusMark = 0f
-    private var isClickMark: Boolean = false
-    private var num: Int = -1
     private lateinit var paint: Paint
-    private var btnIndex: Int = -1
     private var currentZoom = 0f
     private var currentDegree = 0f
     private var bgBitmap: Bitmap? = null
     private var coverBitmap: Bitmap? = null
     private var ratio: Float = 1f
+    private var switchSize: Float = 1f
 
     init {
         initLayer()
@@ -58,6 +58,7 @@ class CustomStationLayer @JvmOverloads constructor(
             (50 * ratio).toInt(),
             (78 * ratio).toInt()
         )!!
+        switchSize = setValue(4 * ratio)
     }
 
     override fun onTouch(event: MotionEvent) {
@@ -75,7 +76,10 @@ class CustomStationLayer @JvmOverloads constructor(
         canvas.save()
         // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
         canvas.concat(currentMatrix)
+        val switchData = ModBusController.getSwitchData()
         pointList.forEach { point ->
+            val switchStatus = switchData
+                .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
             // point.pos.x/y 已经是「图内像素坐标」
             val x = point.pos.x
             val y = point.pos.y
@@ -84,7 +88,6 @@ class CustomStationLayer @JvmOverloads constructor(
                 canvas.drawBitmap(
                     it, x, y, paint
                 )
-                // 再画 icon
                 point.icon?.let { icon ->
                     canvas.drawBitmap(
                         icon,
@@ -154,6 +157,7 @@ class CustomStationLayer @JvmOverloads constructor(
         val entityName: String,
         val icon: Bitmap?,
         val entityId: Long,
-        val isSelected: Boolean
+        val pointSerialNumber: String?,
+        val isSelected: Boolean,
     )
 }

+ 221 - 0
app/src/main/java/com/grkj/iscs/view/widget/CustomSwitchStationLayer.kt

@@ -0,0 +1,221 @@
+package com.grkj.iscs.view.widget
+
+import android.graphics.Bitmap
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.PointF
+import android.os.SystemClock
+import android.util.Pair
+import android.view.MotionEvent
+import androidx.core.content.ContextCompat
+import com.grkj.iscs.MyApplication
+import com.grkj.iscs.R
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.util.BitmapUtil
+import com.onlylemi.mapview.library.MapView
+import com.onlylemi.mapview.library.layer.MapBaseLayer
+import kotlin.math.cos
+import kotlin.math.sin
+
+class CustomSwitchStationLayer @JvmOverloads constructor(
+    mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
+) : MapBaseLayer(mapView) {
+    // 呼吸灯周期(毫秒)
+    private val breathePeriod = 1200f
+    private val FRAME_INTERVAL = 32L   // 约 30fps
+    private var alpha = 255
+    private val refreshRunnable = Runnable {
+        // 2. uptimeMillis 保证单调递增
+        val now = SystemClock.uptimeMillis()
+        // 3. 先在 Long 上做模,再转 Float 计算 phase
+        val phase = ((now % breathePeriod).toFloat()) / breathePeriod
+        val normalized = ((sin(phase * 2 * Math.PI) + 1) / 2).toFloat()
+        alpha = (normalized * (255 - 50) + 50).toInt()
+        mapView?.refresh()
+        mapView?.post {
+            startAnimation()
+        }
+    }
+
+    private var listener: MarkIsClickListener? = null
+    private var radiusMark = 0f
+    private var isClickMark: Boolean = false
+    private var num: Int = -1
+    private lateinit var paint: Paint
+    private var btnIndex: Int = -1
+    private var currentZoom = 0f
+    private var currentDegree = 0f
+    private var bgBitmap: Bitmap? = null
+    private var coverBitmap: Bitmap? = null
+    private var ratio: Float = 1f
+    private var switchSize: Float = 1f
+
+    init {
+        initLayer()
+    }
+
+    private fun initLayer() {
+        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()
+        )!!
+        switchSize = setValue(4 * ratio)
+    }
+
+    fun startAnimation() {
+        // 先干掉前一次没执行的
+        mapView.removeCallbacks(refreshRunnable)
+        // 延后 16ms 再刷新,自动合并一堆连续的调用
+        mapView.postDelayed(refreshRunnable, FRAME_INTERVAL)
+    }
+
+    fun stopAnimation() {
+        // 先干掉前一次没执行的
+        mapView.removeCallbacks(refreshRunnable)
+    }
+
+    override fun onTouch(event: MotionEvent) {
+
+    }
+
+    override fun draw(
+        canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
+    ) {
+        this.currentZoom = currentZoom
+        currentDegree = 360 - currentRotateDegrees
+
+        if (!isVisible) return
+
+        canvas.save()
+        // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
+        canvas.concat(currentMatrix)
+        val switchData = ModBusController.getSwitchData()
+        pointList.forEach { point ->
+            val switchStatus = switchData
+                .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
+            // point.pos.x/y 已经是「图内像素坐标」
+            val x = point.pos.x
+            val y = point.pos.y
+            // 先画背景(它会被 currentMatrix 自动缩放)
+            bgBitmap?.let {
+                canvas.drawBitmap(
+                    it, x, y, paint
+                )
+                paint.alpha = 255
+                if (switchStatus != null) {
+                    // 再画 icon
+                    if (switchStatus) {
+                        paint.color = ContextCompat.getColor(
+                            MyApplication.instance?.applicationContext!!,
+                            R.color.common_switch_enable
+                        )
+                        paint.alpha = alpha
+                        canvas.drawCircle(
+                            x + (it.width - switchSize) / 2 + switchSize / 2,
+                            y + (it.width - switchSize) / 2 + switchSize / 2,
+                            switchSize, paint
+                        )
+                        paint.alpha = 255
+                    } else {
+                        paint.color = ContextCompat.getColor(
+                            MyApplication.instance?.applicationContext!!,
+                            R.color.common_switch_disable
+                        )
+                        canvas.drawCircle(
+                            x + (it.width - switchSize) / 2 + switchSize / 2,
+                            y + (it.width - switchSize) / 2 + switchSize / 2,
+                            switchSize, paint
+                        )
+                    }
+//                    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(
+                        "√",
+                        x + (it.width - checkW) / 2,
+                        y + (it.height / 2 + radiusMark / 2),
+                        paint
+                    )
+                }
+            }
+        }
+
+        canvas.restore()
+    }
+
+    private fun rotatePoint(
+        oriX: Float, oriY: Float, desX: Float, desY: Float, rotateDegrees: Float
+    ): Pair<Float, Float> {
+        // 将度数转换为弧度
+        val theta = Math.toRadians(rotateDegrees.toDouble())
+
+        // 计算旋转后的坐标
+        val newX = (oriX - desX) * cos(theta) - (oriY - desY) * sin(theta) + desX
+        val newY = (oriX - desX) * sin(theta) + ((oriY - desY) * cos(theta)) + desY
+
+        return Pair(newX.toFloat(), newY.toFloat())
+    }
+
+    fun setMarkIsClickListener(listener: MarkIsClickListener?) {
+        this.listener = listener
+    }
+
+    interface MarkIsClickListener {
+        fun markIsClick(index: Int, btnIndex: Int)
+    }
+
+    data class IsolationPoint(
+        val pos: PointF,
+        val entityName: String,
+        val icon: Bitmap?,
+        val entityId: Long,
+        val pointSerialNumber: String?,
+        val isSelected: Boolean,
+    )
+}

+ 37 - 9
app/src/main/java/com/grkj/iscs/view/widget/SelectableInput.kt

@@ -87,7 +87,10 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                 mBinding.et.background = if (mBinding.et.isEnabled) {
                     AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_bg)
                 } else {
-                    AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_disabled_bg)
+                    AppCompatResources.getDrawable(
+                        ctx,
+                        R.drawable.selectable_input_spinner_disabled_bg
+                    )
                 }
             }
             // 输入模式
@@ -95,7 +98,10 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                 mBinding.et.background = if (mBinding.et.isEnabled) {
                     AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_text_bg)
                 } else {
-                    AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_text_disabled_bg)
+                    AppCompatResources.getDrawable(
+                        ctx,
+                        R.drawable.selectable_input_text_disabled_bg
+                    )
                 }
             }
         }
@@ -110,7 +116,10 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                 mBinding.et.background = if (mBinding.et.isEnabled) {
                     AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_bg)
                 } else {
-                    AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_disabled_bg)
+                    AppCompatResources.getDrawable(
+                        ctx,
+                        R.drawable.selectable_input_spinner_disabled_bg
+                    )
                 }
 
                 mBinding.et.inputType = 0
@@ -133,7 +142,10 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                 mBinding.et.background = if (mBinding.et.isEnabled) {
                     AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_text_bg)
                 } else {
-                    AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_text_disabled_bg)
+                    AppCompatResources.getDrawable(
+                        ctx,
+                        R.drawable.selectable_input_text_disabled_bg
+                    )
                 }
 
                 val textWatcher = object : TextWatcher {
@@ -153,7 +165,8 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                         mBinding.et.addTextChangedListener(textWatcher)
                     } else {
                         mBinding.et.removeTextChangedListener(textWatcher)
-                        val imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+                        val imm =
+                            ctx.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
                         imm.hideSoftInputFromWindow(view.windowToken, 0)
                     }
                 }
@@ -165,7 +178,10 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
                 mBinding.et.background = if (mBinding.et.isEnabled) {
                     AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_bg)
                 } else {
-                    AppCompatResources.getDrawable(ctx, R.drawable.selectable_input_spinner_disabled_bg)
+                    AppCompatResources.getDrawable(
+                        ctx,
+                        R.drawable.selectable_input_spinner_disabled_bg
+                    )
                 }
 
                 mBinding.et.inputType = 0
@@ -186,6 +202,12 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
         }
     }
 
+    fun setTitle(v: String?) {
+        isSkipListener = true
+        mBinding.tvName.text = v
+        isSkipListener = false
+    }
+
     fun setText(v: String?) {
         isSkipListener = true
         mBinding.et.setText(v)
@@ -202,11 +224,16 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
     fun setOptionList(list: MutableList<String?>) {
         mOptionList.clear()
         mOptionList.addAll(list)
+        mPopWindow?.contentView?.let {
+            val popBinding = LayoutSelectableinputSpinnerBinding.bind(it)
+            popBinding.rvOptions.adapter?.notifyDataSetChanged()
+        }
     }
 
     private fun showDropdown() {
-        mDropdownView?:let {
-            mDropdownView = LayoutInflater.from(ctx).inflate(R.layout.layout_selectableinput_spinner, null)
+        mDropdownView ?: let {
+            mDropdownView =
+                LayoutInflater.from(ctx).inflate(R.layout.layout_selectableinput_spinner, null)
         }
 
         mPopWindow ?: let {
@@ -218,7 +245,8 @@ class SelectableInput(private val ctx: Context, attrs: AttributeSet) : LinearLay
             mPopWindow?.isFocusable = true
 
             val popBinding = LayoutSelectableinputSpinnerBinding.bind(mDropdownView!!)
-            popBinding.rvOptions.adapter = object : CommonAdapter<String>(ctx, R.layout.item_rv_selectableinput_spinner, mOptionList) {
+            popBinding.rvOptions.adapter = object :
+                CommonAdapter<String>(ctx, R.layout.item_rv_selectableinput_spinner, mOptionList) {
                 override fun convert(holder: ViewHolder?, option: String?, position: Int) {
                     holder?.setText(R.id.tv_option, option ?: "")
                     holder?.setOnClickListener(R.id.tv_option) {

+ 59 - 0
app/src/main/res/layout/dialog_cabinet_serial_no.xml

@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    app:cardCornerRadius="@dimen/common_radius">
+
+    <RelativeLayout
+        android:layout_width="@dimen/dialog_tip_width"
+        android:layout_height="@dimen/dialog_tip_height">
+
+        <TextView
+            style="@style/CommonTextView"
+            android:layout_width="match_parent"
+            android:background="@color/main_color"
+            android:gravity="left"
+            android:paddingVertical="5dp"
+            android:paddingLeft="10dp"
+            android:text="@string/action_confirm" />
+
+        <TextView
+            android:id="@+id/serial_no"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:layout_marginHorizontal="@dimen/common_spacing"
+            android:background="@drawable/selectable_input_text_bg"
+            android:hint="@string/please_set_url"
+            android:padding="@dimen/selectable_input_edit_padding"
+            android:textColor="@color/black"
+            android:textSize="@dimen/common_text_size" />
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="@dimen/common_spacing"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/btn_confirm"
+                style="@style/CommonBtnBlue"
+                android:layout_width="80dp"
+                android:layout_height="25dp"
+                android:backgroundTint="#E600AE00"
+                android:text="@string/confirm" />
+
+            <TextView
+                android:id="@+id/btn_cancel"
+                style="@style/CommonBtnBlue"
+                android:layout_width="80dp"
+                android:layout_height="25dp"
+                android:layout_marginLeft="@dimen/common_spacing"
+                android:backgroundTint="#99FF0000"
+                android:text="@string/cancel" />
+        </LinearLayout>
+    </RelativeLayout>
+</androidx.cardview.widget.CardView>

+ 77 - 0
app/src/main/res/layout/dialog_slot_exception.xml

@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    xmlns:tools="http://schemas.android.com/tools"
+    app:cardCornerRadius="@dimen/common_radius">
+
+    <RelativeLayout
+        android:layout_width="@dimen/dialog_tip_width"
+        android:layout_height="@dimen/dialog_tip_height">
+
+        <TextView
+            android:id="@+id/title"
+            style="@style/CommonTextView"
+            android:layout_width="match_parent"
+            android:background="@color/main_color"
+            android:gravity="left"
+            android:paddingVertical="5dp"
+            android:paddingLeft="10dp"
+            android:text="@string/action_confirm" />
+
+        <TextView
+            android:id="@+id/hardware_info"
+            style="@style/CommonTextView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_below="@+id/title"
+            android:textColor="@color/black"
+            android:layout_marginHorizontal="@dimen/common_spacing"
+            android:layout_marginTop="@dimen/common_spacing"
+            tools:text="@string/hardware_info"/>
+
+        <EditText
+            android:id="@+id/et"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_above="@+id/ll_control"
+            android:layout_below="@+id/hardware_info"
+            android:layout_centerInParent="true"
+            android:layout_marginHorizontal="@dimen/common_spacing"
+            android:layout_marginVertical="@dimen/common_spacing"
+            android:background="@drawable/selectable_input_text_bg"
+            android:gravity="left|top"
+            android:hint="@string/please_input_exception_reason"
+            android:padding="@dimen/selectable_input_edit_padding"
+            android:textColor="@color/black"
+            android:textSize="@dimen/common_text_size" />
+
+        <LinearLayout
+            android:id="@+id/ll_control"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_centerHorizontal="true"
+            android:layout_marginBottom="@dimen/common_spacing"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/btn_confirm"
+                style="@style/CommonBtnBlue"
+                android:layout_width="80dp"
+                android:layout_height="25dp"
+                android:backgroundTint="#E600AE00"
+                android:text="@string/confirm" />
+
+            <TextView
+                android:id="@+id/btn_cancel"
+                style="@style/CommonBtnBlue"
+                android:layout_width="80dp"
+                android:layout_height="25dp"
+                android:layout_marginLeft="@dimen/common_spacing"
+                android:backgroundTint="#99FF0000"
+                android:text="@string/cancel" />
+        </LinearLayout>
+    </RelativeLayout>
+</androidx.cardview.widget.CardView>

+ 14 - 2
app/src/main/res/layout/fragment_exception_report.xml

@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    xmlns:app="http://schemas.android.com/apk/res-auto"
     tools:context=".view.fragment.ExceptionReportFragment">
 
     <com.grkj.iscs.view.widget.CommonBtn
         android:id="@+id/cb_submit"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:layout_alignParentBottom="true"
         android:layout_alignParentRight="true"
+        android:layout_alignParentBottom="true"
         android:layout_marginRight="@dimen/common_spacing"
         app:btn_bg="@drawable/common_btn_red_bg"
         app:btn_name="@string/submit" />
@@ -49,6 +49,18 @@
             app:required="true"
             app:text_size="@dimen/common_text_size" />
 
+        <com.grkj.iscs.view.widget.SelectableInput
+            android:id="@+id/si_device"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_marginVertical="@dimen/common_spacing"
+            android:visibility="gone"
+            app:edit_width="200dp"
+            app:edittext_hint="@string/exception_report_level_hint"
+            app:mode="select"
+            app:required="true"
+            app:text_size="@dimen/common_text_size" />
+
         <com.grkj.iscs.view.widget.SelectableInput
             android:id="@+id/si_description"
             android:layout_width="match_parent"

+ 7 - 0
app/src/main/res/layout/item_rv_dock_test_child.xml

@@ -29,8 +29,15 @@
         <TextView
             android:id="@+id/tv_turn_off"
             style="@style/CommonTextView"
+            android:layout_marginRight="@dimen/common_spacing_small"
             android:background="@color/main_color_dark"
             android:padding="2dp"
             android:text="@string/turn_off" />
+        <TextView
+            android:id="@+id/tv_read"
+            style="@style/CommonTextView"
+            android:background="@color/main_color_dark"
+            android:padding="2dp"
+            android:text="@string/turn_read" />
     </LinearLayout>
 </LinearLayout>

+ 1 - 2
app/src/main/res/layout/item_rv_empty_dock_status.xml

@@ -5,5 +5,4 @@
     android:layout_height="wrap_content"
     android:minHeight="90dp"
     android:layout_marginHorizontal="@dimen/common_spacing_small"
-    android:layout_marginVertical="@dimen/common_spacing_smallest"
-    android:background="@color/white" />
+    android:layout_marginVertical="@dimen/common_spacing_smallest" />

+ 74 - 22
app/src/main/res/layout/item_rv_key_dock_status.xml

@@ -17,8 +17,7 @@
         <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:orientation="vertical">
+            android:layout_weight="1">
 
             <ImageView
                 android:id="@+id/iv_key_1"
@@ -35,7 +34,8 @@
                 android:layout_centerHorizontal="true"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
-                android:gravity="center">
+                android:gravity="center"
+                android:visibility="invisible">
 
                 <TextView
                     android:id="@+id/tv_status_1"
@@ -50,26 +50,38 @@
                 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" />
+                android:text="@string/repair_key"
+                android:visibility="invisible" />
 
             <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" />
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_1"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignLeft="@+id/iv_key_1"
+                android:layout_alignTop="@+id/iv_key_1"
+                android:layout_alignRight="@+id/iv_key_1"
+                android:layout_alignBottom="@+id/iv_key_1"
+                android:visibility="gone" />
         </RelativeLayout>
 
         <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:gravity="center_horizontal">
+            android:layout_weight="1">
 
             <ImageView
                 android:id="@+id/iv_key_2"
                 android:layout_width="50dp"
                 android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
                 android:background="@drawable/dock_key_selector" />
 
             <RelativeLayout
@@ -77,9 +89,11 @@
                 android:layout_width="50dp"
                 android:layout_height="20dp"
                 android:layout_below="@+id/iv_key_2"
+                android:layout_centerHorizontal="true"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
-                android:gravity="center">
+                android:gravity="center"
+                android:visibility="invisible">
 
                 <TextView
                     android:id="@+id/tv_status_2"
@@ -94,14 +108,26 @@
                 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" />
+                android:text="@string/repair_key"
+                android:visibility="invisible" />
 
             <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" />
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_2"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignLeft="@+id/iv_key_2"
+                android:layout_alignTop="@+id/iv_key_2"
+                android:layout_alignRight="@+id/iv_key_2"
+                android:layout_alignBottom="@+id/iv_key_2"
+                android:visibility="gone" />
         </RelativeLayout>
     </LinearLayout>
 
@@ -115,14 +141,13 @@
         <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:gravity="center_horizontal"
-            android:orientation="vertical">
+            android:layout_weight="1">
 
             <ImageView
                 android:id="@+id/iv_key_3"
                 android:layout_width="50dp"
                 android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
                 android:background="@drawable/dock_key_selector" />
 
             <RelativeLayout
@@ -130,9 +155,11 @@
                 android:layout_width="50dp"
                 android:layout_height="20dp"
                 android:layout_below="@+id/iv_key_3"
+                android:layout_centerHorizontal="true"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
-                android:gravity="center">
+                android:gravity="center"
+                android:visibility="invisible">
 
                 <TextView
                     android:id="@+id/tv_status_3"
@@ -147,27 +174,38 @@
                 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" />
+                android:text="@string/repair_key"
+                android:visibility="invisible" />
 
             <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" />
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_3"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignLeft="@+id/iv_key_3"
+                android:layout_alignTop="@+id/iv_key_3"
+                android:layout_alignRight="@+id/iv_key_3"
+                android:layout_alignBottom="@+id/iv_key_3"
+                android:visibility="gone" />
         </RelativeLayout>
 
         <RelativeLayout
             android:layout_width="0dp"
             android:layout_height="match_parent"
-            android:layout_weight="1"
-            android:gravity="center"
-            android:orientation="vertical">
+            android:layout_weight="1">
 
             <ImageView
                 android:id="@+id/iv_key_4"
                 android:layout_width="50dp"
                 android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
                 android:background="@drawable/dock_key_selector" />
 
             <RelativeLayout
@@ -175,9 +213,11 @@
                 android:layout_width="50dp"
                 android:layout_height="20dp"
                 android:layout_below="@+id/iv_key_4"
+                android:layout_centerHorizontal="true"
                 android:layout_marginVertical="@dimen/common_spacing_small"
                 android:background="@drawable/dock_key_status_bg_selector"
-                android:gravity="center">
+                android:gravity="center"
+                android:visibility="invisible">
 
                 <TextView
                     android:id="@+id/tv_status_4"
@@ -192,14 +232,26 @@
                 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" />
+                android:text="@string/repair_key"
+                android:visibility="invisible" />
 
             <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" />
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_4"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignLeft="@+id/iv_key_4"
+                android:layout_alignTop="@+id/iv_key_4"
+                android:layout_alignRight="@+id/iv_key_4"
+                android:layout_alignBottom="@+id/iv_key_4"
+                android:visibility="gone" />
         </RelativeLayout>
     </LinearLayout>
 </LinearLayout>

+ 17 - 2
app/src/main/res/layout/item_rv_lock_dock_child_status.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout 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"
@@ -12,11 +12,16 @@
         android:background="@drawable/dock_lock_selector" />
 
     <LinearLayout
+        android:id="@+id/ll_lock_status"
         android:layout_width="wrap_content"
         android:layout_height="match_parent"
+        android:layout_alignTop="@+id/root"
+        android:layout_alignBottom="@+id/root"
         android:layout_marginLeft="@dimen/divider_line_margin"
+        android:layout_toRightOf="@+id/root"
         android:divider="@drawable/divider_dock_lock_status"
         android:gravity="center"
+        android:visibility="invisible"
         android:orientation="vertical"
         android:showDividers="middle">
 
@@ -32,5 +37,15 @@
             android:layout_height="@dimen/common_status_circle_medium"
             android:background="@drawable/common_status_circle" />
     </LinearLayout>
-</LinearLayout>
+
+    <ImageView
+        android:id="@+id/iv_lock_exception"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_alignLeft="@+id/root"
+        android:layout_alignTop="@+id/root"
+        android:layout_alignRight="@+id/root"
+        android:layout_alignBottom="@+id/root"
+        android:visibility="gone" />
+</RelativeLayout>
 

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

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="common_switch_enable">#91ce93</color>
+    <color name="common_switch_disable">#f0f0f0</color>
+</resources>

+ 20 - 1
app/src/main/res/values-en/strings.xml

@@ -216,6 +216,10 @@
     <string name="exception_report_type_hint">Please select exception type</string>
     <string name="exception_report_level">Severity Level:</string>
     <string name="exception_report_level_hint">Please select severity level</string>
+    <string name="exception_report_hardware_key">Key number:</string>
+    <string name="exception_report_hardware_lock">Lock number:</string>
+    <string name="exception_report_hardware_job_card">Job card number:</string>
+    <string name="exception_report_hardware_rfid_point">Rfid number:</string>
     <string name="exception_report_description">Exception Description:</string>
     <string name="fingerprint_config">Fingerprint Settings</string>
     <string name="face_config">Face Settings</string>
@@ -281,7 +285,7 @@
     <string name="sending_ticket">Issuing permit...</string>
     <string name="send_ticket_fail">Failed to issue permit</string>
     <string name="login_method_tip">● You can login directly via fingerprint or card</string>
-    <string name="use_default_url">Will use default URL</string>
+    <string name="use_default_cabinet_id">Will use default CabinetId</string>
     <string name="please_set_url">Please enter URL, empty will use default</string>
     <string name="url_format_error">Please start with http:// or https://</string>
     <string name="no_key_available_dialog_tip">No available keys, confirm to continue permit?</string>
@@ -335,4 +339,19 @@
     <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>
+    <string name="please_input_exception_reason">please input exception reason</string>
+    <string name="hardware_info">Hardware Info: %1$s</string>
+    <string name="hardware_unknown">unknown</string>
+    <string name="hardware_key">Key</string>
+    <string name="hardware_lock">Lock</string>
+    <string name="number">Number: </string>
+    <string name="report_success">exception report success</string>
+    <string name="report_failed">exception report failed</string>
+    <string name="is_device_recovery_check">Is device recovery check(%1$s,%2$s)?</string>
+    <string name="recovery_success">recovery success</string>
+    <string name="recovery_failed">recovery failed</string>
+    <string name="no_permission">no permission</string>
+    <string name="exception_select_hardware_tip">Please select hardware</string>
+    <string name="check_key_and_lock">check key and lock</string>
+    <string name="turn_read">read</string>
 </resources>

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

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="common_switch_enable">#91ce93</color>
+    <color name="common_switch_disable">#f0f0f0</color>
+</resources>

+ 20 - 1
app/src/main/res/values-zh/strings.xml

@@ -217,6 +217,10 @@
     <string name="exception_report_level">严重等级:</string>
     <string name="exception_report_level_hint">请选择严重等级</string>
     <string name="exception_report_description">异常描述:</string>
+    <string name="exception_report_hardware_key">钥匙编号:</string>
+    <string name="exception_report_hardware_lock">挂锁编号:</string>
+    <string name="exception_report_hardware_job_card">工卡编号:</string>
+    <string name="exception_report_hardware_rfid_point">Rfid标记:</string>
     <string name="fingerprint_config">指纹设置</string>
     <string name="face_config">人脸设置</string>
     <string name="add_fingerprint">添加指纹</string>
@@ -281,7 +285,7 @@
     <string name="sending_ticket">工作票下发中······</string>
     <string name="send_ticket_fail">作业票下发失败</string>
     <string name="login_method_tip">● 您可以通过指纹或刷卡直接进行登录</string>
-    <string name="use_default_url">将使用默认地址</string>
+    <string name="use_default_cabinet_id">将使用默认锁柜id</string>
     <string name="please_set_url">请输入地址,清空保存将使用默认地址</string>
     <string name="url_format_error">请以http://或https://开头</string>
     <string name="no_key_available_dialog_tip">暂无可用钥匙,确认继续执行作业票吗?</string>
@@ -335,4 +339,19 @@
     <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>
+    <string name="please_input_exception_reason">请输入异常原因</string>
+    <string name="hardware_info">硬件信息: %1$s</string>
+    <string name="hardware_unknown">未知</string>
+    <string name="hardware_key">钥匙</string>
+    <string name="hardware_lock">挂锁</string>
+    <string name="number">编号: </string>
+    <string name="report_success">异常上报成功</string>
+    <string name="report_failed">异常上报失败</string>
+    <string name="is_device_recovery_check">是否确认恢复设备(%1$s,%2$s)</string>
+    <string name="recovery_success">恢复成功</string>
+    <string name="recovery_failed">恢复失败</string>
+    <string name="no_permission">权限不足</string>
+    <string name="exception_select_hardware_tip">请选择硬件</string>
+    <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
+    <string name="turn_read">读</string>
 </resources>

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

@@ -41,4 +41,6 @@
     <color name="item_rv_step_bg_done">#1d7153</color>
     <color name="item_rv_step_bg_doing">#838a53</color>
     <color name="item_rv_step_bg_ready">#B3FFFFFF</color>
+    <color name="common_switch_enable">#91ce93</color>
+    <color name="common_switch_disable">#f0f0f0</color>
 </resources>

+ 21 - 2
app/src/main/res/values/strings.xml

@@ -217,6 +217,10 @@
     <string name="exception_report_level">严重等级:</string>
     <string name="exception_report_level_hint">请选择严重等级</string>
     <string name="exception_report_description">异常描述:</string>
+    <string name="exception_report_hardware_key">钥匙编号:</string>
+    <string name="exception_report_hardware_lock">挂锁编号:</string>
+    <string name="exception_report_hardware_job_card">工卡编号:</string>
+    <string name="exception_report_hardware_rfid_point">Rfid标记:</string>
     <string name="fingerprint_config">指纹设置</string>
     <string name="face_config">人脸设置</string>
     <string name="add_fingerprint">添加指纹</string>
@@ -281,7 +285,7 @@
     <string name="sending_ticket">工作票下发中······</string>
     <string name="send_ticket_fail">作业票下发失败</string>
     <string name="login_method_tip">● 您可以通过指纹或刷卡直接进行登录</string>
-    <string name="use_default_url">将使用默认地址</string>
+    <string name="use_default_cabinet_id">将使用锁柜id</string>
     <string name="please_set_url">请输入地址,清空保存将使用默认地址</string>
     <string name="url_format_error">请以http://或https://开头</string>
     <string name="no_key_available_dialog_tip">暂无可用钥匙,确认继续执行作业票吗?</string>
@@ -306,7 +310,7 @@
     <string name="repair_key">修复</string>
     <string name="no_key_to_repair">钥匙不存在,无法修复</string>
     <string name="key_take_error_tip">钥匙分配失败,请检查硬件状态</string>
-    
+
     <string name="index_number">编号</string>
     <string name="icon">图标</string>
     <string name="ticket_name">作业票名称</string>
@@ -335,4 +339,19 @@
     <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>
+    <string name="please_input_exception_reason">请输入异常原因</string>
+    <string name="hardware_info">硬件信息: %1$s</string>
+    <string name="hardware_unknown">未知</string>
+    <string name="hardware_key">钥匙锁仓</string>
+    <string name="hardware_lock">挂锁锁仓</string>
+    <string name="number">编号: </string>
+    <string name="report_success">异常上报成功</string>
+    <string name="report_failed">异常上报失败</string>
+    <string name="is_device_recovery_check">是否确认恢复设备(%1$s,%2$s)</string>
+    <string name="recovery_success">恢复成功</string>
+    <string name="recovery_failed">恢复失败</string>
+    <string name="no_permission">权限不足</string>
+    <string name="exception_select_hardware_tip">请选择硬件</string>
+    <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
+    <string name="turn_read">读</string>
 </resources>

Some files were not shown because too many files changed in this diff