Explorar el Código

refactor(开关和设备异常)
- 增加异常仓位和异常设备派发的过滤
- 增加开关量的数据获取和上报
- 增加开关状态的界面
- 增加仓位的长按上报

周文健 hace 5 meses
padre
commit
74702cfbdd
Se han modificado 43 ficheros con 1385 adiciones y 239 borrados
  1. 77 36
      app/src/main/java/com/grkj/iscs/BusinessManager.kt
  2. 1 0
      app/src/main/java/com/grkj/iscs/extentions/Context.kt
  3. 53 25
      app/src/main/java/com/grkj/iscs/modbus/DockBean.kt
  4. 107 80
      app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt
  5. 0 14
      app/src/main/java/com/grkj/iscs/modbus/ModBusManager.kt
  6. 46 0
      app/src/main/java/com/grkj/iscs/model/DictConstants.kt
  7. 1 1
      app/src/main/java/com/grkj/iscs/model/ISCSDatabase.kt
  8. 27 2
      app/src/main/java/com/grkj/iscs/model/UrlConsts.kt
  9. 4 1
      app/src/main/java/com/grkj/iscs/model/eventmsg/MsgEventConstants.kt
  10. 33 0
      app/src/main/java/com/grkj/iscs/model/vo/cabinet/LockCabinetPageRespVO.kt
  11. 20 20
      app/src/main/java/com/grkj/iscs/model/vo/hardware/CabinetSlotsRespVo.kt
  12. 9 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/SwitchListReqVO.kt
  13. 10 0
      app/src/main/java/com/grkj/iscs/model/vo/hardware/UpdateHardwareEsStatusReqVO.kt
  14. 15 0
      app/src/main/java/com/grkj/iscs/model/vo/key/KeyPageRespVO.kt
  15. 14 0
      app/src/main/java/com/grkj/iscs/model/vo/lock/LockPageRespVO.kt
  16. 3 1
      app/src/main/java/com/grkj/iscs/model/vo/map/MapInfoRespVO.kt
  17. 12 0
      app/src/main/java/com/grkj/iscs/model/vo/system/SystemAttributeByKeyRespVO.kt
  18. 189 8
      app/src/main/java/com/grkj/iscs/util/NetApi.kt
  19. 16 0
      app/src/main/java/com/grkj/iscs/util/SPUtils.kt
  20. 4 1
      app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt
  21. 5 1
      app/src/main/java/com/grkj/iscs/view/activity/LoginActivity.kt
  22. 27 0
      app/src/main/java/com/grkj/iscs/view/dialog/CabinetSerialNoDialog.kt
  23. 49 0
      app/src/main/java/com/grkj/iscs/view/dialog/SlotExceptionDialog.kt
  24. 102 11
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt
  25. 22 7
      app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt
  26. 2 1
      app/src/main/java/com/grkj/iscs/view/fragment/StepFragment.kt
  27. 125 1
      app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt
  28. 66 5
      app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt
  29. 14 0
      app/src/main/java/com/grkj/iscs/view/presenter/HomePresenter.kt
  30. 28 4
      app/src/main/java/com/grkj/iscs/view/presenter/LoginPresenter.kt
  31. 15 0
      app/src/main/java/com/grkj/iscs/view/presenter/SwitchStatusPresenter.kt
  32. 69 13
      app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt
  33. 59 0
      app/src/main/res/layout/dialog_cabinet_serial_no.xml
  34. 77 0
      app/src/main/res/layout/dialog_slot_exception.xml
  35. 1 2
      app/src/main/res/layout/item_rv_empty_dock_status.xml
  36. 28 0
      app/src/main/res/layout/item_rv_key_dock_status.xml
  37. 16 2
      app/src/main/res/layout/item_rv_lock_dock_child_status.xml
  38. 5 0
      app/src/main/res/values-en/colors.xml
  39. 9 1
      app/src/main/res/values-en/strings.xml
  40. 5 0
      app/src/main/res/values-zh/colors.xml
  41. 9 1
      app/src/main/res/values-zh/strings.xml
  42. 2 0
      app/src/main/res/values/colors.xml
  43. 9 1
      app/src/main/res/values/strings.xml

+ 77 - 36
app/src/main/java/com/grkj/iscs/BusinessManager.kt

@@ -36,6 +36,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.DictConstants
 import com.grkj.iscs.model.bo.DeviceTakeUpdateBO
 import com.grkj.iscs.model.bo.UpdateKeyReturnBO
 import com.grkj.iscs.model.bo.WorkTicketGetBO
@@ -50,10 +51,12 @@ import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_CURRENT_MODE
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_DEVICE_EXCEPTION
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_DEVICE_TAKE_UPDATE
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_LOADING
+import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_SWITCH_COLLECTION_UPDATE
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_SWITCH_MODE
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_UPDATE_TICKET_PROGRESS
 import com.grkj.iscs.model.eventmsg.SwitchModeMsg
 import com.grkj.iscs.model.eventmsg.UpdateTicketProgressMsg
+import com.grkj.iscs.model.vo.hardware.SwitchListReqVO
 import com.grkj.iscs.model.vo.lock.LockTakeUpdateReqVO
 import com.grkj.iscs.model.vo.ticket.LockPointUpdateReqVO
 import com.grkj.iscs.model.vo.ticket.TicketDetailRespVO
@@ -68,11 +71,10 @@ import com.grkj.iscs.view.activity.LoginActivity
 import com.grkj.iscs.view.base.BaseActivity
 import com.grkj.iscs.view.dialog.TipDialog
 import com.sik.sikcore.activity.ActivityTracker
-import com.sik.sikcore.extension.getMMKVData
-import com.sik.sikcore.extension.saveMMKVData
-import com.sik.sikcore.extension.toJson
-import com.tencent.mmkv.MMKV
+import com.sik.sikcore.date.TimeUtils
+import com.sik.sikcore.thread.ThreadUtils
 import pub.devrel.easypermissions.AfterPermissionGranted
+import java.time.LocalDateTime
 
 /**
  * 业务层管理
@@ -214,6 +216,21 @@ object BusinessManager {
                         }
                     }
                 }
+
+                MSG_EVENT_SWITCH_COLLECTION_UPDATE -> {
+                    ThreadUtils.runOnIO {
+                        val switchStatus = NetApi.getDictData(DictConstants.KEY_SWITCH_STATUS)
+                        val switchListReqVOS = ModBusController.getSwitchData().map {
+                            SwitchListReqVO(
+                                it.idx.toString(),
+                                if (it.enabled) switchStatus?.find { it.dictLabel == "打开" }?.dictValue else switchStatus?.find { it.dictLabel == "关闭" }?.dictValue
+                            )
+                        }
+                        NetApi.updateSwitchList(switchListReqVOS) {
+                            LogUtil.i("开关更新完成")
+                        }
+                    }
+                }
             }
         }
     }
@@ -229,9 +246,6 @@ object BusinessManager {
 
         if (isNeedInit) {
             ModBusController.initDevicesStatus()
-            ModBusController.updateAllBuckleStatus {
-                //todo 上传开关信息
-            }
         }
     }
 
@@ -516,6 +530,13 @@ object BusinessManager {
         ModBusController.updateAllBuckleStatus(done)
     }
 
+    /**
+     * 更新开关状态
+     */
+    fun updateSwitchStatus(done: () -> Unit) {
+        ModBusController.updateSwitchStatus(done)
+    }
+
     /**
      * 钥匙归还提示确认弹框,当前策略:作业票未完成禁止归还钥匙
      */
@@ -607,39 +628,59 @@ object BusinessManager {
         callBack: (Pair<Byte, DockBean.KeyBean?>?, MutableMap<Byte, MutableList<DockBean.LockBean>>) -> Unit
     ) {
         var lockCount = 0
-        val lockMap = ModBusController.getLocks(needLockCount)
-        lockMap.forEach { (_, rfidList) ->
-            lockCount += rfidList.size
-        }
+        NetApi.getIsLockCabinetSlotsPage { slots ->
+            NetApi.getIsLockPage { locks ->
+                val lockMap = ModBusController.getLocks(
+                    needLockCount,
+                    slots?.records?.filter { it.slotType == "1" && it.status == "1" }
+                        ?.toMutableList()
+                        ?: mutableListOf(),
+                    locks?.records?.filter { it.exStatus == "1" }?.map { it.lockNfc }
+                        ?.toMutableList()
+                        ?: mutableListOf()
+                )
+                lockMap.forEach { (_, rfidList) ->
+                    lockCount += rfidList.size
+                }
 
-        var tipStr = ""
-        if (lockCount < needLockCount) {
-            val msg =
-                MyApplication.instance!!.applicationContext.resources.getString(R.string.lock_is_not_enough)
-            LogUtil.w(msg)
-            lockMap.clear()
-        }
+                var tipStr = ""
+                if (lockCount < needLockCount) {
+                    val msg =
+                        MyApplication.instance!!.applicationContext.resources.getString(R.string.lock_is_not_enough)
+                    LogUtil.w(msg)
+                    lockMap.clear()
+                }
 
-        var key: Pair<Byte, DockBean.KeyBean?>? = null
-        if (isNeedKey && lockCount >= needLockCount) {
-            key = ModBusController.getOneKey()
-            if (key == null) {
-                val msg =
-                    MyApplication.instance!!.applicationContext.resources.getString(R.string.no_available_key)
-                LogUtil.w(msg)
-                tipStr = if (tipStr.isEmpty()) {
-                    msg
-                } else {
-                    tipStr + "\n" + msg
+                var key: Pair<Byte, DockBean.KeyBean?>? = null
+                if (isNeedKey && lockCount >= needLockCount) {
+                    NetApi.getIsKeyPage { keyList ->
+                        key = ModBusController.getOneKey(
+                            slots?.records?.filter { it.slotType == "0" && it.status == "1" }
+                                ?.toMutableList()
+                                ?: mutableListOf(),
+                            keyList?.records?.filter { it.exStatus == "1" }?.map { it.keyNfc }
+                                ?.toMutableList() ?: mutableListOf()
+                        )
+                        if (key == null) {
+                            val msg =
+                                MyApplication.instance!!.applicationContext.resources.getString(R.string.no_available_key)
+                            LogUtil.w(msg)
+                            tipStr = if (tipStr.isEmpty()) {
+                                msg
+                            } else {
+                                tipStr + "\n" + msg
+                            }
+                        }
+                    }
                 }
-            }
-        }
 
-        if (tipStr.isNotEmpty()) {
-            ToastUtils.tip(tipStr)
+                if (tipStr.isNotEmpty()) {
+                    ToastUtils.tip(tipStr)
+                }
+                LogUtil.i("checkEquipCount : key = $key, lockMap = $lockMap")
+                callBack.invoke(key, lockMap)
+            }
         }
-        LogUtil.i("checkEquipCount : key = $key, lockMap = $lockMap")
-        callBack.invoke(key, lockMap)
     }
 
     /**
@@ -1790,7 +1831,7 @@ object BusinessManager {
                                     ToastUtils.tip(R.string.lock_take_report_fail)
                                     SPUtils.saveTicketTakeLockException(info.ticketId)
                                     mDeviceTakeList.removeIf { it.deviceType == DEVICE_TYPE_LOCK && it.nfc == info.nfc }
-                                    mDeviceTakeList.removeIf { it.deviceType == DEVICE_TYPE_KEY &&  it.ticketId == info.ticketId }
+                                    mDeviceTakeList.removeIf { it.deviceType == DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
                                     sendLoadingEventMsg(null, false)
                                     return@runOnMain
                                 }

+ 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)

+ 107 - 80
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,6 @@ 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
 
 
 /**
@@ -77,10 +76,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 +91,9 @@ object ModBusController {
                         l.listener(res)
                     }
                 }
-            }, REPEAT_FREQUENCY)
-            ?.also {
+            }, REPEAT_FREQUENCY)?.also {
                 modBusManager = it
-            }
-            ?.start()
+            }?.start()
     }
 
     /**
@@ -229,8 +225,7 @@ object ModBusController {
                                         if (isDone && bleBean?.bleDevice != null) {
                                             Executor.delayOnMain(500) {
                                                 BusinessManager.getCurrentStatus(
-                                                    3,
-                                                    bleBean.bleDevice
+                                                    3, bleBean.bleDevice
                                                 )
                                             }
                                         }
@@ -286,6 +281,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 +321,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 +329,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 +376,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 +385,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 +401,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 +445,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 +516,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 +551,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 ->
@@ -776,8 +801,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,10 +897,20 @@ object ModBusController {
      *
      * @return 底座地址,钥匙
      */
-    fun getOneKey(): Pair<Byte, DockBean.KeyBean?>? {
+    fun getOneKey(
+        exceptionSlots: MutableList<CabinetSlotsRecord>,
+        exceptionKeys: MutableList<String>
+    ): 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 }
+        keyDockList.sortedBy { it.addr }
+        keyDockList.forEach { it.deviceList.sortedBy { it.idx } }
+        val keyList =
+            keyDockList.map { it.deviceList }.flatten()
+                .filterIsInstance<DockBean.KeyBean>()
+                .filterIndexed { index, _ -> (index + 1) !in exceptionSlots.map { it.col?.toInt() } }
+                .filter { it.rfid !in exceptionKeys }
+                .toMutableList()
         LogUtil.i("keyList : $keyList")
         if (keyList.isEmpty()) {
             ToastUtils.tip(R.string.no_available_key)
@@ -884,22 +919,22 @@ object ModBusController {
 
         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)}"
+                "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()
+            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
@@ -920,22 +955,31 @@ 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) {
+        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 + 1) }
+                            .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
             }
         }
@@ -946,8 +990,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 +1004,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)
         }

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

@@ -0,0 +1,46 @@
+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"
+}

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

@@ -8,7 +8,7 @@ import com.grkj.iscs.MyApplication
 /**
  * 本地数据库
  */
-@Database(version = ISCSMigrations.VERSION)
+//@Database(version = ISCSMigrations.VERSION)
 abstract class ISCSDatabase : RoomDatabase() {
     companion object {
         /**

+ 27 - 2
app/src/main/java/com/grkj/iscs/model/UrlConsts.kt

@@ -29,7 +29,7 @@ object UrlConsts {
     /**
      * 字典前缀
      */
-    private const val DICT_PREFIX = "/system/dict/data/type"
+    const val DICT_PREFIX = "/system/dict/data/type"
 
     /**
      * 查询字典 - 工作票类型
@@ -290,5 +290,30 @@ 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"
 }

+ 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?
 )

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

@@ -0,0 +1,9 @@
+package com.grkj.iscs.model.vo.hardware
+
+/**
+ * 开关状态请求实体
+ */
+data class SwitchListReqVO(
+    val pointSerialNumber: String,
+    val switchStatus: 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
 )

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

@@ -0,0 +1,15 @@
+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 keyNfc: String,
+    val macAddress: String,
+    val exStatus: String,
+    val exRemark: String
+)

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

@@ -0,0 +1,14 @@
+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 lockNfc: String,
+    val exStatus: String,
+    val exRemark: 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?,
+)

+ 189 - 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
@@ -14,8 +16,12 @@ import com.grkj.iscs.model.vo.hardware.CabinetSlotsRespVo
 import com.grkj.iscs.model.vo.hardware.JobCardExDTO
 import com.grkj.iscs.model.vo.hardware.KeyExDTO
 import com.grkj.iscs.model.vo.hardware.LockExDTO
+import com.grkj.iscs.model.vo.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.MachineryDetailRespVO
 import com.grkj.iscs.model.vo.machinery.MachineryPageRespVO
@@ -23,6 +29,7 @@ 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 +44,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
 
 /**
  * 网络请求
@@ -1122,9 +1132,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 +1144,8 @@ object NetApi {
             mapOf(
                 "jobCardExDTOList" to jobCardExDTOList,
                 "keyExDTOList" to keyExDTOList,
-                "lockExDTOList" to lockExDTOList
+                "lockExDTOList" to lockExDTOList,
+                "slotsExDTOList" to slotsExDTOList
             ),
             { res, _, _ ->
                 res?.let {
@@ -1172,16 +1184,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 +1201,174 @@ 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 = it.toBean(SystemAttributeByKeyRespVO::class.java)
+                    BitmapUtil.loadBitmapFromUrl(
+                        MyApplication.instance?.applicationContext!!,
+                        bean.sysAttrValue,
+                        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
+        )
+    }
 }

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

@@ -42,6 +42,8 @@ object SPUtils {
 
     private const val KEY_TICKET_TAKE_LOCK_EXCEPTION = "ticket_take_lock_exception"
 
+    private const val KEY_CABINET_ID = "key_cabinet_id"
+
     fun getLoginUser(context: Context): LoginUserBO? {
         val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
         if (sp.getLong(KEY_LOGIN_USER_USER_ID, -1) == -1L) {
@@ -271,4 +273,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)
+    }
 }

+ 4 - 1
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
@@ -52,7 +53,7 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
 
     override fun initView() {
         presenter?.registerStatusListener()
-
+        presenter?.getAndSaveCabinetId()
         val userInfo = intent.getSerializableExtra("userInfo")
 
         BusinessManager.isTestMode = false
@@ -123,6 +124,8 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
         }
 
         BusinessManager.mEventBus.observe(this, observer)
+        ModBusController.updateAllBuckleStatus{}
+        ModBusController.updateSwitchStatus{}
     }
 
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {

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

@@ -16,6 +16,7 @@ import com.grkj.iscs.util.AppUtils
 import com.grkj.iscs.util.FingerprintUtil
 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.iview.ILoginView
 import com.grkj.iscs.view.presenter.LoginPresenter
@@ -35,7 +36,10 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
     override fun initView() {
         MMKV.initialize(SIKCore.getApplication())
         mBinding?.tvVersion?.text = "v${AppUtils.getPkgVerName(this)}"
-
+        mBinding?.tvVersion?.setOnLongClickListener {
+            CabinetSerialNoDialog(this).show()
+            true
+        }
         mBinding?.main?.setBackgroundResource(R.mipmap.login_bg)
 
         val pairList = mutableListOf(

+ 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)} ${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)
+        }
+    }
+}

+ 102 - 11
app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt

@@ -3,6 +3,7 @@ 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
@@ -13,9 +14,9 @@ import com.grkj.iscs.extentions.setVisibleWithHolder
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
-import com.grkj.iscs.util.Executor
-import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseMvpFragment
+import com.grkj.iscs.view.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,14 +25,13 @@ import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.MultiItemTypeAdapter
 import com.zhy.adapter.recyclerview.base.ItemViewDelegate
 import com.zhy.adapter.recyclerview.base.ViewHolder
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.withContext
 
 /**
  * 硬件状态页
  */
 class DeviceStatusFragment :
     BaseMvpFragment<IDeviceStatusView, DeviceStatusPresenter, FragmentDeviceStatusBinding>() {
+    private val tipDialog: TipDialog by lazy { TipDialog(requireContext()) }
     private var mRowList = mutableListOf<DockStatusBO>()
 
     override val viewBinding: FragmentDeviceStatusBinding
@@ -39,7 +39,19 @@ class DeviceStatusFragment :
 
     override fun initView() {
         presenter?.initData(mRowList)
-
+        presenter?.getExceptionIcon {
+            mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+        }
+        presenter?.mExceptionHintTip = {
+            tipDialog.setType(TipDialog.TYPE_HINT)
+            tipDialog.setTip(it)
+            tipDialog.showCancelCountdown(10)
+        }
+        presenter?.mExceptionReporter = { row, col, slotType ->
+            SlotExceptionDialog(requireContext(), row, col, slotType) {
+                presenter?.reportException(row, col, slotType, it)
+            }.show()
+        }
         val adapter = MultiItemTypeAdapter(requireContext(), mRowList)
         adapter.addItemViewDelegate(KeyDockItemDelegate(requireContext(), presenter))
         adapter.addItemViewDelegate(
@@ -57,7 +69,13 @@ class DeviceStatusFragment :
         super.onResume()
         mBinding?.rvDock?.adapter?.notifyDataSetChanged()
         fun refreshBuckleStatus() {
+            if (!isResumed) {
+                return
+            }
             ThreadUtils.runOnIODelayed(1000) {
+                presenter?.getIsLockCabinetSlotsPage {
+                    mBinding?.rvDock?.adapter?.notifyDataSetChanged()
+                }
                 BusinessManager.updateAllBuckleStatus {
                     ThreadUtils.runOnMain {
                         mBinding?.rvDock?.adapter?.notifyDataSetChanged()
@@ -172,20 +190,75 @@ class DeviceStatusFragment :
             holder?.setOnClickListener(R.id.tv_repair_4) {
                 presenter?.repairKey(row.dockList.find { it.column == "2" }?.address, false)
             }
+            presenter?.mCabinetSlotsData?.filter { it.row == "1" }?.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 ?: "")
+                            }
+                        }
+                    }
+
+                    cabinetSlotsRecord.col == "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 ?: "")
+                            }
+                        }
+                    }
+
+                    cabinetSlotsRecord.col == "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 ?: "")
+                            }
+                        }
+                    }
+
+                    cabinetSlotsRecord.col == "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_1) {
-                ToastUtils.tip("钥匙1上报异常")
+                presenter?.mExceptionReporter?.invoke(0, 1, 0)
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_2) {
-                ToastUtils.tip("钥匙2上报异常")
+                presenter?.mExceptionReporter?.invoke(0, 2, 0)
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_3) {
-                ToastUtils.tip("钥匙3上报异常")
+                presenter?.mExceptionReporter?.invoke(0, 3, 0)
                 true
             }
             holder?.setOnLongClickListener(R.id.iv_key_4) {
-                ToastUtils.tip("钥匙4上报异常")
+                presenter?.mExceptionReporter?.invoke(0, 4, 0)
                 true
             }
         }
@@ -225,7 +298,11 @@ class DeviceStatusFragment :
                         ModBusController.isLockExist(row.dockList[0].address, lockIdx)
                     )
                     holder?.convertView?.setOnLongClickListener {
-                        ToastUtils.tip("锁${row.dockList[0].row},${row.dockList[0].column},${lockIdx}上报异常")
+                        presenter?.mExceptionReporter?.invoke(
+                            row.dockList[0].row.toInt(),
+                            row.dockList[0].column.toInt(),
+                            1
+                        )
                         true
                     }
                     ColorStateList.valueOf(statusNotLightTintColor).let {
@@ -244,6 +321,20 @@ 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 ?: "")
+                                }
+                            }
+                        }
                 }
             }
         }
@@ -262,7 +353,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
         }
     }
 }

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

@@ -28,7 +28,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 +51,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) {
@@ -68,12 +77,18 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
         }
 
         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) {

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

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

+ 125 - 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.R
 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.onlylemi.mapview.library.MapViewListener
+import com.sik.sikcore.thread.ThreadUtils
 
 class SwitchStatusFragment :
     BaseMvpFragment<ISwitchStatusView, SwitchStatusPresenter, FragmentSwitchStatusBinding>() {
+    private var mapRatio: Float = 1f
+    private var stationLayer: CustomStationLayer? = null
+    private val mStationList = mutableListOf<CustomStationLayer.IsolationPoint>()
     override fun initPresenter(): SwitchStatusPresenter {
         return SwitchStatusPresenter()
     }
@@ -15,6 +27,118 @@ class SwitchStatusFragment :
         get() = FragmentSwitchStatusBinding.inflate(layoutInflater)
 
     override fun initView() {
-        
+        initMap()
+        presenter?.getMapInfo(5) { 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?.forEach { pt ->
+                    // 1) 格数 → 全局像素
+                    val globalX = pt.x!!.toFloat() * cellPx
+                    val globalY = pt.y!!.toFloat() * cellPx
+                    // 2) 全局像素 - 子图偏移 = 子图内像素
+                    val localX = globalX - offsetX
+                    val localY = globalY - offsetY
+                    // 3) 再乘缩放比,得到真实 Bitmap 上的像素坐标
+                    val finalX = localX * ratioX
+                    val finalY = localY * ratioY
+                    // 异步加载点位图标,固定请求尺寸
+                    BitmapUtil.loadBitmapFromUrl(
+                        requireContext(),
+                        pt.pointIcon!!,
+                        reqWidth = iconReqPx,
+                        reqHeight = iconReqPx
+                    ) { bmpIcon ->
+                        val icon = bmpIcon ?: BitmapUtil.getResizedBitmapFromMipmap(
+                            requireContext(),
+                            R.mipmap.ticket_type_placeholder,
+                            iconReqPx,
+                            iconReqPx
+                        )
+
+                        mStationList.add(
+                            CustomStationLayer.IsolationPoint(
+                                PointF(finalX, finalY),
+                                pt.entityName!!,
+                                icon,
+                                pt.entityId!!.toLong(),
+                                pt.pointSerialNumber,
+                                false
+                            )
+                        )
+
+                        // 全部点都加载完后,设置给 layer 并绘制
+                        if (mStationList.size == itMapInfo.pointList.size) {
+                            mBinding?.mapview?.loadMap(mapBmp)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        fun refreshSwitchStatus() {
+            if (!isResumed) {
+                return
+            }
+            ThreadUtils.runOnIODelayed(1000) {
+                BusinessManager.updateSwitchStatus {
+                    refreshSwitchStatus()
+                }
+            }
+        }
+        refreshSwitchStatus()
+    }
+
+    /**
+     * 初始化地图
+     */
+    private fun initMap() {
+        mBinding?.mapview?.isScaleAndRotateTogether = false
+        mBinding?.mapview?.setMapViewListener(object : MapViewListener {
+            override fun onMapLoadSuccess() {
+                if (stationLayer != null) {
+                    mBinding?.mapview?.currentRotateDegrees = 0f
+                    return
+                }
+                stationLayer = CustomStationLayer(mBinding?.mapview, mStationList)
+                mBinding?.mapview?.addLayer(stationLayer)
+                stationLayer?.setRatio(mapRatio)
+                stationLayer?.startAnimation()
+            }
+
+            override fun onMapLoadFail() {
+                ToastUtils.tip("onMapLoadFail")
+            }
+        })
     }
 }

+ 66 - 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
@@ -8,8 +9,11 @@ import com.grkj.iscs.BusinessManager.getCurrentStatus
 import com.grkj.iscs.R
 import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.toHexStrings
+import com.grkj.iscs.modbus.DockBean
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
+import com.grkj.iscs.model.vo.hardware.CabinetSlotsRecord
+import com.grkj.iscs.model.vo.hardware.SlotExDTO
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.SPUtils
@@ -19,17 +23,23 @@ 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 kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
 
 class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
+    var mCabinetSlotsData: List<CabinetSlotsRecord> = listOf()
+    var mExceptionIcon: Bitmap? = null
+    var mExceptionHintTip: ((String) -> Unit)? = null
+    var mExceptionReporter: ((row: Int, col: Int, slotType: Int) -> Unit)? =
+        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 +157,55 @@ 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, slotType: Int, exceptionReason: String) {
+        NetApi.updateHardwareEsStatus(
+            slotsExDTOList = mutableListOf(
+                SlotExDTO(
+                    col.toString(),
+                    exceptionReason,
+                    row.toString(),
+                    slotType.toString()
+                )
+            )
+        ) {
+            if (it) {
+                ToastUtils.tip(R.string.report_success)
+            } else {
+                ToastUtils.tip(R.string.report_failed)
+            }
+        }
+    }
 }

+ 14 - 0
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
 
@@ -42,4 +46,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 跳转页面处理
                             }

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

@@ -1,7 +1,22 @@
 package com.grkj.iscs.view.presenter
 
+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)
+            }
+        }
+    }
 }

+ 69 - 13
app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt

@@ -6,21 +6,39 @@ 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 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
@@ -32,6 +50,7 @@ class CustomStationLayer @JvmOverloads constructor(
     private var bgBitmap: Bitmap? = null
     private var coverBitmap: Bitmap? = null
     private var ratio: Float = 1f
+    private var switchSize: Float = 1f
 
     init {
         initLayer()
@@ -58,6 +77,14 @@ class CustomStationLayer @JvmOverloads constructor(
             (50 * ratio).toInt(),
             (78 * ratio).toInt()
         )!!
+        switchSize = setValue(4 * ratio)
+    }
+
+    fun startAnimation() {
+        // 先干掉前一次没执行的
+        mapView.removeCallbacks(refreshRunnable)
+        // 延后 16ms 再刷新,自动合并一堆连续的调用
+        mapView.postDelayed(refreshRunnable, FRAME_INTERVAL)
     }
 
     override fun onTouch(event: MotionEvent) {
@@ -75,7 +102,9 @@ class CustomStationLayer @JvmOverloads constructor(
         canvas.save()
         // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
         canvas.concat(currentMatrix)
-        pointList.forEach { point ->
+        pointList.forEachIndexed { index, point ->
+            val switchStatus = ModBusController.getSwitchData()
+                .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
             // point.pos.x/y 已经是「图内像素坐标」
             val x = point.pos.x
             val y = point.pos.y
@@ -84,14 +113,40 @@ class CustomStationLayer @JvmOverloads constructor(
                 canvas.drawBitmap(
                     it, x, y, paint
                 )
-                // 再画 icon
-                point.icon?.let { icon ->
-                    canvas.drawBitmap(
-                        icon,
-                        x + (it.width - icon.width) / 2,
-                        y + (it.width - icon.width) / 2,
-                        paint
-                    )
+                paint.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
@@ -154,6 +209,7 @@ class CustomStationLayer @JvmOverloads constructor(
         val entityName: String,
         val icon: Bitmap?,
         val entityId: Long,
-        val isSelected: Boolean
+        val pointSerialNumber: String?,
+        val isSelected: Boolean,
     )
 }

+ 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>

+ 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" />

+ 28 - 0
app/src/main/res/layout/item_rv_key_dock_status.xml

@@ -58,6 +58,13 @@
                 android:layout_height="@dimen/common_status_circle_small"
                 android:layout_toRightOf="@+id/iv_key_1"
                 android:background="@drawable/common_status_circle" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_1"
+                android:layout_width="match_parent"
+                android:layout_height="80dp"
+                android:layout_centerInParent="true"
+                android:visibility="gone" />
         </RelativeLayout>
 
         <RelativeLayout
@@ -103,6 +110,13 @@
                 android:layout_toRightOf="@+id/iv_key_2"
                 android:background="@drawable/common_status_circle" />
         </RelativeLayout>
+
+        <ImageView
+            android:id="@+id/iv_key_exception_2"
+            android:layout_width="match_parent"
+            android:layout_height="80dp"
+            android:layout_centerInParent="true"
+            android:visibility="gone" />
     </LinearLayout>
 
     <LinearLayout
@@ -155,6 +169,13 @@
                 android:layout_height="@dimen/common_status_circle_small"
                 android:layout_toRightOf="@+id/iv_key_3"
                 android:background="@drawable/common_status_circle" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_3"
+                android:layout_width="match_parent"
+                android:layout_height="80dp"
+                android:layout_centerInParent="true"
+                android:visibility="gone" />
         </RelativeLayout>
 
         <RelativeLayout
@@ -200,6 +221,13 @@
                 android:layout_height="@dimen/common_status_circle_small"
                 android:layout_toRightOf="@+id/iv_key_4"
                 android:background="@drawable/common_status_circle" />
+
+            <ImageView
+                android:id="@+id/iv_key_exception_4"
+                android:layout_width="match_parent"
+                android:layout_height="80dp"
+                android:layout_centerInParent="true"
+                android:visibility="gone" />
         </RelativeLayout>
     </LinearLayout>
 </LinearLayout>

+ 16 - 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,9 +12,13 @@
         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:orientation="vertical"
@@ -32,5 +36,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/ll_lock_status"
+        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>

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

@@ -281,7 +281,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 +335,12 @@
     <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>
 </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>

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

@@ -281,7 +281,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 +335,12 @@
     <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>
 </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>

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

@@ -281,7 +281,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 +335,12 @@
     <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>
 </resources>