Forráskód Böngészése

Merge remote-tracking branch 'origin/master' into wzbs

周文健 5 hónapja
szülő
commit
f00fc9a716
97 módosított fájl, 5211 hozzáadás és 627 törlés
  1. 6 1
      app/build.gradle
  2. 1 0
      app/src/main/AndroidManifest.xml
  3. 451 247
      app/src/main/java/com/grkj/iscs/BusinessManager.kt
  4. 1 1
      app/src/main/java/com/grkj/iscs/MyApplication.kt
  5. 9 0
      app/src/main/java/com/grkj/iscs/ble/BleCmdManager.kt
  6. 391 136
      app/src/main/java/com/grkj/iscs/ble/BleConnectionManager.kt
  7. 8 0
      app/src/main/java/com/grkj/iscs/enums/DeviceInputTypeEnum.kt
  8. 98 15
      app/src/main/java/com/grkj/iscs/modbus/DockBean.kt
  9. 21 4
      app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt
  10. 116 66
      app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt
  11. 11 1
      app/src/main/java/com/grkj/iscs/model/DictAndSystemConstants.kt
  12. 18 0
      app/src/main/java/com/grkj/iscs/model/ISCSDomainData.kt
  13. 22 2
      app/src/main/java/com/grkj/iscs/model/UrlConsts.kt
  14. 8 0
      app/src/main/java/com/grkj/iscs/model/bo/DockStatusBO.kt
  15. 61 6
      app/src/main/java/com/grkj/iscs/model/bo/WorkTicketGetBO.kt
  16. 1 0
      app/src/main/java/com/grkj/iscs/model/eventmsg/MsgEventConstants.kt
  17. 22 6
      app/src/main/java/com/grkj/iscs/util/ArcSoftUtil.kt
  18. 11 2
      app/src/main/java/com/grkj/iscs/util/BitmapUtil.kt
  19. 14 0
      app/src/main/java/com/grkj/iscs/util/MapViewExtension.kt
  20. 92 11
      app/src/main/java/com/grkj/iscs/util/NetApi.kt
  21. 12 10
      app/src/main/java/com/grkj/iscs/util/NetHttpManager.kt
  22. 87 28
      app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt
  23. 0 1
      app/src/main/java/com/grkj/iscs/view/activity/LoginActivity.kt
  24. 4 0
      app/src/main/java/com/grkj/iscs/view/activity/test/ModbusActivity.kt
  25. 83 0
      app/src/main/java/com/grkj/iscs/view/base/BaseNavFragment.kt
  26. 12 0
      app/src/main/java/com/grkj/iscs/view/base/BaseViewModel.kt
  27. 18 1
      app/src/main/java/com/grkj/iscs/view/dialog/FaceCaptureDialog.kt
  28. 14 2
      app/src/main/java/com/grkj/iscs/view/dialog/LoginDialog.kt
  29. 21 0
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationHomeFragment.kt
  30. 347 0
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationKeyAndLockFragment.kt
  31. 86 0
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationScanFragment.kt
  32. 44 0
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationTypeSelectFragment.kt
  33. 1 9
      app/src/main/java/com/grkj/iscs/view/fragment/DeviceStatusFragment.kt
  34. 9 0
      app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt
  35. 7 2
      app/src/main/java/com/grkj/iscs/view/fragment/FaceConfigFragment.kt
  36. 18 5
      app/src/main/java/com/grkj/iscs/view/fragment/FingerprintConfigFragment.kt
  37. 5 2
      app/src/main/java/com/grkj/iscs/view/fragment/JobExecutionFragment.kt
  38. 28 13
      app/src/main/java/com/grkj/iscs/view/fragment/JobProgressFragment.kt
  39. 41 2
      app/src/main/java/com/grkj/iscs/view/fragment/StepFragment.kt
  40. 37 1
      app/src/main/java/com/grkj/iscs/view/fragment/SwitchStatusFragment.kt
  41. 2 0
      app/src/main/java/com/grkj/iscs/view/fragment/WorkerFragment.kt
  42. 60 14
      app/src/main/java/com/grkj/iscs/view/fragment/WorkshopFragment.kt
  43. 9 0
      app/src/main/java/com/grkj/iscs/view/iview/IDeviceRegistrationHomeView.kt
  44. 7 0
      app/src/main/java/com/grkj/iscs/view/presenter/DeviceRegistrationHomePresenter.kt
  45. 4 5
      app/src/main/java/com/grkj/iscs/view/presenter/DeviceStatusPresenter.kt
  46. 12 0
      app/src/main/java/com/grkj/iscs/view/presenter/FaceConfigPresetner.kt
  47. 12 0
      app/src/main/java/com/grkj/iscs/view/presenter/FingerprintConfigPresenter.kt
  48. 69 16
      app/src/main/java/com/grkj/iscs/view/presenter/HomePresenter.kt
  49. 9 1
      app/src/main/java/com/grkj/iscs/view/presenter/JobProgressPresenter.kt
  50. 37 1
      app/src/main/java/com/grkj/iscs/view/presenter/StepPresenter.kt
  51. 32 1
      app/src/main/java/com/grkj/iscs/view/presenter/WorkerPresenter.kt
  52. 5 0
      app/src/main/java/com/grkj/iscs/view/step_mode/IStepMode.kt
  53. 7 0
      app/src/main/java/com/grkj/iscs/view/step_mode/StepMode1.kt
  54. 7 0
      app/src/main/java/com/grkj/iscs/view/step_mode/StepMode2.kt
  55. 291 0
      app/src/main/java/com/grkj/iscs/view/viewmodel/DeviceRegistrationKeyAndLockViewModel.kt
  56. 53 0
      app/src/main/java/com/grkj/iscs/view/viewmodel/DeviceRegistrationViewModel.kt
  57. 8 2
      app/src/main/java/com/grkj/iscs/view/widget/CustomStationLayer.kt
  58. 9 1
      app/src/main/java/com/grkj/iscs/view/widget/CustomSwitchStationLayer.kt
  59. 319 0
      app/src/main/java/com/onlylemi/mapview/library/MapView.java
  60. 19 0
      app/src/main/java/com/onlylemi/mapview/library/MapViewListener.java
  61. 59 0
      app/src/main/java/com/onlylemi/mapview/library/layer/MapBaseLayer.java
  62. 109 0
      app/src/main/java/com/onlylemi/mapview/library/layer/MapLayer.java
  63. 305 0
      app/src/main/java/com/onlylemi/mapview/library/utils/MapMath.java
  64. 334 0
      app/src/main/java/com/onlylemi/mapview/library/utils/MapUtils.java
  65. 87 0
      app/src/main/java/com/onlylemi/mapview/library/utils/math/FloydAlgorithm.java
  66. 440 0
      app/src/main/java/com/onlylemi/mapview/library/utils/math/GeneticAlgorithm.java
  67. 63 0
      app/src/main/java/com/onlylemi/mapview/library/utils/math/TSPNearestNeighbour.java
  68. 8 0
      app/src/main/res/drawable/bg_device_registration_type_select.xml
  69. 7 0
      app/src/main/res/drawable/divider_horizontal.xml
  70. 16 0
      app/src/main/res/layout/dialog_face_capture.xml
  71. 13 0
      app/src/main/res/layout/fragment_device_registration_home.xml
  72. 66 0
      app/src/main/res/layout/fragment_device_registration_key_and_lock.xml
  73. 27 0
      app/src/main/res/layout/fragment_device_registration_scan.xml
  74. 85 0
      app/src/main/res/layout/fragment_device_registration_type_select.xml
  75. 1 0
      app/src/main/res/layout/fragment_face_config.xml
  76. 1 0
      app/src/main/res/layout/fragment_fingerprint_config.xml
  77. 1 1
      app/src/main/res/layout/fragment_step.xml
  78. 1 1
      app/src/main/res/layout/fragment_switch_status.xml
  79. 2 1
      app/src/main/res/layout/item_rv_dock_test_child.xml
  80. 194 0
      app/src/main/res/layout/item_rv_key_dock_device_registration.xml
  81. 52 0
      app/src/main/res/layout/item_rv_lock_dock_child_device_registration.xml
  82. BIN
      app/src/main/res/mipmap-hdpi/end_point.png
  83. BIN
      app/src/main/res/mipmap-hdpi/mark_touch.png
  84. BIN
      app/src/main/res/mipmap-hdpi/start_point.png
  85. BIN
      app/src/main/res/mipmap-xhdpi/compass.png
  86. BIN
      app/src/main/res/mipmap-xhdpi/icon_card_input.png
  87. BIN
      app/src/main/res/mipmap-xhdpi/icon_key_lock_scan.png
  88. BIN
      app/src/main/res/mipmap-xhdpi/icon_rfid_input.png
  89. BIN
      app/src/main/res/mipmap-xhdpi/mark.png
  90. BIN
      app/src/main/res/mipmap/menu_icon_device_registration.png
  91. 26 0
      app/src/main/res/navigation/nav_device_input.xml
  92. 31 3
      app/src/main/res/values-en/strings.xml
  93. 31 3
      app/src/main/res/values-zh/strings.xml
  94. 1 0
      app/src/main/res/values/colors.xml
  95. 6 0
      app/src/main/res/values/dimens.xml
  96. 31 3
      app/src/main/res/values/strings.xml
  97. 7 0
      gradle/libs.versions.toml

+ 6 - 1
app/build.gradle

@@ -95,6 +95,11 @@ dependencies {
     implementation(libs.room.testing)
     ksp(libs.room.compiler)
 
+    implementation(libs.android.navigation.fragment)
+    implementation(libs.android.navigation.ui)
+    implementation(libs.android.navigation.dynamic.features.fragment)
+    implementation(libs.kotlinx.serialization.json)
+
     implementation fileTree(dir: "libs", include: ["*.jar", "*.aar"])
 
     // RV通用Adapter  https://github.com/hongyangAndroid/base-adapter
@@ -127,7 +132,7 @@ dependencies {
     implementation 'com.github.bumptech.glide:glide:4.11.0'
 
     // https://github.com/onlylemi/MapView
-    implementation 'com.github.onlylemi:mapview:v1.0'
+//    implementation 'com.github.onlylemi:mapview:v1.0'
 
     // CameraX 核心库
     implementation "androidx.camera:camera-core:1.2.0"

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

@@ -36,6 +36,7 @@
         android:roundIcon="@mipmap/logo"
         android:supportsRtl="true"
         android:theme="@style/Theme.ISCS"
+        android:largeHeap="true"
         tools:targetApi="31">
         <activity
             android:name=".view.activity.test.face.arcsoft.ArcsoftTestActivity"

A különbségek nem kerülnek megjelenítésre, a fájl túl nagy
+ 451 - 247
app/src/main/java/com/grkj/iscs/BusinessManager.kt


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

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

+ 9 - 0
app/src/main/java/com/grkj/iscs/ble/BleCmdManager.kt

@@ -112,6 +112,7 @@ object BleCmdManager {
      * 获取令牌
      */
     fun getToken(mac: String?, callback: CustomBleWriteCallback?) {
+        LogUtil.e("发送指令:获取令牌")
         LogUtil.i("$mac")
         BusinessManager.getBleDeviceByMac(mac)?.bleDevice?.let {
             LogUtil.i("Get token : $mac")
@@ -149,6 +150,7 @@ object BleCmdManager {
      * @param mode 0x01:工作模式 0x02:待机模式
      */
     fun switchMode(mode: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        LogUtil.e("发送指令:工作模式切换")
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
             BleUtil.instance?.write(
                 it.bleDevice,
@@ -179,6 +181,7 @@ object BleCmdManager {
         bleDevice: BleDevice,
         callback: CustomBleWriteCallback?
     ) {
+        LogUtil.e("发送指令:工作票下发")
         LogUtil.i("sendWorkTicket : $idx")
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
             it.ticketSend = json
@@ -279,6 +282,7 @@ object BleCmdManager {
      * 获取设备当前状态
      */
     fun getCurrentStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        LogUtil.w("发送指令:获取设备当前状态")
         BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
             BleUtil.instance?.write(
                 it.bleDevice,
@@ -358,6 +362,9 @@ object BleCmdManager {
 
                     override fun onWriteFailure(exception: BleException?) {
                         LogUtil.e("getTicketStatusPart fail")
+                        BusinessManager.getBleDeviceByMac(bleDevice.mac)?.let {
+                            it.ticketStatus = byteArrayOf()
+                        }
                         BusinessManager.sendEventMsg(
                             MsgEvent(
                                 MSG_EVENT_GET_TICKET_STATUS,
@@ -401,6 +408,7 @@ object BleCmdManager {
      * 获取钥匙电量
      */
     fun getPower(mac: String?, callback: CustomBleWriteCallback?) {
+        LogUtil.w("发送指令:获取钥匙电量")
         BusinessManager.getBleDeviceByMac(mac)?.let {
             BleUtil.instance?.write(
                 it.bleDevice,
@@ -521,6 +529,7 @@ object BleCmdManager {
      * 获取版本
      */
     fun getVersion(mac: String?, callback: CustomBleWriteCallback?) {
+        LogUtil.w("发送指令:获取版本")
         BusinessManager.getBleDeviceByMac(mac)?.let {
             BleUtil.instance?.write(
                 it.bleDevice,

+ 391 - 136
app/src/main/java/com/grkj/iscs/ble/BleConnectionManager.kt

@@ -16,6 +16,7 @@ import com.grkj.iscs.BusinessManager.sendLoadingEventMsg
 import com.grkj.iscs.R
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.modbus.ModBusController.controlKeyCharge
 import com.grkj.iscs.model.Constants.PERMISSION_REQUEST_CODE
 import com.grkj.iscs.model.eventmsg.LoadingMsg
 import com.grkj.iscs.model.eventmsg.MsgEvent
@@ -23,12 +24,19 @@ import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_LOADING
 import com.grkj.iscs.util.ActivityUtils
 import com.grkj.iscs.util.CommonUtils
 import com.grkj.iscs.util.Executor
+import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BaseActivity
 import com.sik.sikcore.activity.ActivityTracker
 import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
 import pub.devrel.easypermissions.AfterPermissionGranted
 import java.util.LinkedList
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 
 /**
  * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
@@ -62,26 +70,45 @@ object BleConnectionManager {
      * - 如果 mac 已在待连接队列或正在连接,忽略重复请求
      * - 否则将 mac 添加到队列并触发连接流程
      */
-    fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
-        LogUtil.i("registerConnectListener : $mac")
+    fun registerConnectListener(
+        mac: String,
+        connectNow: Boolean = false,
+        callBack: ((Boolean, BleBean?) -> Unit)? = null
+    ) {
+        LogUtil.i("蓝牙连接-开始连接 : $mac")
         // 已连接且已获取 token
-        deviceList.find { it.bleDevice.mac == mac && it.token != null }?.let { bean ->
+        deviceList.find {
+            it.bleDevice.mac == mac && BleManager.getInstance().isConnected(mac) && it.token != null
+        }?.let { bean ->
+            LogUtil.i("蓝牙连接-设备已连接")
             callBack?.invoke(true, bean)
             return
         }
+        if (connectNow) {
+            LogUtil.w("蓝牙连接-立即连接 mac: $mac")
+            unregisterConnectListener(mac)
+        }
         // 重复注册检查
         if (connectListeners.any { it.mac == mac } || currentConnectingMac == mac) {
-            LogUtil.w("忽略重复注册 mac: $mac")
+            LogUtil.w("蓝牙连接-忽略重复注册 mac: $mac")
+            callBack?.invoke(false, null)
             return
         }
         // 加入队列并启动连接
-        fun checkAndConnect() {
+        fun checkAndConnect(isDisconnectAll: Boolean = false) {
+            LogUtil.w("蓝牙连接-开始检查连接 mac: $mac")
             if (BleManager.getInstance().allConnectedDevice.size < maxConnectCount) {
                 connectListeners.add(ConnectListener(mac, callBack))
                 connectKey()
             } else {
-                ThreadUtils.runOnIODelayed(500) {
-                    checkAndConnect()
+                if (connectNow && !isDisconnectAll) {
+                    LogUtil.i("蓝牙连接-超过最大连接数,但是需要立即连接,断开所有连接进行连接")
+                    BleManager.getInstance().disconnectAllDevice()
+                    checkAndConnect(true)
+                } else {
+                    ThreadUtils.runOnIODelayed(500) {
+                        checkAndConnect()
+                    }
                 }
             }
         }
@@ -92,16 +119,17 @@ object BleConnectionManager {
      * 连接监听反注册
      */
     fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
-        LogUtil.i("unregisterConnectListener : $mac")
+        LogUtil.i("蓝牙连接-unregisterConnectListener : $mac")
         connectListeners.removeAll { it.mac == mac }
+        currentConnectingMac = ""
     }
 
     /**
      * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
      */
-    private fun checkProcess(mac: String?): Boolean {
+    private fun checkProcess(mac: String?, hideLoading: Boolean = true): Boolean {
         val canProcess = connectListeners.any { it.mac == mac }
-        if (!canProcess) sendLoadingEventMsg(null, false)
+        if (!canProcess && hideLoading) sendLoadingEventMsg(null, false)
         return canProcess
     }
 
@@ -110,15 +138,19 @@ object BleConnectionManager {
      */
     private fun connectKey() {
         if (connectListeners.isEmpty()) return
-        if (isPreparing || BleManager.getInstance().allConnectedDevice.size >= maxStandbyCount) {
+        if (isPreparing || BleManager.getInstance().allConnectedDevice.size > maxStandbyCount) {
+            LogUtil.i("暂时不能连接:${isPreparing},${BleManager.getInstance().allConnectedDevice.size > maxStandbyCount}")
             Executor.delayOnMain(1000) { connectKey() }
             return
         }
         val listener = connectListeners.first()
         currentConnectingMac = listener.mac
+        ThreadUtils.runOnIODelayed(10 * 1000) {
+            isPreparing = false
+        }
         isPreparing = true
         if (ActivityTracker.getCurrentActivity() == null) {
-            LogUtil.w("Ignore connectKey : ${listener.mac} no current activity")
+            LogUtil.w("蓝牙连接-Ignore connectKey : ${listener.mac} no current activity")
             isPreparing = false
             currentConnectingMac = null
             return
@@ -131,15 +163,12 @@ object BleConnectionManager {
                 currentConnectingMac = null
                 if (!isDone) {
                     // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
-                    if (checkProcess(listener.mac)) {
+                    if (checkProcess(listener.mac, false)) {
+                        listener.callBack?.invoke(false, null)
                         unregisterConnectListener(listener.mac)
-                        Executor.delayOnMain(2000) {
-                            registerConnectListener(
-                                listener.mac,
-                                listener.callBack
-                            )
-                        }
                     }
+                    ModBusController.controlKeyBuckle(true, listener.mac)
+                    LogUtil.i("蓝牙连接-连接钥匙失败")
                     return@runOnMain
                 }
                 // 判断是否仍然待连,防止拿走
@@ -148,30 +177,11 @@ object BleConnectionManager {
                     listener.callBack?.invoke(true, bleBean)
                     unregisterConnectListener(listener.mac)
                 }
-//                bleBean?.bleDevice?.mac?.let { mac ->
-//                    addToStandby(mac)
-//                }
                 if (connectListeners.isNotEmpty()) connectKey()
             }
         }
     }
 
-    /**
-     * 添加到待机队列并断开最旧超出连接
-     */
-    private fun addToStandby(mac: String) {
-        synchronized(standbyQueue) {
-            standbyQueue.addLast(mac)
-            while (standbyQueue.size > maxStandbyCount) {
-                val oldMac = standbyQueue.removeFirst()
-                deviceList.find { it.bleDevice.mac == oldMac }?.let { oldBean ->
-                    LogUtil.i("断开最旧待机设备: $oldMac")
-                    BleManager.getInstance().disconnect(oldBean.bleDevice)
-                }
-            }
-        }
-    }
-
     /**
      * @param loadingCallBack 是否显示loading、loading文字、流程是否结束
      * @param prepareDoneCallBack 蓝牙连接是否成功、蓝牙连接对象
@@ -182,8 +192,8 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        if (!checkProcess(mac)) {
-            LogUtil.e("Prepare is canceled : $mac")
+        if (!checkProcess(mac, false)) {
+            LogUtil.e("蓝牙连接-Prepare is canceled : $mac")
             return
         }
         Executor.runOnMain {
@@ -199,9 +209,9 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        LogUtil.i("doScanBle:$mac")
-        if (!checkProcess(mac)) {
-            LogUtil.e("Prepare is canceled : $mac")
+        LogUtil.i("蓝牙连接-doScanBle:$mac")
+        if (!checkProcess(mac, false)) {
+            LogUtil.e("蓝牙连接-Prepare is canceled : $mac")
             return
         }
         if (isNeedLoading) sendEventMsg(
@@ -212,12 +222,13 @@ object BleConnectionManager {
         BleUtil.instance?.scan(object : CustomBleScanCallback() {
             override fun onPrompt(promptStr: String?) {
                 // 蓝牙未启动重试
+                LogUtil.i("蓝牙连接-参数:${promptStr}")
                 BleManager.getInstance().enableBluetooth()
                 doScanBle(mac, isNeedLoading, prepareDoneCallBack)
             }
 
             override fun onScanStarted(success: Boolean) {
-                LogUtil.i("onScanStarted:${success}")
+                LogUtil.i("蓝牙连接-onScanStarted:${success}")
                 if (!success) {
                     if (isNeedLoading) sendEventMsg(
                         MsgEvent(
@@ -229,14 +240,19 @@ object BleConnectionManager {
             }
 
             override fun onScanning(bleDevice: BleDevice?) {
-                LogUtil.i("onScanning:${bleDevice?.mac}")
-                bleDevice?.let {
-                    doConnect(it, isNeedLoading, prepareDoneCallBack)
+                val mac = bleDevice?.mac ?: return
+                LogUtil.i("蓝牙连接-onScanning:$mac")
+                if (mac.equals(mac, ignoreCase = true)) {
+                    // 找到目标设备,马上停止扫描
+                    LogUtil.i("找到目标设备 $mac,停止扫描并尝试连接")
+                    BleManager.getInstance().cancelScan()
+                    // 立刻调用 doConnect,下一步进入连接流程
+                    doConnect(bleDevice, isNeedLoading, prepareDoneCallBack)
                 }
             }
 
             override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
-                LogUtil.i("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
+                LogUtil.i("蓝牙连接-onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
                 if (isNeedLoading) sendEventMsg(
                     MsgEvent(
                         MSG_EVENT_LOADING, LoadingMsg(false, null, null)
@@ -259,9 +275,9 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        LogUtil.i("doConnect : ${bleDevice.mac}")
-        if (!checkProcess(bleDevice.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+        LogUtil.i("蓝牙连接-doConnect : ${bleDevice.mac}")
+        if (!checkProcess(bleDevice.mac, false)) {
+            LogUtil.e("蓝牙连接-Prepare is canceled : ${bleDevice.mac}")
             return
         }
         if (isNeedLoading) sendEventMsg(
@@ -270,97 +286,105 @@ object BleConnectionManager {
                 LoadingMsg(true, CommonUtils.getStr(R.string.ble_connecting), null)
             )
         )
-        BleManager.getInstance().disconnect(bleDevice)
-        BleUtil.instance?.connectBySelect(
-            bleDevice, object : CustomBleGattCallback() {
-                override fun onPrompt(promptStr: String?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, promptStr, null)
+        ThreadUtils.runOnIO {
+            BleManager.getInstance().disconnect(bleDevice)
+            delay(300)
+            BleUtil.instance?.connectBySelect(
+                bleDevice, object : CustomBleGattCallback() {
+                    override fun onPrompt(promptStr: String?) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING, LoadingMsg(false, promptStr, null)
+                            )
                         )
-                    )
-                }
+                    }
 
-                override fun onStartConnect() {}
+                    override fun onStartConnect() {}
 
-                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING,
-                            LoadingMsg(false, CommonUtils.getStr(R.string.ble_connect_fail), false)
+                    override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING,
+                                LoadingMsg(
+                                    false,
+                                    CommonUtils.getStr(R.string.ble_connect_fail),
+                                    false
+                                )
+                            )
                         )
-                    )
-                    LogUtil.e("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
-                    prepareDoneCallBack?.invoke(false, null)
-                }
+                        LogUtil.e("蓝牙连接-onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
+                        prepareDoneCallBack?.invoke(false, null)
+                    }
 
-                override fun onConnectSuccess(
-                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
-                ) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                    override fun onConnectSuccess(
+                        bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
+                    ) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING, LoadingMsg(false, null, null)
+                            )
                         )
-                    )
-                    LogUtil.i("onConnectSuccess : ${bleDevice?.mac}")
-                    bleDevice?.let {
-                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
-                        val bleBean = BleBean(it)
-                        deviceList.add(bleBean)
-                        removeExceptionKey(it.mac)
-                        // 设置MTU
-                        Executor.delayOnMain(200) {
-                            if (!checkProcess(bleDevice.mac)) {
-                                LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
-                                return@delayOnMain
+                        LogUtil.i("蓝牙连接-onConnectSuccess : ${bleDevice?.mac}")
+                        bleDevice?.let {
+                            deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                            val bleBean = BleBean(it)
+                            deviceList.add(bleBean)
+                            removeExceptionKey(it.mac)
+                            // 设置MTU
+                            Executor.delayOnMain(200) {
+                                if (!checkProcess(bleDevice.mac, false)) {
+                                    LogUtil.e("Prepare is canceled : ${bleDevice.mac}")
+                                    return@delayOnMain
+                                }
+                                BleUtil.instance?.setMtu(it)
+                            }
+                            // 监听
+                            Executor.delayOnMain(500) {
+                                indicate(bleBean, isNeedLoading, prepareDoneCallBack)
                             }
-                            BleUtil.instance?.setMtu(it)
-                        }
-                        // 监听
-                        Executor.delayOnMain(500) {
-                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
                         }
                     }
-                }
 
-                override fun onDisConnected(
-                    isActiveDisConnected: Boolean,
-                    device: BleDevice?,
-                    gatt: BluetoothGatt?,
-                    status: Int
-                ) {
-                    if (isNeedLoading) sendEventMsg(
-                        MsgEvent(
-                            MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                    override fun onDisConnected(
+                        isActiveDisConnected: Boolean,
+                        device: BleDevice?,
+                        gatt: BluetoothGatt?,
+                        status: Int
+                    ) {
+                        if (isNeedLoading) sendEventMsg(
+                            MsgEvent(
+                                MSG_EVENT_LOADING, LoadingMsg(false, null, false)
+                            )
                         )
-                    )
-                    LogUtil.i("onDisConnected : ${device?.mac} - $isActiveDisConnected")
-                    getBleDeviceByMac(device?.mac)?.let {
-                        deviceList.remove(it)
-                    }
-                    bleDevice.mac?.let { itMac ->
-                        unregisterConnectListener(itMac)
-                    }
-                    if (!isActiveDisConnected) {
-                        // 测试模式下不重连
-                        if (isTestMode) {
-                            return
+                        LogUtil.i("蓝牙连接-onDisConnected : ${device?.mac} - $isActiveDisConnected")
+                        getBleDeviceByMac(device?.mac)?.let {
+                            deviceList.remove(it)
+                            it.token = null
+                        }
+                        bleDevice.mac?.let { itMac ->
+                            unregisterConnectListener(itMac)
                         }
-                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
-                        Executor.delayOnMain(300) {
-                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
-                                if (isDone && bleBean != null) {
-                                    Executor.delayOnMain(300) {
-                                        getCurrentStatus(6, bleBean.bleDevice)
+                        if (!isActiveDisConnected) {
+                            // 测试模式下不重连
+                            if (isTestMode) {
+                                return
+                            }
+                            // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
+                            Executor.delayOnMain(300) {
+                                registerConnectListener(bleDevice.mac) { isDone, bleBean ->
+                                    if (isDone && bleBean != null) {
+                                        Executor.delayOnMain(300) {
+                                            getCurrentStatus(6, bleBean.bleDevice)
+                                        }
                                     }
                                 }
                             }
+                        } else {
+                            ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
                         }
-                    } else {
-                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
                     }
-                }
-            })
+                })
+        }
     }
 
     /**
@@ -371,8 +395,8 @@ object BleConnectionManager {
         isNeedLoading: Boolean = false,
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
-        if (!checkProcess(bleBean?.bleDevice?.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+        if (!checkProcess(bleBean?.bleDevice?.mac, false)) {
+            LogUtil.e("蓝牙连接-Prepare is canceled : ${bleBean?.bleDevice?.mac}")
             return
         }
         if (isNeedLoading) sendEventMsg(
@@ -385,19 +409,19 @@ object BleConnectionManager {
             BleUtil.instance?.indicate(
                 it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
                     override fun onPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onPrompt : $promptStr")
+                        LogUtil.i("蓝牙连接-indicate onPrompt : $promptStr")
                     }
 
                     override fun onConnectPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onConnectPrompt : $promptStr")
+                        LogUtil.i("蓝牙连接-indicate onConnectPrompt : $promptStr")
                     }
 
                     override fun onDisConnectPrompt(promptStr: String?) {
-                        LogUtil.i("indicate onDisConnectPrompt : $promptStr")
+                        LogUtil.i("蓝牙连接-indicate onDisConnectPrompt : $promptStr")
                     }
 
                     override fun onIndicateSuccess() {
-                        LogUtil.i("onIndicateSuccess")
+                        LogUtil.i("蓝牙连接-onIndicateSuccess")
                         isIndicateSuccess = true
                         getToken(bleBean, isNeedLoading, prepareDoneCallBack)
                     }
@@ -408,7 +432,7 @@ object BleConnectionManager {
                                 MSG_EVENT_LOADING, LoadingMsg(false, null, false)
                             )
                         )
-                        LogUtil.e("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
+                        LogUtil.e("蓝牙连接-onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
                         Executor.delayOnIO(500) {
                             if (isIndicateSuccess) {
                                 return@delayOnIO
@@ -418,7 +442,7 @@ object BleConnectionManager {
                     }
 
                     override fun onCharacteristicChanged(data: ByteArray?) {
-                        LogUtil.i("onCharacteristicChanged : ${data?.toHexStrings()}")
+                        LogUtil.i("蓝牙连接-onCharacteristicChanged : ${data?.toHexStrings()}")
                         data?.let { itData ->
                             BusinessManager.handleRsp(
                                 it,
@@ -441,7 +465,7 @@ object BleConnectionManager {
         prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
     ) {
         if (!checkProcess(bleBean?.bleDevice?.mac)) {
-            LogUtil.e("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            LogUtil.e("蓝牙连接-Prepare is canceled : ${bleBean?.bleDevice?.mac}")
             return
         }
         if (isNeedLoading) sendEventMsg(
@@ -457,7 +481,7 @@ object BleConnectionManager {
                             MSG_EVENT_LOADING, LoadingMsg(false, "token获取成功", null)
                         )
                     )
-                    LogUtil.i("getToken success : ${bleBean.bleDevice.mac}")
+                    LogUtil.i("蓝牙连接-getToken success : ${bleBean.bleDevice.mac}")
                 }
 
                 override fun onWriteFailure(exception: BleException?) {
@@ -466,13 +490,244 @@ object BleConnectionManager {
                             MSG_EVENT_LOADING, LoadingMsg(false, "token获取失败", false)
                         )
                     )
-                    LogUtil.e("getToken fail : ${bleBean.bleDevice.mac}")
+                    LogUtil.e("蓝牙连接-getToken fail : ${bleBean.bleDevice.mac}")
                     prepareDoneCallBack?.invoke(false, null)
                 }
             })
         }
     }
 
+    /**
+     * 对单个 MAC 做下面两步:
+     *   1. 先尝试不充电连接,若成功就返回 true;
+     *   2. 否则开启“充电”,等 500ms,再尝试一次连接,连接成功后断电并返回 true;否则返回 false。
+     */
+    suspend fun tryConnectWithOptionalCharge(mac: String, withOpenCharge: Boolean = true): Boolean =
+        withContext(Dispatchers.IO) {
+            // -------- 第一次尝试 --------
+            LogUtil.i("蓝牙连接-第一次尝试")
+            val firstTry = suspendCancellableCoroutine<Boolean> { cont ->
+                // 1. 定义一个 flag,确保只 resume 一次
+                var isCalled = false
+                BusinessManager.registerConnectListener(mac, true) { isDone, _ ->
+                    if (isCalled) {
+                        return@registerConnectListener
+                    }
+                    isCalled = true
+                    if (isDone) {
+                        // 连接成功后,把电关掉(以防万一)
+                        controlKeyCharge(false, mac) { }
+                        cont.resume(true)
+                    } else {
+                        cont.resume(false)
+                    }
+                    cont.cancel()
+                }
+            }
+            LogUtil.i("蓝牙连接-第一次连接:${firstTry},是否继续尝试上电连接${withOpenCharge}")
+            if (firstTry) {
+                return@withContext true
+            }
+            if (!withOpenCharge) {
+                return@withContext false
+            }
+            // -------- 第二次尝试:先开电,再连 --------
+            // 开电,并等待回调
+            suspendCoroutine<Unit> { unitCont ->
+                controlKeyCharge(false, mac) {
+                    ThreadUtils.runOnIO {
+                        delay(500)
+                        controlKeyCharge(true, mac) {
+                            unitCont.resume(Unit)
+                        }
+                    }
+                }
+            }
+            LogUtil.i("蓝牙连接-开启充电并等待500ms")
+            // 等 500ms 保证硬件电源稳定
+            delay(500)
+
+            // 再次注册连接监听
+            val secondTry = suspendCancellableCoroutine<Boolean> { cont ->
+                var isCalled = false
+                BusinessManager.registerConnectListener(mac, true) { isDone, _ ->
+                    if (isCalled) {
+                        return@registerConnectListener
+                    }
+                    isCalled = true
+                    // 无论成功或失败,都先把电关掉
+                    controlKeyCharge(false, mac) { }
+                    cont.resume(isDone)
+                    cont.cancel()
+                }
+            }
+            return@withContext secondTry
+        }
+
+    /**
+     * 扫描在线的蓝牙钥匙并发送指令关机
+     */
+    suspend fun scanOnlineKeyLockMacAndSwitchModeToClose(): Boolean {
+        return suspendCancellableCoroutine { parentCont ->
+            BleUtil.instance?.scan(object : CustomBleScanCallback() {
+                override fun onPrompt(promptStr: String?) {
+                    // 蓝牙未启动重试
+                    LogUtil.i("设备录入-参数:${promptStr}")
+                    BleManager.getInstance().enableBluetooth()
+                    ThreadUtils.runOnMainDelayed(300) {
+                        scanOnlineKeyLockMacAndSwitchModeToClose()
+                    }
+                }
+
+                override fun onScanStarted(success: Boolean) {
+                    LogUtil.i("设备录入-onScanStarted:${success}")
+                    if (!success) {
+                        ThreadUtils.runOnMainDelayed(300) {
+                            scanOnlineKeyLockMacAndSwitchModeToClose()
+                        }
+                    }
+                }
+
+                override fun onScanning(bleDevice: BleDevice?) {
+                    val mac = bleDevice?.mac ?: return
+                    LogUtil.i("设备录入-onScanning:$mac")
+                }
+
+                override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                    val devicesSnapshot = scanResultList?.toList().orEmpty()
+                    ThreadUtils.runOnIO {
+                        devicesSnapshot.forEach {
+                            val connected = tryConnectWithOptionalCharge(it.mac, false)
+                            if (connected) {
+                                val sendSuccess = sendEmptyTicketJson(it)
+                                LogUtil.i("设备录入-发送切换工作模式:${it.mac},${sendSuccess}")
+                            }
+                        }
+                        parentCont.resume(true)
+                    }
+                }
+            })
+        }
+    }
+
+    /**
+     * 发送空作业票
+     */
+    private suspend fun sendEmptyTicketJson(bleDevice: BleDevice): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            BleCmdManager.sendWorkTicket(
+                BusinessManager.generateEmptyTicketSendJson(),
+                bleDevice = bleDevice,
+                callback = object : CustomBleWriteCallback() {
+                    override fun onWriteSuccess(
+                        current: Int,
+                        total: Int,
+                        justWrite: ByteArray?
+                    ) {
+                        ThreadUtils.runOnIO {
+                            delay(3000)
+                            cont.resume(switchWorkMode(bleDevice))
+                        }
+                    }
+
+                    override fun onWriteFailure(exception: BleException?) {
+                        ThreadUtils.runOnMainDelayed(300) {
+                            cont.resume(sendEmptyTicketJson(bleDevice))
+                        }
+                    }
+                })
+        }
+    }
+
+    /**
+     * 切换工作模式
+     */
+    private suspend fun switchWorkMode(bleDevice: BleDevice): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            BleCmdManager.switchMode(
+                BleConst.STATUS_WORK,
+                bleDevice,
+                object : CustomBleWriteCallback() {
+                    override fun onWriteSuccess(
+                        current: Int,
+                        total: Int,
+                        justWrite: ByteArray?
+                    ) {
+                        BleManager.getInstance().disconnect(bleDevice)
+                        ThreadUtils.runOnIO {
+                            delay(800)
+                            cont.resume(true)
+                        }
+                        LogUtil.i("设备录入-切换模式发送成功 : ${bleDevice.mac}")
+                    }
+
+                    override fun onWriteFailure(exception: BleException?) {
+                        LogUtil.e("设备录入-切换模式发送失败 : ${bleDevice.mac}")
+                        ThreadUtils.runOnMainDelayed(300) {
+                            cont.resume(sendEmptyTicketJson(bleDevice))
+                        }
+                    }
+                })
+        }
+    }
+
+    /**
+     * 切换待机模式
+     */
+    fun switchReadyMode(bleDevice: BleDevice) {
+        BleCmdManager.switchMode(
+            BleConst.STATUS_READY,
+            bleDevice,
+            object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(
+                    current: Int,
+                    total: Int,
+                    justWrite: ByteArray?
+                ) {
+                    BleManager.getInstance().disconnect(bleDevice)
+                    LogUtil.i("设备录入-切换模式发送成功 : ${bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    LogUtil.e("设备录入-切换模式发送失败 : ${bleDevice.mac}")
+                }
+            })
+    }
+
+    /**
+     * 扫描在线的蓝牙
+     */
+    fun scanOnlineKeyLockMac(callback: (List<BleDevice>?) -> Unit) {
+        BleUtil.instance?.scan(object : CustomBleScanCallback() {
+            override fun onPrompt(promptStr: String?) {
+                // 蓝牙未启动重试
+                LogUtil.d("设备录入-参数:${promptStr}")
+                BleManager.getInstance().enableBluetooth()
+                ThreadUtils.runOnMainDelayed(300) {
+                    scanOnlineKeyLockMac(callback)
+                }
+            }
+
+            override fun onScanStarted(success: Boolean) {
+                LogUtil.d("设备录入-onScanStarted:${success}")
+                if (!success) {
+                    ThreadUtils.runOnMainDelayed(300) {
+                        scanOnlineKeyLockMac(callback)
+                    }
+                }
+            }
+
+            override fun onScanning(bleDevice: BleDevice?) {
+                val mac = bleDevice?.mac ?: return
+                LogUtil.d("设备录入-onScanning:$mac")
+            }
+
+            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                LogUtil.d("设备录入-扫描完成:$scanResultList")
+                callback(scanResultList?.toList())
+            }
+        })
+    }
 
     // 蓝牙连接准备监听
     data class ConnectListener(

+ 8 - 0
app/src/main/java/com/grkj/iscs/enums/DeviceInputTypeEnum.kt

@@ -0,0 +1,8 @@
+package com.grkj.iscs.enums
+
+/**
+ * 设备录入类型
+ */
+enum class DeviceInputTypeEnum {
+    CARD, RFID, NONE;
+}

+ 98 - 15
app/src/main/java/com/grkj/iscs/modbus/DockBean.kt

@@ -1,6 +1,9 @@
 package com.grkj.iscs.modbus
 
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
 import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.MyApplication
 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
@@ -13,7 +16,9 @@ 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.SPUtils
 import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.fragment.DockTestFragment
 import java.util.concurrent.atomic.AtomicInteger
 
 /**
@@ -25,10 +30,21 @@ import java.util.concurrent.atomic.AtomicInteger
  */
 class DockBean(
     var addr: Byte,
+    var row: Int,
+    var col: Int,
     var type: Byte?,
     var isWorking: Boolean = true,
     var deviceList: MutableList<DeviceBean>
 ) {
+    companion object {
+        val dockConfig: List<DockTestFragment.DockTestBean> by lazy {
+            val json =
+                SPUtils.getDockConfig(MyApplication.instance?.applicationContext!!)
+            val type = object : TypeToken<List<DockTestFragment.DockTestBean>>() {}.type
+            return@lazy Gson().fromJson(json, type)
+        }
+    }
+
     /**
      * 转换状态
      */
@@ -67,11 +83,10 @@ class DockBean(
                                 keyBean.isCharging = isLeftCharging
 
                                 // 拿走钥匙,移除钥匙信息
-                                if (!leftHasKey) {
+                                if (!leftHasKey && !keyBean.isReady) {
                                     changeList.add(keyBean.clone())
                                     keyBean.rfid = null
                                     keyBean.mac = null
-                                    keyBean.isReady = false
                                 } else {
                                     changeList.add(keyBean)
                                 }
@@ -81,19 +96,24 @@ class DockBean(
                                 keyBean.isExist = rightHasKey
                                 keyBean.isCharging = isRightCharging
                                 // 拿走钥匙,移除钥匙信息
-                                if (!rightHasKey) {
+                                if (!rightHasKey && !keyBean.isReady) {
                                     changeList.add(keyBean.clone())
                                     keyBean.rfid = null
                                     keyBean.mac = null
-                                    keyBean.isReady = false
                                 } else {
                                     changeList.add(keyBean)
                                 }
                             }
                         }
                     }
-
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_LOCK -> {
@@ -125,7 +145,14 @@ class DockBean(
                     }
 
                     LogUtil.i("锁具刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_ELEC_LOCK_BOARD -> {
@@ -204,13 +231,27 @@ class DockBean(
                         changeList.add(getFingerPrintList()[0])
                     }
                     LogUtil.i("便携式刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_COLLECT -> {
                     val working = (byteArray[4].toInt() shr 0) and 0x1 == 1
                     LogUtil.i("开关量采集板是否工作 : $working")
-                    return DockBean(addr, it, working, mutableListOf())
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        working,
+                        mutableListOf()
+                    )
                 }
 
                 else -> return null
@@ -244,7 +285,14 @@ class DockBean(
                         }
                     }
 
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_LOCK -> {
@@ -263,7 +311,14 @@ class DockBean(
                     }
 
                     LogUtil.i("锁具刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_ELEC_LOCK_BOARD -> {
@@ -284,7 +339,14 @@ class DockBean(
                     }
 
                     LogUtil.i("电磁锁具刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 DOCK_TYPE_PORTABLE -> {
@@ -305,7 +367,14 @@ class DockBean(
                         getKeyList()[0].lockEnabled = keyLockEnabled
                     }
                     LogUtil.i("便携式刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 else -> return null
@@ -363,7 +432,14 @@ class DockBean(
                             }
                         }
                     }
-                    return DockBean(addr, type, isWorking, deviceList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        type,
+                        isWorking,
+                        deviceList
+                    )
                 }
 
                 else -> return null
@@ -396,7 +472,14 @@ class DockBean(
                     }
 
                     LogUtil.i("锁具刷新状态 : $changeList")
-                    return DockBean(addr, it, true, changeList)
+                    return DockBean(
+                        addr,
+                        dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
+                        dockConfig.find { it.address == addr }?.column?.toInt() ?: 0,
+                        it,
+                        true,
+                        changeList
+                    )
                 }
 
                 else -> return null

+ 21 - 4
app/src/main/java/com/grkj/iscs/modbus/ModBusCMDHelper.kt

@@ -1,5 +1,7 @@
 package com.grkj.iscs.modbus
 
+import com.grkj.iscs.model.DeviceConst
+
 /**
  * 通信指令帮助工具
  */
@@ -112,8 +114,23 @@ object ModBusCMDHelper {
             byteArrayOf(
                 0x00,
                 0x11,
-                if (index == 1) 0b00010000.toByte() else 0b0000001.toByte(),
-                if (isOpen) 0x00 else 0xFF.toByte()
+                (if (index == 1) 0b00010000 else 0b0000001).toByte(),
+                (if (isOpen) 0x00 else 0xFF).toByte()
+            )
+        )
+    }
+
+    /**
+     * 操作钥匙/便携式底座钥匙卡扣 打开,所有
+     */
+    fun generateAllKeyBuckleOpenCmd(type: Byte): MBFrame {
+        return MBFrame(
+            FRAME_TYPE_WRITE,
+            byteArrayOf(
+                0x00,
+                0x11,
+                (if (type == DeviceConst.DOCK_TYPE_PORTABLE) 0b00110000 else 0xff).toByte(),
+                0x00
             )
         )
     }
@@ -130,8 +147,8 @@ object ModBusCMDHelper {
             byteArrayOf(
                 0x00,
                 0x11,
-                if (index == 1) 0b00100000.toByte() else 0b0000010.toByte(),
-                if (isOpen) 0xFF.toByte() else 0x00.toByte()
+                (if (index == 1) 0b00100000 else 0b00000010).toByte(),
+                (if (isOpen) 0xFF else 0x00).toByte()
             )
         )
     }

+ 116 - 66
app/src/main/java/com/grkj/iscs/modbus/ModBusController.kt

@@ -5,6 +5,7 @@ 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.ble.BleConnectionManager
 import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_CARD
@@ -22,10 +23,9 @@ import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
+import com.sik.sikcore.thread.ThreadUtils
 import java.util.concurrent.atomic.AtomicInteger
 import java.util.stream.Collectors
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
 
 
 /**
@@ -152,6 +152,8 @@ object ModBusController {
                 }
                 LogUtil.i("initDevicesStatus 设备(${bytes[0].toInt()})类型:$type")
             }
+            //先打开所有钥匙仓位,再进行初始化,防止一开始锁仓没有钥匙,但是锁定状态下会出现状态锁定
+            controlAllKeyBuckleOpen()
             // TODO 待完善
             Executor.repeatOnMain({
                 if (isInitReady) {
@@ -186,9 +188,10 @@ object ModBusController {
                         val rfid = res.copyOfRange(3, 11).toHexStrings(false).removeLeadingZeros()
                         LogUtil.i("初始化锁具 RFID : $rfid")
                         updateLockRfid(dockBean.addr, idx, rfid)
-                        NetApi.getLockInfo(rfid) {
-                            updateLockNewHardware(dockBean.addr, idx, it == null)
-                        }
+                        //todo 为设备录入增加
+//                        NetApi.getLockInfo(rfid) {
+//                            updateLockNewHardware(dockBean.addr, idx, it == null)
+//                        }
                     }
                 }
                 controlLockBuckle(false, dockBean.addr, hasLockIdxList)
@@ -217,37 +220,39 @@ object ModBusController {
                             // 更新rfid
                             updateKeyRfid(dockBean.addr, isLeft, rfid)
                             // 蓝牙准备操作
-                            NetApi.getKeyInfo(rfid) {
-                                LogUtil.i("getKeyInfo : $rfid - ${it?.macAddress}")
-                                updateKeyNewHardware(dockBean.addr, isLeft, it == null)
-                                if (it != null && !it.macAddress.isNullOrEmpty()) {
+                            NetApi.getKeyInfo(rfid) { keyInfo ->
+                                LogUtil.i("getKeyInfo : $rfid - ${keyInfo?.macAddress}")
+                                updateKeyNewHardware(dockBean.addr, isLeft, keyInfo == null)
+                                if (keyInfo != null && !keyInfo.macAddress.isNullOrEmpty()) {
                                     // 更新mac
-                                    updateKeyMac(dockBean.addr, key.isLeft, it.macAddress)
-                                    BusinessManager.registerConnectListener(
-                                        it.macAddress
-                                    ) { isDone, bleBean ->
-                                        if (isDone && bleBean?.bleDevice != null) {
-                                            Executor.delayOnMain(500) {
-                                                BusinessManager.getCurrentStatus(
-                                                    3, bleBean.bleDevice
-                                                )
-                                                BusinessManager.getBatteryPower(bleBean.bleDevice)
+                                    updateKeyMac(dockBean.addr, key.isLeft, keyInfo.macAddress)
+                                    ThreadUtils.runOnIO {
+                                        val isConnect =
+                                            BleConnectionManager.tryConnectWithOptionalCharge(
+                                                keyInfo.macAddress
+                                            )
+                                        if (isConnect) {
+                                            val bleBean =
+                                                BusinessManager.getBleDeviceByMac(keyInfo.macAddress)
+                                            bleBean?.let {
+                                                Executor.delayOnMain(500) {
+                                                    BusinessManager.getCurrentStatus(
+                                                        3, it.bleDevice
+                                                    )
+                                                    BusinessManager.getBatteryPower(it.bleDevice)
+                                                }
                                             }
                                         }
                                     }
+                                    controlKeyBuckle(false, key.isLeft, dockBean.addr)
                                 } else {
                                     ToastUtils.tip(R.string.get_key_info_fail)
+                                    controlKeyBuckle(true, key.isLeft, dockBean.addr)
                                 }
                             }
                         }
-                        controlKeyBuckle(false, key.isLeft, dockBean.addr)
                     } else {
                         controlKeyBuckle(true, key.isLeft, dockBean.addr)
-                        controlKeyCharge(
-                            false,
-                            key.isLeft,
-                            dockBean.addr
-                        )
                     }
                 }
             }
@@ -494,7 +499,16 @@ object ModBusController {
         dock?.let {
             it.type = type
         } ?: let {
-            dockList.add(DockBean(idx, type, true, mutableListOf()))
+            dockList.add(
+                DockBean(
+                    idx,
+                    DockBean.dockConfig.find { it.address == idx }?.row?.toInt() ?: 0,
+                    DockBean.dockConfig.find { it.address == idx }?.column?.toInt() ?: 0,
+                    type,
+                    true,
+                    mutableListOf()
+                )
+            )
         }
     }
 
@@ -680,12 +694,45 @@ object ModBusController {
         slaveAddress?.let {
             ModBusCMDHelper.generateKeyBuckleCmd(isOpen, if (isLeft) 0 else 1)?.let { cmd ->
                 modBusManager?.sendTo(it, cmd) { res ->
+                    if (isOpen) {
+                        controlKeyCharge(false, isLeft, slaveAddress)
+                    }
                     done?.invoke(res)
                 }
             }
         }
     }
 
+    /**
+     * 打开所有钥匙锁仓
+     */
+    fun controlAllKeyBuckleOpen() {
+        dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+            .forEach { dock ->
+                dock.type?.let { dockType ->
+                    ModBusCMDHelper.generateAllKeyBuckleOpenCmd(dockType).let { cmd ->
+                        LogUtil.i("硬件:打开所有钥匙锁仓,${dock.addr},${cmd.data}")
+                        modBusManager?.sendTo(dock.addr, cmd) { res ->
+                        }
+                    }
+                }
+            }
+    }
+
+    /**
+     * 关锁并充电
+     */
+    fun controlKeyLockAndCharge(
+        isOpen: Boolean,
+        isLeft: Boolean,
+        slaveAddress: Byte?,
+        done: ((res: ByteArray) -> Unit)? = null
+    ) {
+        controlKeyBuckle(!isOpen, isLeft, slaveAddress) {
+            controlKeyCharge(isOpen, isLeft, slaveAddress, done)
+        }
+    }
+
     /**
      * 控制钥匙充电
      */
@@ -711,7 +758,9 @@ object ModBusController {
     ) {
         slaveAddress?.let {
             ModBusCMDHelper.generateKeyBuckleChargeCmd(isOpen, if (isLeft) 0 else 1)?.let { cmd ->
+                LogUtil.i("钥匙充电:${isOpen},${isLeft},${slaveAddress},${cmd.data.toHexStrings()}")
                 modBusManager?.sendTo(it, cmd) { res ->
+                    LogUtil.i("钥匙充电接收:${isOpen},${isLeft},${slaveAddress},${res.toHexStrings()}")
                     done?.invoke(res)
                 }
             }
@@ -751,6 +800,15 @@ object ModBusController {
             ?.find { it.isLeft == isLeft && it.isExist } != null
     }
 
+    /**
+     * 钥匙是否是新设备
+     */
+    fun isKeyNewHardware(dockAddr: Byte?, isLeft: Boolean): Boolean {
+        dockAddr ?: return false
+        return dockList.find { it.addr == dockAddr }?.getKeyList()
+            ?.find { it.isLeft == isLeft && it.newHardware } != null
+    }
+
     /**
      * 获取钥匙锁仓的锁定状态
      */
@@ -778,6 +836,15 @@ object ModBusController {
             ?.find { it.idx == lockIdx && it.isExist } != null
     }
 
+    /**
+     * 获取挂锁是否是新设备
+     */
+    fun isLockNewHardware(dockAddr: Byte?, lockIdx: Int): Boolean {
+        dockAddr ?: return false
+        return dockList.find { it.addr == dockAddr }?.getLockList()
+            ?.find { it.idx == lockIdx && it.newHardware } != null
+    }
+
     /**
      * 获取钥匙状态
      *
@@ -946,55 +1013,37 @@ object ModBusController {
     ): Pair<Byte, DockBean.KeyBean?>? {
         // 1. 过滤并准备钥匙列表
         val slotCols = exceptionSlots.mapNotNull { it.col?.toInt() }
-        val keyDockList = dockList
-            .filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
-            .sortedBy { it.addr }
-            .onEach { it.deviceList.sortBy { dev -> dev.idx } }
-
-        val keyList = keyDockList
-            .flatMap { it.deviceList }
-            .filterIsInstance<DockBean.KeyBean>()
-            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }
-            .filter { kb ->
-                !kb.rfid.isNullOrEmpty()
-                        && kb.rfid !in exceptionKeysRfid
-                        && kb.mac !in exceptionKeysMac
-                        && !kb.mac.isNullOrEmpty()
-                        && kb.isExist
-            }
-            .shuffled()
-
+        val keyDockList =
+            dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+                .sortedBy { it.addr }.onEach { it.deviceList.sortBy { dev -> dev.idx } }
+
+        val keyList = keyDockList.flatMap { it.deviceList }.apply {
+            LogUtil.i("keyStatus:${this}")
+        }.filterIsInstance<DockBean.KeyBean>()
+            .filterIndexed { idx, _ -> (idx + 1) !in slotCols }.filter { kb ->
+                !kb.rfid.isNullOrEmpty() && kb.rfid !in exceptionKeysRfid && kb.mac !in exceptionKeysMac && !kb.mac.isNullOrEmpty() && kb.isExist
+            }.shuffled()
+
+        LogUtil.i("蓝牙连接-获取到钥匙信息:${keyList}")
         if (keyList.isEmpty()) {
             ToastUtils.tip(R.string.no_available_key)
             return null
         }
+        keyList.sortedBy { BusinessManager.getBleDeviceByMac(it.mac)?.token != null }
 
-        // —— 优先检查已经连接的 ——
-        val already = keyList.firstOrNull { kb ->
-            BleManager.getInstance().isConnected(kb.mac!!)  // mac 一定 non-null
-        }
-        if (already != null) {
-            val addr = keyDockList
-                .firstOrNull { dock ->
-                    dock.getKeyList().any { it.rfid == already.rfid }
-                }?.addr
-            if (addr != null) return addr to already
-        }
-
-        // —— 如果没有已连的,再顺序挂起尝试连接 ——
         for (kb in keyList) {
             val mac = kb.mac ?: continue
-            val found = suspendCoroutine<DockBean.KeyBean?> { cont ->
-                BusinessManager.registerConnectListener(mac) { isDone, _ ->
-                    if (isDone) cont.resume(kb)
+
+            val connected = BleConnectionManager.tryConnectWithOptionalCharge(mac)
+            if (connected) {
+                LogUtil.i("蓝牙连接完成 :${mac}")
+                // 找到第一个能连的:从 keyDockList 里拿同 rfid 的 addr
+                val addr =
+                    keyDockList.firstOrNull { it.getKeyList().any { it.rfid == kb.rfid } }?.addr
+                if (addr != null) {
+                    return addr to kb
                 }
             }
-            if (found != null) {
-                val addr = keyDockList
-                    .firstOrNull { it.getKeyList().any { it.rfid == found.rfid } }
-                    ?.addr
-                return if (addr != null) addr to found else null
-            }
         }
 
         // 一个都没成功
@@ -1002,6 +1051,7 @@ object ModBusController {
         return null
     }
 
+
     /**
      * 根据数量获取锁具(基于锁柜和便携柜不存在接一起的情况)
      *

+ 11 - 1
app/src/main/java/com/grkj/iscs/model/DictConstants.kt → app/src/main/java/com/grkj/iscs/model/DictAndSystemConstants.kt

@@ -3,7 +3,7 @@ package com.grkj.iscs.model
 /**
  * 字典参数
  */
-object DictConstants {
+object DictAndSystemConstants {
     /**
      * 仓位状态
      */
@@ -48,4 +48,14 @@ object DictConstants {
      * 锁仓类型
      */
     const val KEY_SLOT_TYPE = "slot_type"
+
+    /**
+     * 指纹数量限制
+     */
+    const val KEY_SYS_FINGERPRINT_LIMIT = "sys.fingerprint.limit"
+
+    /**
+     * 人脸数量限制
+     */
+    const val KEY_SYS_FACE_LIMIT = "sys.face.limit"
 }

+ 18 - 0
app/src/main/java/com/grkj/iscs/model/ISCSDomainData.kt

@@ -0,0 +1,18 @@
+package com.grkj.iscs.model
+
+import com.grkj.iscs.enums.DeviceInputTypeEnum
+
+/**
+ * 业务数据
+ */
+object ISCSDomainData {
+    /**
+     * 设备是否初始化完成
+     */
+    var deviceInputType: DeviceInputTypeEnum = DeviceInputTypeEnum.NONE
+
+    /**
+     * 是否是硬件录入
+     */
+    var isDeviceRegistration: Boolean = false
+}

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

@@ -5,9 +5,9 @@ object UrlConsts {
 //    const val BASE_URL = "http://192.168.28.97:9190"    // 车
 //    const val BASE_URL = "http://36.133.174.236:9190"    // 外
 //    const val BASE_URL = "http://192.168.0.10:9190"    // 外
-    const val BASE_URL = "http://192.168.1.121:9190"    // 外
+//    const val BASE_URL = "http://192.168.1.121:9190"    // 外
 
-    //    const val BASE_URL = "http://120.27.232.27:9190"    // 外
+    const val BASE_URL = "http://120.27.232.27:9190"    // 外
     const val WEB_SOCKET = "ws://192.168.1.127:9090/websocket/iot/127"
 
     const val AUTOCODE_TICKET_NUMBER = "JOB_TICKET_CODE"
@@ -332,4 +332,24 @@ object UrlConsts {
      * 获取锁定站列表
      */
     const val GET_IS_LOTO_STATION_PAGE = "/iscs/station/getIsLotoStationPage"
+
+    /**
+     * 机柜录入工卡(只要cardNfc参数)
+     */
+    const val INSERT_IS_JOB_CARD_BY_CABINET = "/iscs/card/insertIsJobCardByCabinet"
+
+    /**
+     * 机柜录入钥匙(需要参数keyNfc,macAddress)
+     */
+    const val INSERT_IS_KEY_BY_CABINET = "/iscs/key/insertIsKeyByCabinet"
+
+    /**
+     * 机柜录入挂锁(需要参数lockNfc)
+     */
+    const val INSERT_IS_LOCK_BY_CABINET = "/iscs/lock/insertIsLockByCabinet"
+
+    /**
+     * 机柜录入rfid标签(需要参数rfid)
+     */
+    const val INSERT_IS_RFID_BY_CABINET = "/iscs/token/insertIsRfidTokenByCabinet"
 }

+ 8 - 0
app/src/main/java/com/grkj/iscs/model/bo/DockStatusBO.kt

@@ -0,0 +1,8 @@
+package com.grkj.iscs.model.bo
+
+import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
+
+data class DockStatusBO(
+    val row: Int,
+    val dockList: MutableList<DockTestBean>
+)

+ 61 - 6
app/src/main/java/com/grkj/iscs/model/bo/WorkTicketGetBO.kt

@@ -1,14 +1,23 @@
 package com.grkj.iscs.model.bo
 
+import com.grkj.iscs.MyApplication
+import com.grkj.iscs.R
+import com.grkj.iscs.util.NetApi
+import com.grkj.iscs.util.log.LogUtil
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
+
 class WorkTicketGetBO {
     /**
      * 权限卡号
      */
     var cardNo: String? = null
+
     /**
      * 用户密码
      */
     var password: String? = null
+
     /**
      * 工作票数组
      */
@@ -19,10 +28,12 @@ class WorkTicketGetBO {
          * 工作票号
          */
         var taskCode: String? = null
+
         /**
          * 工作票ID
          */
         var taskId: String? = null
+
         /**
          * 工作票下挂任务列表
          */
@@ -33,23 +44,28 @@ class WorkTicketGetBO {
              * 任务ID
              */
             var dataId: Int? = null
+
             /**
              * 工作点位RFID号
              */
             var equipRfidNo: String? = null
+
             /**
              * 锁RFID号
              */
             var infoRfidNo: String? = null
+
             /**
              * 任务目标 0:挂锁 1:解锁
              */
             var target: Int? = null
+
             /**
              * 任务当前状态:
              * 0—挂锁;1—解锁;2-无操作
              */
             var status: Int? = null
+
             /**
              * 任务操作状态
              * 0—待完成;1—已完成
@@ -74,14 +90,53 @@ class WorkTicketGetBO {
     }
 
     // 判断是否有closed字段为0的
-    fun hasFinished(): Boolean {
-        data?.forEach {
-            it.dataList?.forEach {
-                if (it.closed == 0) {
-                    return false
+    suspend fun hasFinished(): Pair<Boolean, Boolean> {
+        // 如果没有数据,默认本地完成且远程未完成
+        if (data.isNullOrEmpty()) return true to false
+
+        var anyRemoteFinished = false
+        //点位的rfid是否被刷到锁的rfid
+        val pointRfidEqLockRfid = data?.any { dataDto ->
+            dataDto.dataList?.any { dataListDto ->
+                dataDto.dataList?.map { it.equipRfidNo }?.contains(dataListDto.infoRfidNo) == true
+            } == true
+        }
+        if (pointRfidEqLockRfid == true) {
+            return false to false
+        }
+        for (item in data) {
+            // 1. 调用回调接口获取远程的 ticketStatus,并根据情况设置两个标志
+            val (localTicketFinish, remoteTicketFinished) = suspendCoroutine<Pair<Boolean, Boolean>> { cont ->
+                NetApi.getTicketDetail(item.taskCode?.toLong() ?: 0) { res, code ->
+                    LogUtil.i("作业票状态: ${res?.ticketStatus}")
+                    if (res?.ticketStatus in listOf(
+                            "5",
+                            "6"
+                        ) || code == 500
+                    ) {
+                        // 只要远程状态是 5 或 6,就认为 remoteTicketFinished = true
+                        cont.resume(true to true)
+                    } else {
+                        // 否则,用本地 dataList 的所有 closed 字段判断 localTicketFinish,本地没完成就返回 false
+                        val allClosed = item.dataList?.all { it.closed == 1 } == true
+                        cont.resume(allClosed to false)
+                    }
                 }
             }
+
+            // 如果这一次远程已经为 true,就标记下来
+            if (remoteTicketFinished) {
+                anyRemoteFinished = true
+            }
+
+            // 如果本地没走完,就可以立刻返回:本地 false + 这时 anyRemoteFinished 的值
+            if (!localTicketFinish) {
+                return false to anyRemoteFinished
+            }
+            // 本地都走完才继续下一轮
         }
-        return true
+
+        // 如果所有项本地都完成,就返回 true;远程是否有一次命中,就看 anyRemoteFinished
+        return true to anyRemoteFinished
     }
 }

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

@@ -11,6 +11,7 @@ object MsgEventConstants {
     // ------------------------------ 作业票 1-003-000 ------------------------------
     const val MSG_EVENT_GET_TICKET_STATUS = 1_003_000   // 获取设备工作票完成情况
     const val MSG_EVENT_UPDATE_TICKET_PROGRESS = 1_003_001  // 更新作业票进度(页面刷新)
+    const val MSG_EVENT_TICKET_FINISHED = 1_003_002 //作业票已结束
 
     // ------------------------------ 蓝牙 1-004-000 ------------------------------
     const val MSG_EVENT_CURRENT_MODE = 1_004_000        // 当前模式

+ 22 - 6
app/src/main/java/com/grkj/iscs/util/ArcSoftUtil.kt

@@ -94,7 +94,12 @@ object ArcSoftUtil {
         }
     }
 
-    fun initCamera(context: Context, windowManager: WindowManager, preview: View, callBack: (Bitmap?) -> Unit) {
+    fun initCamera(
+        context: Context,
+        windowManager: WindowManager,
+        preview: View,
+        callBack: (Bitmap?, Int, Boolean) -> Unit
+    ) {
         val metrics = DisplayMetrics()
         windowManager.defaultDisplay.getMetrics(metrics)
 
@@ -119,7 +124,7 @@ object ArcSoftUtil {
                     FaceEngine.CP_PAF_NV21,
                     faceInfoList
                 )
-                if (code == ErrorInfo.MOK && faceInfoList.size > 0) {
+                if (code == ErrorInfo.MOK && faceInfoList.isNotEmpty()) {
                     code = faceEngine!!.process(
                         nv21,
                         previewSize!!.width,
@@ -146,16 +151,22 @@ object ArcSoftUtil {
 
                 // 有其中一个的错误码不为ErrorInfo.MOK,return
                 if ((ageCode or genderCode or face3DAngleCode or livenessCode) != ErrorInfo.MOK) {
+                    LogUtil.d("人脸检测结果:年龄、性别、角度、获取验证失败")
                     return
                 }
 
                 // 自己加的,必须有活体检测
                 if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
+                    callBack(null, faceInfoList.size, false)
                     return
                 }
-                val bitmap = NV21ToBitmap(context).nv21ToBitmap(nv21, previewSize!!.width, previewSize!!.height)
-                LogUtil.i("识别结果 : ${bitmap == null} - $faceInfoList")
-                callBack(bitmap)
+                val bitmap = NV21ToBitmap(context).nv21ToBitmap(
+                    nv21,
+                    previewSize!!.width,
+                    previewSize!!.height
+                )
+                LogUtil.d("人脸检测结果-识别结果 : ${bitmap == null} - $faceInfoList")
+                callBack(bitmap, faceInfoList.size, true)
             }
 
             override fun onCameraClosed() {
@@ -182,7 +193,12 @@ object ArcSoftUtil {
         cameraHelper!!.start()
     }
 
-    fun start(context: Context, windowManager: WindowManager, preview: View, callBack: (Bitmap?) -> Unit) {
+    fun start(
+        context: Context,
+        windowManager: WindowManager,
+        preview: View,
+        callBack: (Bitmap?, Int, Boolean) -> Unit
+    ) {
         initEngine(context)
         initCamera(context, windowManager, preview, callBack)
     }

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

@@ -26,7 +26,8 @@ object BitmapUtil {
 
     fun bitmapToFile(bitmap: Bitmap, fileName: String): File? {
         // 创建一个临时文件
-        val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
+        val storageDir =
+            Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
         if (!storageDir.exists()) {
             storageDir.mkdirs()
         }
@@ -51,7 +52,11 @@ object BitmapUtil {
         }
     }
 
-    fun bitmapToByteArray(bitmap: Bitmap, format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG, quality: Int = 100): ByteArray {
+    fun bitmapToByteArray(
+        bitmap: Bitmap,
+        format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
+        quality: Int = 100
+    ): ByteArray {
         val stream = ByteArrayOutputStream()
 
         // 将 Bitmap 压缩并写入到 ByteArrayOutputStream 中
@@ -69,6 +74,10 @@ object BitmapUtil {
         reqHeight: Int? = null,
         callback: (Bitmap?) -> Unit
     ) {
+        if (url?.isEmpty() == true) {
+            callback(null)
+            return
+        }
         try {
             val builder = Glide.with(ctx)
                 .asBitmap()

+ 14 - 0
app/src/main/java/com/grkj/iscs/util/MapViewExtension.kt

@@ -0,0 +1,14 @@
+package com.grkj.iscs.util
+
+import android.graphics.Bitmap
+import android.graphics.Color
+import com.onlylemi.mapview.library.MapView
+
+/**
+ * mapview释放已有的
+ */
+fun MapView?.release() {
+    this?.loadMap(Bitmap.createBitmap(1, 1, Bitmap.Config.RGB_565).apply {
+        eraseColor(Color.WHITE)
+    })
+}

+ 92 - 11
app/src/main/java/com/grkj/iscs/util/NetApi.kt

@@ -449,14 +449,14 @@ object NetApi {
     /**
      * 获取作业票和关联数据
      */
-    fun getTicketDetail(ticketId: Long, callBack: (TicketDetailRespVO?) -> Unit) {
+    fun getTicketDetail(ticketId: Long, callBack: (TicketDetailRespVO?, Int?) -> Unit) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.TICKET_EQUIP_DETAIL,
             false,
             mapOf("ticketId" to ticketId),
-            { res, _, _ ->
+            { res, msg, code ->
                 res?.let {
-                    callBack.invoke(getRefBean(it))
+                    callBack.invoke(getRefBean(it), code)
                 }
             }, isGet = true, isAuth = true
         )
@@ -493,7 +493,7 @@ object NetApi {
         keyNfc: String,
         serialNumber: String,
         retryCount: Int = 3,
-        callBack: (Boolean, String) -> Unit
+        callBack: (Boolean, String?, Int) -> Unit
     ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.KEY_RETURN_UPDATE,
@@ -503,9 +503,9 @@ object NetApi {
                 "keyNfc" to keyNfc,
                 "serialNumber" to serialNumber
             ),
-            { res, msg, _ ->
+            { res, msg, code ->
                 res?.let {
-                    callBack(true, msg ?: "")
+                    callBack(true, msg, code)
                 } ?: let {
                     if (retryCount > 0) {
                         Executor.delayOnIO(500) {
@@ -518,7 +518,7 @@ object NetApi {
                             )
                         }
                     } else {
-                        callBack.invoke(false, msg ?: "")
+                        callBack.invoke(false, msg, code)
                     }
                 }
                 // TODO isAuth需要配置
@@ -549,7 +549,7 @@ object NetApi {
     fun updateLockPointBatch(
         list: MutableList<LockPointUpdateReqVO>,
         retryCount: Int = 3,
-        callBack: (Boolean) -> Unit
+        callBack: (Boolean, String, Int) -> Unit
     ) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.LOCK_POINT_UPDATE_BATCH,
@@ -557,16 +557,16 @@ object NetApi {
             mapOf(
                 "list" to list
             ),
-            { res, _, _ ->
+            { res, msg, code ->
                 res?.let {
-                    callBack(true)
+                    callBack(true, msg.toString(), code)
                 } ?: let {
                     if (retryCount > 0) {
                         Executor.delayOnIO(500) {
                             updateLockPointBatch(list, retryCount - 1, callBack)
                         }
                     } else {
-                        callBack.invoke(false)
+                        callBack.invoke(false, msg.toString(), code)
                     }
                 }
             }, isGet = false, isAuth = true
@@ -1443,4 +1443,85 @@ object NetApi {
             }, isGet = true, isAuth = true
         )
     }
+
+    /**
+     * 设备录入工卡
+     */
+    fun deviceInputCard(deviceNfc: String, callBack: (Boolean) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.INSERT_IS_JOB_CARD_BY_CABINET,
+            false,
+            mapOf(
+                "cardNfc" to deviceNfc
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 设备录入RFID
+     */
+    fun deviceInputRFID(deviceNfc: String, callBack: (Boolean) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.INSERT_IS_RFID_BY_CABINET,
+            false,
+            mapOf(
+                "rfid" to deviceNfc
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 设备录入钥匙
+     */
+    fun deviceInputKey(keyNfc: String, keyMacAddress: String, callBack: (Boolean) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.INSERT_IS_KEY_BY_CABINET,
+            false,
+            mapOf(
+                "keyNfc" to keyNfc,
+                "macAddress" to keyMacAddress,
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
+
+    /**
+     * 设备录入挂锁
+     */
+    fun deviceInputLock(lockNfc: String, callBack: (Boolean) -> Unit) {
+        NetHttpManager.getInstance().doRequestNet(
+            UrlConsts.INSERT_IS_LOCK_BY_CABINET,
+            false,
+            mapOf(
+                "lockNfc" to lockNfc
+            ),
+            { res, _, _ ->
+                res?.let {
+                    callBack.invoke(true)
+                } ?: run {
+                    callBack.invoke(false)
+                }
+            }, isGet = false, isAuth = true
+        )
+    }
 }

+ 12 - 10
app/src/main/java/com/grkj/iscs/util/NetHttpManager.kt

@@ -18,7 +18,7 @@ import com.grkj.iscs.model.Constants.DEVICE_TYPE_NORMAL
 import com.grkj.iscs.model.Constants.DEVICE_TYPE_PORTABLE
 import com.grkj.iscs.model.Token
 import com.grkj.iscs.model.UrlConsts
-import com.grkj.iscs.model.UrlConsts.LOGIN_CARD
+import com.grkj.iscs.model.vo.BaseVO
 import com.grkj.iscs.model.vo.FileStreamReqParam
 import com.grkj.iscs.util.log.LogUtil
 import okhttp3.logging.HttpLoggingInterceptor
@@ -70,12 +70,14 @@ class NetHttpManager {
                 it.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
             }
             .addSerialPreprocessor { itPreChain ->
-                itPreChain.task.addHeader("Module", when (DEVICE_TYPE) {
-                    2 -> DEVICE_TYPE_MATERIAL
-                    3 -> DEVICE_TYPE_PORTABLE
-                    4 -> DEVICE_TYPE_HYBRID
-                    else -> DEVICE_TYPE_NORMAL
-                })
+                itPreChain.task.addHeader(
+                    "Module", when (DEVICE_TYPE) {
+                        2 -> DEVICE_TYPE_MATERIAL
+                        3 -> DEVICE_TYPE_PORTABLE
+                        4 -> DEVICE_TYPE_HYBRID
+                        else -> DEVICE_TYPE_NORMAL
+                    }
+                )
                 if (!itPreChain.task.isTagged(tagAuth)) {
                     itPreChain.proceed()
                     return@addSerialPreprocessor
@@ -158,11 +160,11 @@ class NetHttpManager {
                                 ToastUtils.tip(itMsg)
                                 BusinessManager.sendLoadingEventMsg(null, false)
                             }
-                            val bobyStr = it.body.toString()
+                            val bodyStr = it.body.toString()
                             callback(
-                                null, bobyStr.ifEmpty {
+                                null, baseVO?.msg ?: bodyStr.ifEmpty {
                                     it.toString()
-                                }, it.status
+                                }, baseVO?.code ?: it.status
                             )
                         }
                     } catch (e: Exception) {

+ 87 - 28
app/src/main/java/com/grkj/iscs/view/activity/HomeActivity.kt

@@ -22,11 +22,12 @@ import com.grkj.iscs.model.eventmsg.DeviceExceptionMsg
 import com.grkj.iscs.model.eventmsg.MsgEvent
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_DEVICE_EXCEPTION
 import com.grkj.iscs.model.vo.user.UserInfoRespVO
-import com.grkj.iscs.util.SPUtils
+import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.adapter.MenuAdapter
 import com.grkj.iscs.view.base.BaseFragment
 import com.grkj.iscs.view.base.BaseMvpActivity
+import com.grkj.iscs.view.fragment.DeviceRegistrationHomeFragment
 import com.grkj.iscs.view.fragment.DeviceStatusFragment
 import com.grkj.iscs.view.fragment.DockTestFragment
 import com.grkj.iscs.view.fragment.ExceptionReportFragment
@@ -44,7 +45,7 @@ import com.zhy.adapter.recyclerview.base.ViewHolder
  */
 class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBinding>() {
 
-    private var cardNo = ""
+    private var cardNo: String? = ""
     val cardNoLiveData = MutableLiveData("")
     private var mMenuList = mutableListOf<Menu>()
     private lateinit var observer: Observer<MsgEvent>
@@ -54,30 +55,77 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
 
     override fun initView() {
         presenter?.registerStatusListener()
+        presenter?.updateKeyData()
         presenter?.getAndSaveCabinetId()
         val userInfo = intent.getSerializableExtra("userInfo")
-        
+
         BusinessManager.isTestMode = false
         if (userInfo != null && (userInfo as UserInfoRespVO).roles != null) {
             if (userInfo.roles?.any { it == USER_ROLE_DRAWER || it == USER_ROLE_LOCKER || it == USER_ROLE_COLOCKER || it == USER_ROLE_GUARD } == true) {
-                mMenuList.addAll(mutableListOf(
-                    Menu(getString(R.string.job_management), R.mipmap.job_management, JobManagementFragment())
-                ))
+                mMenuList.addAll(
+                    mutableListOf(
+                        Menu(
+                            getString(R.string.job_management),
+                            R.mipmap.job_management,
+                            JobManagementFragment()
+                        )
+                    )
+                )
+            }
+            if (userInfo.roles?.any { it == USER_ROLE_ADMHDWTESTER } == true) {
+                BusinessManager.isTestMode = true
+                mMenuList.add(
+                    Menu(
+                        getString(R.string.test),
+                        R.mipmap.menu_icon_test,
+                        DockTestFragment()
+                    )
+                )
             }
             if (userInfo.roles?.any { it == USER_ROLE_ADMHDWTESTER } == true) {
                 BusinessManager.isTestMode = true
-                mMenuList.add(Menu(getString(R.string.test), R.mipmap.menu_icon_test, DockTestFragment()))
+                mMenuList.add(
+                    Menu(
+                        getString(R.string.device_registration),
+                        R.mipmap.menu_icon_device_registration,
+                        DeviceRegistrationHomeFragment()
+                    )
+                )
             }
             if (userInfo.roles?.any { it == USER_ROLE_ADMHDWSETTER } == true) {
-                mMenuList.add(Menu(getString(R.string.system_setting), R.mipmap.menu_icon_sys_setting, SystemSettingFragment()))
+                mMenuList.add(
+                    Menu(
+                        getString(R.string.system_setting),
+                        R.mipmap.menu_icon_sys_setting,
+                        SystemSettingFragment()
+                    )
+                )
             }
         }
 //        mMenuList.add(Menu(getString(R.string.test), R.mipmap.menu_icon_test, DockTestFragment()))
 //        mMenuList.add(Menu(getString(R.string.system_setting), R.mipmap.menu_icon_sys_setting, SystemSettingFragment()))
 
-        mMenuList.add(Menu(getString(R.string.device_status), R.mipmap.menu_icon_device_status, DeviceStatusFragment()))
-        mMenuList.add(Menu(getString(R.string.switch_status), R.mipmap.menu_icon_device_status, SwitchStatusFragment()))
-        mMenuList.add(Menu(getString(R.string.exception_report), R.mipmap.exception_handle, ExceptionReportFragment()))
+        mMenuList.add(
+            Menu(
+                getString(R.string.device_status),
+                R.mipmap.menu_icon_device_status,
+                DeviceStatusFragment()
+            )
+        )
+        mMenuList.add(
+            Menu(
+                getString(R.string.switch_status),
+                R.mipmap.menu_icon_device_status,
+                SwitchStatusFragment()
+            )
+        )
+        mMenuList.add(
+            Menu(
+                getString(R.string.exception_report),
+                R.mipmap.exception_handle,
+                ExceptionReportFragment()
+            )
+        )
         mMenuList.add(Menu(getString(R.string.settings), R.mipmap.settings, SettingFragment()))
 
         mBinding?.itemSetting?.ivIcon?.setImageResource(R.mipmap.settings)
@@ -95,22 +143,25 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
         mBinding?.vp?.offscreenPageLimit = mMenuList.size
         mBinding?.vp?.adapter = MenuAdapter(supportFragmentManager, lifecycle, mMenuList)
 
-        mBinding?.rvMenu?.adapter = object : CommonAdapter<Menu>(this, R.layout.item_rv_menu, mMenuList) {
-            override fun convert(holder: ViewHolder, data: Menu, position: Int) {
-                holder.setVisible(R.id.root, data.title != getString(R.string.settings))
-                holder.setText(R.id.tv_name, data.title)
-                holder.getView<ImageView>(R.id.iv_icon).setImageResource(data.icon!!)
-                holder.setOnClickListener(R.id.root) {
-                    mBinding?.itemSetting?.root?.setBackgroundColor(0)
-                    mBinding?.vp?.currentItem = position
-                    notifyDataSetChanged()
+        mBinding?.rvMenu?.adapter =
+            object : CommonAdapter<Menu>(this, R.layout.item_rv_menu, mMenuList) {
+                override fun convert(holder: ViewHolder, data: Menu, position: Int) {
+                    holder.setVisible(R.id.root, data.title != getString(R.string.settings))
+                    holder.setText(R.id.tv_name, data.title)
+                    holder.getView<ImageView>(R.id.iv_icon).setImageResource(data.icon!!)
+                    holder.setOnClickListener(R.id.root) {
+                        mBinding?.itemSetting?.root?.setBackgroundColor(0)
+                        mBinding?.vp?.currentItem = position
+                        notifyDataSetChanged()
+                    }
+                    holder.setBackgroundColor(
+                        R.id.root,
+                        if (position == mBinding?.vp?.currentItem)
+                            getColor(R.color.common_bg_white_30)
+                        else 0
+                    )
                 }
-                holder.setBackgroundColor(R.id.root,
-                    if (position == mBinding?.vp?.currentItem)
-                        getColor(R.color.common_bg_white_30)
-                    else 0)
             }
-        }
 
         observer = Observer { newData ->
             if (newData.code != MSG_EVENT_DEVICE_EXCEPTION) {
@@ -125,15 +176,23 @@ class HomeActivity : BaseMvpActivity<IHomeView, HomePresenter, ActivityHomeBindi
         }
 
         BusinessManager.mEventBus.observe(this, observer)
-        ModBusController.updateAllBuckleStatus{}
-        ModBusController.updateSwitchStatus{}
+        ModBusController.updateAllBuckleStatus {}
+        ModBusController.updateSwitchStatus {}
     }
 
     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
         if (event.action == KeyEvent.ACTION_UP && event.source == InputDevice.SOURCE_KEYBOARD) {
             // 检测到回车开始处理
             if (event.keyCode == 66) {
-                cardNo = cardNo.toLong().toByteArrays().toHexStrings(false)
+                cardNo = try {
+                    cardNo?.toLong()?.toByteArrays()?.toHexStrings(false)
+                } catch (e: Exception) {
+                    null
+                }
+                if (cardNo == null) {
+                    ToastUtils.tip(R.string.card_invalid)
+                    return  super.dispatchKeyEvent(event)
+                }
                 LogUtil.i("Swipe card home: $cardNo")
                 cardNoLiveData.postValue(cardNo)
                 // 重置cardNo

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

@@ -91,7 +91,6 @@ class LoginActivity : BaseMvpActivity<ILoginView, LoginPresenter, ActivityLoginB
             BusinessManager.connectDock(true)
         }
         presenter?.registerListener()
-
         FingerprintUtil.init(this)
         FingerprintUtil.start()
         FingerprintUtil.setScanListener(object : FingerprintUtil.OnScanListener {

+ 4 - 0
app/src/main/java/com/grkj/iscs/view/activity/test/ModbusActivity.kt

@@ -110,6 +110,10 @@ class ModbusActivity : BaseMvpActivity<IModbusView, ModBusPresenter, ActivityMod
                 LogUtil.i("开左钥匙卡扣 : ${it.toHexStrings()}")
                 LogUtil.i("____________________________________")
             }
+            ModBusController.controlKeyCharge(isOpen = true, true, mBinding?.slaveIdx?.text.toString().toByte()) {
+                LogUtil.i("左钥匙卡扣充电 : ${it.toHexStrings()}")
+                LogUtil.i("____________________________________")
+            }
         }
 
         mBinding?.closeLeftKeyBuckle?.setOnClickListener {

+ 83 - 0
app/src/main/java/com/grkj/iscs/view/base/BaseNavFragment.kt

@@ -0,0 +1,83 @@
+package com.grkj.iscs.view.base
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.LayoutRes
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import androidx.fragment.app.Fragment
+import androidx.navigation.NavController
+import androidx.navigation.fragment.findNavController
+import com.grkj.iscs.model.eventmsg.MsgEvent
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * BaseFragment: 支持 ViewBinding, EventBus, 权限管理, 串口 & 蓝牙, 以及 Navigation 切换
+ */
+abstract class BaseNavFragment<V : ViewDataBinding> : Fragment() {
+    protected val logger: Logger = LoggerFactory.getLogger(this::class.java)
+    protected lateinit var binding: V
+    private var isFirstLoad: Boolean = true
+    protected val navController: NavController get() = findNavController()
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        if (!::binding.isInitialized) {
+            binding = DataBindingUtil.inflate(inflater, getLayoutId(), container, false)
+            binding.lifecycleOwner = viewLifecycleOwner
+        }
+        return binding.root
+    }
+
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+        // EventBus 注册
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this)
+        }
+        if (isFirstLoad) {
+            initView()
+            initData()
+            initListeners()
+            initObservers()
+            isFirstLoad = false
+        }
+    }
+
+    override fun onDestroyView() {
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this)
+        }
+        super.onDestroyView()
+    }
+
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    open fun onEvent(event: MsgEvent) {
+
+    }
+
+    /** 获取布局资源 ID */
+    @LayoutRes
+    protected abstract fun getLayoutId(): Int
+
+    /** 初始化视图 */
+    protected abstract fun initView()
+
+    /** 可选:初始化数据 */
+    protected open fun initData() {}
+
+    /** 可选:初始化监听 */
+    protected open fun initListeners() {}
+
+    /** 可选:初始化 LiveData/事件观察 */
+    protected open fun initObservers() {}
+}

+ 12 - 0
app/src/main/java/com/grkj/iscs/view/base/BaseViewModel.kt

@@ -0,0 +1,12 @@
+package com.grkj.iscs.view.base
+
+import androidx.lifecycle.ViewModel
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * ViewModel基类
+ */
+abstract class BaseViewModel : ViewModel() {
+    protected val logger: Logger = LoggerFactory.getLogger(this::class.java)
+}

+ 18 - 1
app/src/main/java/com/grkj/iscs/view/dialog/FaceCaptureDialog.kt

@@ -2,10 +2,12 @@ package com.grkj.iscs.view.dialog
 
 import android.graphics.Bitmap
 import android.view.View
+import androidx.core.view.isVisible
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogFaceCaptureBinding
 import com.grkj.iscs.util.ArcSoftUtil
 import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BaseActivity
 import com.grkj.iscs.view.base.BaseDialog
 
@@ -56,7 +58,22 @@ class FaceCaptureDialog(val ctx: BaseActivity<*>, var callback: (Bitmap?) -> Uni
 
     private fun startFace() {
         ArcSoftUtil.initEngine(context)
-        ArcSoftUtil.initCamera(context, ctx.windowManager, mBinding?.preview!!) { bitmap ->
+        ArcSoftUtil.initCamera(
+            context,
+            ctx.windowManager,
+            mBinding?.preview!!
+        ) { bitmap, faceSize, alive ->
+            mBinding?.tipTv?.isVisible = faceSize > 1 || alive == false
+            LogUtil.i("人脸检测结果: ${bitmap == null},$faceSize,$alive")
+            if (faceSize > 1) {
+                mBinding?.tipTv?.text = context.getString(R.string.only_one_person_allowed)
+                return@initCamera
+            }
+            if (alive == false) {
+                mBinding?.tipTv?.text =
+                    context.getString(R.string.real_person_verification_required)
+                return@initCamera
+            }
             ArcSoftUtil.stop()
             mBinding?.preview?.visibility = View.INVISIBLE
             mBinding?.image?.visibility = View.VISIBLE

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

@@ -129,8 +129,20 @@ class LoginDialog(
 
     private fun startFace() {
         ArcSoftUtil.initEngine(context)
-        ArcSoftUtil.initCamera(context, ctx.windowManager, mBinding?.preview!!) {
-            it?.let { itBitmap ->
+        ArcSoftUtil.initCamera(
+            context,
+            ctx.windowManager,
+            mBinding?.preview!!
+        ) { bitmap, faceSize, alive ->
+            bitmap?.let { itBitmap ->
+                if (faceSize>1){
+                    ToastUtils.tip(R.string.only_one_person_allowed)
+                    return@initCamera
+                }
+                if (alive==false){
+                    ToastUtils.tip(R.string.real_person_verification_required)
+                    return@initCamera
+                }
                 BusinessManager.sendLoadingEventMsg(context.getString(R.string.face_detected_do_login))
                 ArcSoftUtil.stop()
                 presenter?.faceLogin(itBitmap) { isSuccess, userInfoRespVO ->

+ 21 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationHomeFragment.kt

@@ -0,0 +1,21 @@
+package com.grkj.iscs.view.fragment
+
+import com.grkj.iscs.databinding.FragmentDeviceRegistrationHomeBinding
+import com.grkj.iscs.view.base.BaseMvpFragment
+import com.grkj.iscs.view.iview.IDeviceRegistrationHomeView
+import com.grkj.iscs.view.presenter.DeviceRegistrationHomePresenter
+
+class DeviceRegistrationHomeFragment :
+    BaseMvpFragment<IDeviceRegistrationHomeView, DeviceRegistrationHomePresenter, FragmentDeviceRegistrationHomeBinding>() {
+
+    override val viewBinding: FragmentDeviceRegistrationHomeBinding
+        get() = FragmentDeviceRegistrationHomeBinding.inflate(layoutInflater)
+
+    override fun initPresenter(): DeviceRegistrationHomePresenter {
+        return DeviceRegistrationHomePresenter()
+    }
+
+    override fun initView() {
+
+    }
+}

+ 347 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationKeyAndLockFragment.kt

@@ -0,0 +1,347 @@
+package com.grkj.iscs.view.fragment
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.view.View
+import android.widget.TextView
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.RecyclerView
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.FragmentDeviceRegistrationKeyAndLockBinding
+import com.grkj.iscs.extentions.setSelected
+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.model.ISCSDomainData
+import com.grkj.iscs.model.bo.DockStatusBO
+import com.grkj.iscs.util.CommonUtils
+import com.grkj.iscs.util.SPUtils
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.base.BaseNavFragment
+import com.grkj.iscs.view.dialog.TipDialog
+import com.grkj.iscs.view.viewmodel.DeviceRegistrationKeyAndLockViewModel
+import com.sik.sikcore.extension.setDebouncedClickListener
+import com.sik.sikcore.thread.ThreadUtils
+import com.zhy.adapter.recyclerview.CommonAdapter
+import com.zhy.adapter.recyclerview.MultiItemTypeAdapter
+import com.zhy.adapter.recyclerview.base.ItemViewDelegate
+import com.zhy.adapter.recyclerview.base.ViewHolder
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
+
+/**
+ * 新硬件录入页
+ */
+class DeviceRegistrationKeyAndLockFragment :
+    BaseNavFragment<FragmentDeviceRegistrationKeyAndLockBinding>() {
+    private val viewModel: DeviceRegistrationKeyAndLockViewModel by lazy { ViewModelProvider(this)[DeviceRegistrationKeyAndLockViewModel::class] }
+    private val tipDialog: TipDialog by lazy { TipDialog(requireContext()) }
+    private var mRowList = mutableListOf<DockStatusBO>()
+    private var isAlreadyInput: Boolean = false
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_device_registration_key_and_lock
+    }
+
+    override fun initView() {
+        viewModel.loginUser = SPUtils.getLoginUser(requireContext())
+        //打开所有钥匙仓并关闭充电
+        viewModel.registerInitListener().observe(this) {
+            LogUtil.d("设备录入-初始化检测任务分发完成")
+        }
+        viewModel.initData(mRowList)
+        binding.cbBack.setDebouncedClickListener {
+            navController.popBackStack()
+        }
+        viewModel.isLoadComplete.observe(this) {
+            binding.scanTip.isVisible = !it
+            binding.rvDockLayout.isVisible = it
+            binding.cbRescanOrInput.isVisible = it
+            binding.scanResultTip.text = requireContext().getString(
+                R.string.device_registration_scan_result_tip,
+                viewModel.newHardwareKeySize,
+                viewModel.newHardwareLockSize
+            )
+        }
+        binding.cbRescanOrInput.setDebouncedClickListener {
+            if (isAlreadyInput) {
+                mRowList.clear()
+                viewModel.isStartCheckKey = false
+                viewModel.isLoadComplete.postValue(false)
+                viewModel.initData(mRowList)
+                isAlreadyInput = false
+                binding.cbRescanOrInput.setText(
+                    if (isAlreadyInput) CommonUtils.getStr(R.string.rescan)
+                        .toString() else CommonUtils.getStr(R.string.registration_to_system)
+                        .toString()
+                )
+            } else {
+                viewModel.deviceInputData(mRowList).observe(this) {
+                    if (it.first) {
+                        isAlreadyInput = true
+                        binding.cbRescanOrInput.setText(
+                            if (isAlreadyInput) CommonUtils.getStr(R.string.rescan)
+                                .toString() else CommonUtils.getStr(R.string.registration_to_system)
+                                .toString()
+                        )
+                        tipDialog.setType(TipDialog.TYPE_CONFIRM)
+                        tipDialog.setTip(
+                            requireContext().getString(
+                                R.string.already_registration_device_tip,
+                                it.second,
+                                it.third
+                            )
+                        )
+                        tipDialog.show()
+                    } else {
+                        tipDialog.setType(TipDialog.TYPE_ALL)
+                        tipDialog.setTip(
+                            requireContext().getString(R.string.registration_device_error)
+                        )
+                        tipDialog.showCancelCountdown(10)
+                    }
+                }
+
+            }
+        }
+        val adapter = MultiItemTypeAdapter(requireContext(), mRowList)
+        adapter.addItemViewDelegate(KeyDockItemDelegate(requireContext(), viewModel))
+        adapter.addItemViewDelegate(
+            LockDockItemDelegate(
+                requireContext(),
+                viewModel,
+                requireContext()
+            )
+        )
+        adapter.addItemViewDelegate(EmptyItemDelegate())
+        binding.rvDock?.adapter = adapter
+    }
+
+    override fun onResume() {
+        super.onResume()
+        fun refreshAdapter() {
+            binding.cbRescanOrInput.setText(
+                if (isAlreadyInput) CommonUtils.getStr(R.string.rescan)
+                    .toString() else CommonUtils.getStr(R.string.registration_to_system).toString()
+            )
+            ThreadUtils.runOnIO {
+                if (isResumed) {
+                    withContext(Dispatchers.Main) {
+                        binding.rvDock?.adapter?.notifyDataSetChanged()
+                    }
+                    delay(1000)
+                    withContext(Dispatchers.Main) {
+                        refreshAdapter()
+                    }
+                }
+            }
+
+        }
+        refreshAdapter()
+    }
+
+    override fun onDestroyView() {
+        ISCSDomainData.isDeviceRegistration = false
+        viewModel.unregisterInitListener()
+        viewModel.isDestroy = true
+        super.onDestroyView()
+    }
+
+    class KeyDockItemDelegate(
+        var context: Context,
+        var presenter: DeviceRegistrationKeyAndLockViewModel?
+    ) :
+        ItemViewDelegate<DockStatusBO> {
+        private val statusCloseTintColor =
+            ContextCompat.getColor(context, R.color.common_status_red)
+        private val statusOpenTintColor =
+            ContextCompat.getColor(context, R.color.common_status_green)
+
+        override fun getItemViewLayoutId(): Int {
+            return R.layout.item_rv_key_dock_device_registration
+        }
+
+        override fun convert(holder: ViewHolder?, row: DockStatusBO, position: Int) {
+            holder?.setVisibleWithHolder(R.id.ll_left, row.dockList.any { it.column == "1" })
+            holder?.setVisibleWithHolder(R.id.ll_right, row.dockList.any { it.column == "2" })
+            holder?.setSelected(
+                R.id.iv_key_1,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, true)
+            )
+            holder?.setSelected(
+                R.id.iv_key_2,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "1" }?.address, false)
+            )
+            holder?.setSelected(
+                R.id.iv_key_3,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, true)
+            )
+            holder?.setSelected(
+                R.id.iv_key_4,
+                ModBusController.isKeyExist(row.dockList.find { it.column == "2" }?.address, false)
+            )
+            holder?.getView<TextView>(R.id.tv_new_device_mac_1)?.apply {
+                val keyBean = ModBusController.getKeyByDock(
+                    row.dockList.find { it.column == "1" }?.address,
+                    true
+                )?.mac
+                isVisible = !keyBean.isNullOrEmpty()
+                text = keyBean
+            }
+            holder?.getView<TextView>(R.id.tv_new_device_mac_2)?.apply {
+                val keyBean = ModBusController.getKeyByDock(
+                    row.dockList.find { it.column == "1" }?.address,
+                    false
+                )?.mac
+                isVisible = !keyBean.isNullOrEmpty()
+                text = keyBean
+            }
+            holder?.getView<TextView>(R.id.tv_new_device_mac_3)?.apply {
+                val keyBean = ModBusController.getKeyByDock(
+                    row.dockList.find { it.column == "2" }?.address,
+                    true
+                )?.mac
+                isVisible = !keyBean.isNullOrEmpty()
+                text = keyBean
+            }
+            holder?.getView<TextView>(R.id.tv_new_device_mac_4)?.apply {
+                val keyBean = ModBusController.getKeyByDock(
+                    row.dockList.find { it.column == "2" }?.address,
+                    false
+                )?.mac
+                isVisible = !keyBean.isNullOrEmpty()
+                text = keyBean
+            }
+//            holder?.getView<View>(R.id.v_buckle_status_1)?.backgroundTintList =
+//                if (presenter?.getKeyBuckleLockEnabled(
+//                        row.dockList.find { it.column == "1" }?.address,
+//                        true
+//                    ) == true
+//                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+//                    statusOpenTintColor
+//                )
+//            holder?.getView<View>(R.id.v_buckle_status_2)?.backgroundTintList =
+//                if (presenter?.getKeyBuckleLockEnabled(
+//                        row.dockList.find { it.column == "1" }?.address,
+//                        false
+//                    ) == true
+//                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+//                    statusOpenTintColor
+//                )
+//            holder?.getView<View>(R.id.v_buckle_status_3)?.backgroundTintList =
+//                if (presenter?.getKeyBuckleLockEnabled(
+//                        row.dockList.find { it.column == "2" }?.address,
+//                        true
+//                    ) == true
+//                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+//                    statusOpenTintColor
+//                )
+//            holder?.getView<View>(R.id.v_buckle_status_4)?.backgroundTintList =
+//                if (presenter?.getKeyBuckleLockEnabled(
+//                        row.dockList.find { it.column == "2" }?.address,
+//                        false
+//                    ) == true
+//                ) ColorStateList.valueOf(statusCloseTintColor) else ColorStateList.valueOf(
+//                    statusOpenTintColor
+//                )
+            holder?.getView<TextView>(R.id.tv_new_device_1)?.isVisible =
+                ModBusController.isKeyNewHardware(
+                    row.dockList.find { it.column == "1" }?.address,
+                    true
+                )
+            holder?.getView<TextView>(R.id.tv_new_device_2)?.isVisible =
+                ModBusController.isKeyNewHardware(
+                    row.dockList.find { it.column == "1" }?.address,
+                    false
+                )
+            holder?.getView<TextView>(R.id.tv_new_device_3)?.isVisible =
+                ModBusController.isKeyNewHardware(
+                    row.dockList.find { it.column == "2" }?.address,
+                    true
+                )
+            holder?.getView<TextView>(R.id.tv_new_device_4)?.isVisible =
+                ModBusController.isKeyNewHardware(
+                    row.dockList.find { it.column == "2" }?.address,
+                    false
+                )
+        }
+
+        override fun isForViewType(item: DockStatusBO?, position: Int): Boolean {
+            return item?.dockList?.all { it.type == DOCK_TYPE_KEY } == true
+        }
+    }
+
+    class LockDockItemDelegate(
+        var context: Context,
+        var presenter: DeviceRegistrationKeyAndLockViewModel?,
+        var ctx: Context
+    ) : ItemViewDelegate<DockStatusBO> {
+        private val statusCloseTintColor =
+            ContextCompat.getColor(context, R.color.common_status_red)
+        private val statusOpenTintColor =
+            ContextCompat.getColor(context, R.color.common_status_green)
+        private val statusNotLightTintColor =
+            ContextCompat.getColor(context, R.color.common_status_not_light)
+
+        override fun getItemViewLayoutId(): Int {
+            return R.layout.item_rv_lock_dock_status
+        }
+
+        override fun convert(holder: ViewHolder?, row: DockStatusBO, position: Int) {
+            val rv = holder?.getView<RecyclerView>(R.id.rv_root)
+            rv?.adapter = object :
+                CommonAdapter<Int>(
+                    ctx,
+                    R.layout.item_rv_lock_dock_child_device_registration,
+                    row.dockList[0].deviceList
+                ) {
+                override fun convert(holder: ViewHolder?, lockIdx: Int, position: Int) {
+                    holder?.setSelected(
+                        R.id.root,
+                        ModBusController.isLockExist(row.dockList[0].address, lockIdx)
+                    )
+                    holder?.getView<TextView>(R.id.tv_new_device)?.isVisible =
+                        ModBusController.isLockNewHardware(
+                            row.dockList[0].address, lockIdx
+                        )
+                    ColorStateList.valueOf(statusNotLightTintColor).let {
+                        holder?.getView<View>(R.id.v_buckle_status_close)?.backgroundTintList = it
+                        holder?.getView<View>(R.id.v_buckle_status_open)?.backgroundTintList = it
+                    }
+//                    (presenter?.getLockBuckleLockEnabled(
+//                        row.dockList[0].address,
+//                        lockIdx
+//                    ) == true).let {
+//                        if (it) {
+//                            holder?.getView<View>(R.id.v_buckle_status_close)?.backgroundTintList =
+//                                ColorStateList.valueOf(statusCloseTintColor)
+//                        } else {
+//                            holder?.getView<View>(R.id.v_buckle_status_open)?.backgroundTintList =
+//                                ColorStateList.valueOf(statusOpenTintColor)
+//                        }
+//                    }
+                }
+            }
+        }
+
+        override fun isForViewType(item: DockStatusBO?, position: Int): Boolean {
+            return item?.dockList?.all { it.type == DOCK_TYPE_LOCK } == true
+        }
+    }
+
+    class EmptyItemDelegate : ItemViewDelegate<DockStatusBO> {
+        override fun getItemViewLayoutId(): Int {
+            return R.layout.item_rv_empty_dock_status
+        }
+
+        override fun convert(holder: ViewHolder?, row: DockStatusBO, position: Int) {
+        }
+
+        override fun isForViewType(item: DockStatusBO?, position: Int): Boolean {
+            return item?.dockList?.isEmpty() == true || item?.dockList?.none { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_LOCK } == true
+        }
+    }
+}

+ 86 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationScanFragment.kt

@@ -0,0 +1,86 @@
+package com.grkj.iscs.view.fragment
+
+import androidx.lifecycle.ViewModelProvider
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.FragmentDeviceRegistrationScanBinding
+import com.grkj.iscs.enums.DeviceInputTypeEnum.CARD
+import com.grkj.iscs.enums.DeviceInputTypeEnum.NONE
+import com.grkj.iscs.enums.DeviceInputTypeEnum.RFID
+import com.grkj.iscs.model.ISCSDomainData
+import com.grkj.iscs.util.CommonUtils
+import com.grkj.iscs.view.activity.HomeActivity
+import com.grkj.iscs.view.base.BaseNavFragment
+import com.grkj.iscs.view.dialog.TipDialog
+import com.grkj.iscs.view.viewmodel.DeviceRegistrationViewModel
+import com.sik.sikcore.extension.setDebouncedClickListener
+
+/**
+ * 设备录入扫描界面(卡片录入、RFID标签录入)
+ */
+class DeviceRegistrationScanFragment : BaseNavFragment<FragmentDeviceRegistrationScanBinding>() {
+    private val viewModel: DeviceRegistrationViewModel by lazy { ViewModelProvider(this)[DeviceRegistrationViewModel::class] }
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_device_registration_scan
+    }
+
+    override fun initView() {
+        binding.cbBack.setDebouncedClickListener {
+            navController.popBackStack()
+        }
+        when (ISCSDomainData.deviceInputType) {
+            CARD -> binding.inputTip.text =
+                CommonUtils.getStr(R.string.device_registration_scan_card_tip)
+
+            RFID -> binding.inputTip.text =
+                CommonUtils.getStr(R.string.device_registration_scan_rfid_tip)
+
+            NONE -> binding.inputTip.text = ""
+        }
+
+        (activity as HomeActivity).cardNoLiveData.observeForever { deviceNfc ->
+            if (isVisible && !deviceNfc.isNullOrEmpty()) {
+                val tipDialog = TipDialog(requireContext())
+                tipDialog.setTip(
+                    requireContext().getString(
+                        R.string.device_registration_recognize_tip,
+                        getTipTypeStr(), deviceNfc ?: ""
+                    )
+                )
+                tipDialog.setConfirmListener {
+                    viewModel.deviceInputScan(deviceNfc).observe(this) {
+                        if (it) {
+                            tipDialog.setTip(
+                                requireContext().getString(
+                                    R.string.device_registration_success_tip,
+                                    getTipTypeStr(), deviceNfc ?: ""
+                                )
+                            )
+                            tipDialog.setConfirmListener {}
+                            tipDialog.setType(TipDialog.TYPE_CONFIRM)
+                            tipDialog.show()
+                        } else {
+                            tipDialog.setTip(
+                                requireContext().getString(
+                                    R.string.device_registration_fail_tip,
+                                    getTipTypeStr()
+                                )
+                            )
+                            tipDialog.setConfirmListener {}
+                            tipDialog.setType(TipDialog.TYPE_CONFIRM)
+                            tipDialog.show()
+                        }
+                    }
+                }
+                tipDialog.showCancelCountdown(10)
+            }
+        }
+    }
+
+    private fun getTipTypeStr(): String {
+        return when (ISCSDomainData.deviceInputType) {
+            CARD -> requireContext().getString(R.string.card)
+            RFID -> requireContext().getString(R.string.rfid)
+            else -> ""
+        }
+    }
+}

+ 44 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DeviceRegistrationTypeSelectFragment.kt

@@ -0,0 +1,44 @@
+package com.grkj.iscs.view.fragment
+
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.FragmentDeviceRegistrationTypeSelectBinding
+import com.grkj.iscs.enums.DeviceInputTypeEnum
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.ISCSDomainData
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.activity.HomeActivity
+import com.grkj.iscs.view.base.BaseNavFragment
+import com.sik.sikcore.extension.setDebouncedClickListener
+
+/**
+ * 设备录入类型选择界面
+ */
+class DeviceRegistrationTypeSelectFragment :
+    BaseNavFragment<FragmentDeviceRegistrationTypeSelectBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.fragment_device_registration_type_select
+    }
+
+    override fun initView() {
+        binding.keyLockScanLayout.setDebouncedClickListener {
+            navController.navigate(R.id.action_deviceInputTypeSelectFragment_to_deviceInputKeyAndLockFragment)
+        }
+        binding.cardInputLayout.setDebouncedClickListener {
+            (activity as HomeActivity).cardNoLiveData.value = ""
+            ISCSDomainData.deviceInputType = DeviceInputTypeEnum.CARD
+            navController.navigate(R.id.action_deviceInputTypeSelectFragment_to_deviceInputScanFragment)
+        }
+        binding.rfidInputLayout.setDebouncedClickListener {
+            (activity as HomeActivity).cardNoLiveData.value = ""
+            ISCSDomainData.deviceInputType = DeviceInputTypeEnum.RFID
+            navController.navigate(R.id.action_deviceInputTypeSelectFragment_to_deviceInputScanFragment)
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        ISCSDomainData.isDeviceRegistration = true
+        LogUtil.d("设备录入-打开所有钥匙仓并关闭充电")
+        ModBusController.controlAllKeyBuckleOpen()
+    }
+}

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

@@ -6,21 +6,19 @@ import android.view.View
 import android.widget.ImageView
 import androidx.core.content.ContextCompat
 import androidx.recyclerview.widget.RecyclerView
-import com.grkj.iscs.MyApplication
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentDeviceStatusBinding
 import com.grkj.iscs.extentions.setSelected
 import com.grkj.iscs.extentions.setVisibleWithHolder
 import com.grkj.iscs.modbus.ModBusController
-import com.grkj.iscs.model.Constants.USER_ROLE_ADMHDWTESTER
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
+import com.grkj.iscs.model.bo.DockStatusBO
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.dialog.SlotExceptionDialog
 import com.grkj.iscs.view.dialog.TipDialog
-import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
 import com.grkj.iscs.view.iview.IDeviceStatusView
 import com.grkj.iscs.view.presenter.DeviceStatusPresenter
 import com.sik.sikcore.thread.ThreadUtils
@@ -120,7 +118,6 @@ class DeviceStatusFragment :
 
     override fun onResume() {
         super.onResume()
-
         fun refreshAdapter() {
             ThreadUtils.runOnIO {
                 if (isResumed) {
@@ -158,11 +155,6 @@ class DeviceStatusFragment :
         return DeviceStatusPresenter()
     }
 
-    data class DockStatusBO(
-        val row: Int,
-        val dockList: MutableList<DockTestBean>
-    )
-
     class KeyDockItemDelegate(var context: Context, var presenter: DeviceStatusPresenter?) :
         ItemViewDelegate<DockStatusBO> {
         private val statusCloseTintColor =

+ 9 - 0
app/src/main/java/com/grkj/iscs/view/fragment/DockTestFragment.kt

@@ -12,6 +12,7 @@ import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BaseFragment
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
@@ -70,9 +71,17 @@ class DockTestFragment : BaseFragment<FragmentDockTestBinding>() {
                         holder.setText(R.id.tv_name, getString(R.string.device_index, deviceIndex))
                         holder.setOnClickListener(R.id.tv_turn_on) {
                             ModBusController.controlKeyBuckle(true, deviceIndex == 1, dock.address)
+                            ModBusController.controlKeyCharge(
+                                false,
+                                deviceIndex == 1,
+                                dock.address
+                            ) {
+                                LogUtil.i("关闭充电:${it.toHexStrings()}")
+                            }
                         }
                         holder.setOnClickListener(R.id.tv_turn_off) {
                             ModBusController.controlKeyBuckle(false, deviceIndex == 1, dock.address)
+                            ModBusController.controlKeyCharge(true, deviceIndex == 1, dock.address)
                         }
                         holder.setOnClickListener(R.id.tv_read) {
                             ModBusController.readKeyRfid(

+ 7 - 2
app/src/main/java/com/grkj/iscs/view/fragment/FaceConfigFragment.kt

@@ -22,14 +22,15 @@ class FaceConfigFragment :
     private val mFaceList = mutableListOf<CharacteristicPageRespVO.Record>()
     private var mCaptureDialog: FaceCaptureDialog? = null
     private var mTipDialog: TipDialog? = null
+    private var mFaceLimit: Int = 5
 
     override val viewBinding: FragmentFaceConfigBinding
         get() = FragmentFaceConfigBinding.inflate(layoutInflater)
 
     override fun initView() {
         mBinding?.cbAddFace?.setOnClickListener {
-            if (mFaceList.size >= 1) {
-                ToastUtils.tip(R.string.face_config_tip)
+            if (mFaceList.size >= mFaceLimit) {
+                ToastUtils.tip(getString(R.string.face_config_tip, mFaceLimit))
                 return@setOnClickListener
             }
             showCaptureDialog()
@@ -53,6 +54,10 @@ class FaceConfigFragment :
         }
 
         refreshFaceList()
+        presenter?.getFaceLimit {
+            mFaceLimit = it
+            mBinding?.faceLimitTv?.text = getString(R.string.face_config_tip, mFaceLimit)
+        }
     }
 
     private fun refreshFaceList() {

+ 18 - 5
app/src/main/java/com/grkj/iscs/view/fragment/FingerprintConfigFragment.kt

@@ -20,15 +20,16 @@ class FingerprintConfigFragment :
 
     private val mFingerList = mutableListOf<CharacteristicPageRespVO.Record>()
     private var mTipDialog: TipDialog? = null
-    private var mFingerDialog: FingerScanDialog?  = null
+    private var mFingerDialog: FingerScanDialog? = null
+    private var mFingerprintLimit: Int = 5
 
     override val viewBinding: FragmentFingerprintConfigBinding
         get() = FragmentFingerprintConfigBinding.inflate(layoutInflater)
 
     override fun initView() {
         mBinding?.cbAddFinger?.setOnClickListener {
-            if (mFingerList.size >= 3) {
-                ToastUtils.tip(R.string.fingerprint_config_tip)
+            if (mFingerList.size >= mFingerprintLimit) {
+                ToastUtils.tip(getString(R.string.fingerprint_config_tip, mFingerprintLimit))
                 return@setOnClickListener
             }
             showFingerScanDialog()
@@ -39,7 +40,11 @@ class FingerprintConfigFragment :
             R.layout.item_rv_finger,
             mFingerList
         ) {
-            override fun convert(holder: ViewHolder, record: CharacteristicPageRespVO.Record, position: Int) {
+            override fun convert(
+                holder: ViewHolder,
+                record: CharacteristicPageRespVO.Record,
+                position: Int
+            ) {
                 holder.setText(R.id.tv_name, "${getString(R.string.fingerprint)}${position + 1}")
                 holder.setOnClickListener(R.id.root) {
                     showTipDialog(
@@ -53,6 +58,11 @@ class FingerprintConfigFragment :
         }
 
         refreshFingerList()
+        presenter?.getFingerprintLimit {
+            mFingerprintLimit = it
+            mBinding?.fingerprintLimitTv?.text =
+                getString(R.string.fingerprint_config_tip, mFingerprintLimit)
+        }
     }
 
     private fun refreshFingerList() {
@@ -90,7 +100,10 @@ class FingerprintConfigFragment :
                     presenter?.insertFinger(it) {
                         mFingerDialog?.dismiss()
                         if (it) {
-                            showTipDialog(getString(R.string.fingerprint_add_success_tip), isAdd = true)
+                            showTipDialog(
+                                getString(R.string.fingerprint_add_success_tip),
+                                isAdd = true
+                            )
                             refreshFingerList()
                         }
                     }

+ 5 - 2
app/src/main/java/com/grkj/iscs/view/fragment/JobExecutionFragment.kt

@@ -30,12 +30,15 @@ class JobExecutionFragment(val changePageCallback: (PageChangeBO) -> Unit) :
             Menu(fragment = WorkerFragment({ refreshPage(mPageChangeBO!!) }) {
                 changePage(it)
             }),
-            Menu(fragment = JobProgressFragment { refreshPage(mPageChangeBO!!) })
+            Menu(fragment = JobProgressFragment({ refreshPage(mPageChangeBO!!) }) {
+                changePage(it)
+            })
         )
 
         mBinding?.vp?.isUserInputEnabled = false
         mBinding?.vp?.offscreenPageLimit = mMenuList.size
-        mBinding?.vp?.adapter = MenuAdapter(requireActivity().supportFragmentManager, lifecycle, mMenuList)
+        mBinding?.vp?.adapter =
+            MenuAdapter(requireActivity().supportFragmentManager, lifecycle, mMenuList)
 
         (activity as HomeActivity).cardNoLiveData.observeForever {
             if (mBinding?.vp?.currentItem == 2) {

+ 28 - 13
app/src/main/java/com/grkj/iscs/view/fragment/JobProgressFragment.kt

@@ -10,10 +10,12 @@ import com.grkj.iscs.databinding.FragmentJobProgressBinding
 import com.grkj.iscs.extentions.debounce
 import com.grkj.iscs.model.bo.PageChangeBO
 import com.grkj.iscs.model.eventmsg.MsgEvent
+import com.grkj.iscs.model.eventmsg.MsgEventConstants
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_UPDATE_TICKET_PROGRESS
 import com.grkj.iscs.model.eventmsg.UpdateTicketProgressMsg
 import com.grkj.iscs.model.vo.ticket.TicketDetailMonitorRespVO
 import com.grkj.iscs.model.vo.ticket.TicketDetailRespVO
+import com.grkj.iscs.util.CommonUtils
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
@@ -27,7 +29,7 @@ import com.zhy.adapter.recyclerview.base.ViewHolder
 /**
  * 作业票工作进度页
  */
-class JobProgressFragment(val goBack: () -> Unit) :
+class JobProgressFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Unit) :
     BaseMvpFragment<IJobProgressView, JobProgressPresenter, FragmentJobProgressBinding>() {
 
     private val mPointList = mutableListOf<TicketDetailMonitorRespVO.IsJobTicketPointsVO>()
@@ -179,18 +181,31 @@ class JobProgressFragment(val goBack: () -> Unit) :
         }
 
         observer = Observer { newData ->
-            if (newData.code != MSG_EVENT_UPDATE_TICKET_PROGRESS) {
-                return@Observer
-            }
-            LogUtil.i("Update progress msg, isVisible : $isVisible")
-            if (!isVisible) {
-                return@Observer
-            }
-            val ticketId = (newData.data as UpdateTicketProgressMsg).ticketId
-            LogUtil.i("Update progress msg, ticketId : $ticketId")
-            if (ticketId == mPageChangeBO?.ticketId) {
-                mPageChangeBO?.let {
-                    refreshPage(it)
+            when (newData.code) {
+                MSG_EVENT_UPDATE_TICKET_PROGRESS -> {
+                    LogUtil.i("Update progress msg, isVisible : $isVisible")
+                    if (!isVisible) {
+                        return@Observer
+                    }
+                    val ticketId = (newData.data as UpdateTicketProgressMsg).ticketId
+                    LogUtil.i("Update progress msg, ticketId : $ticketId")
+                    if (ticketId == mPageChangeBO?.ticketId) {
+                        mPageChangeBO?.let {
+                            refreshPage(it)
+                        }
+                    }
+                }
+
+                MsgEventConstants.MSG_EVENT_TICKET_FINISHED -> {
+                    if (mTipDialog == null) {
+                        mTipDialog = TipDialog(requireContext())
+                    }
+                    mTipDialog?.setType(TipDialog.TYPE_HINT)
+                    mTipDialog?.setTip(CommonUtils.getStr(R.string.job_already_finished).toString())
+                    mTipDialog?.setConfirmListener {
+                        changePage(PageChangeBO(-1))
+                    }
+                    mTipDialog?.show()
                 }
             }
         }

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

@@ -1,6 +1,8 @@
 package com.grkj.iscs.view.fragment
 
 import android.graphics.PointF
+import android.view.GestureDetector
+import android.view.MotionEvent
 import android.view.View
 import android.widget.ImageView
 import androidx.lifecycle.Observer
@@ -21,12 +23,14 @@ import com.grkj.iscs.util.BitmapUtil
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.util.release
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.dialog.TipDialog
 import com.grkj.iscs.view.iview.IStepView
 import com.grkj.iscs.view.presenter.StepPresenter
 import com.grkj.iscs.view.widget.CustomStationLayer
 import com.onlylemi.mapview.library.MapViewListener
+import com.sik.sikcore.thread.ThreadUtils
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
 
@@ -49,6 +53,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
     private lateinit var observer: Observer<MsgEvent>
     private var mCanHandle: Boolean? = null // 是否可以操作,创建人、上锁人至少符合一个才可操作
     private var mapRatio: Float = 1f
+    private lateinit var gestureDetector: GestureDetector
 
     override val viewBinding: FragmentStepBinding
         get() = FragmentStepBinding.inflate(layoutInflater)
@@ -106,7 +111,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             }
 
         mBinding?.cbBack?.setOnClickListener { goBack() }
-
+        initMap()
         mBinding?.cbAction?.setOnClickListener {
             mCanHandle ?: return@setOnClickListener
             if (mCanHandle == false) {
@@ -152,7 +157,6 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                 )
             }
         }
-        initMap()
 
         observer = Observer { newData ->
             if (newData.code != MSG_EVENT_UPDATE_TICKET_PROGRESS) {
@@ -168,8 +172,37 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
         }
     }
 
+    override fun onResume() {
+        super.onResume()
+        mChangePage?.let {
+            refreshPage(it)
+        }
+    }
+
+
     private fun initMap() {
+        gestureDetector = GestureDetector(requireContext(), object :
+            GestureDetector.SimpleOnGestureListener() {
+            override fun onDoubleTap(e: MotionEvent): Boolean {
+                mBinding?.mapview?.currentRotateDegrees = 0f
+                return super.onDoubleTap(e)
+            }
+
+            override fun onFling(
+                e1: MotionEvent?,
+                e2: MotionEvent,
+                velocityX: Float,
+                velocityY: Float
+            ): Boolean {
+                mBinding?.mapview?.currentRotateDegrees = 0f
+                return super.onFling(e1, e2, velocityX, velocityY)
+            }
+        })
         mBinding?.mapview?.isScaleAndRotateTogether = false
+        mBinding?.mapview?.setOnTouchListener { _, event ->
+            gestureDetector.onTouchEvent(event)
+            false
+        }
         mBinding?.mapview?.setMapViewListener(object : MapViewListener {
             override fun onMapLoadSuccess() {
                 if (stationLayer != null) {
@@ -185,6 +218,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
                 })
                 mBinding?.mapview?.addLayer(stationLayer)
                 stationLayer?.setRatio(mapRatio)
+                mBinding?.mapview?.setMinZoom(mBinding?.mapview?.currentZoom ?: 0f)
             }
 
             override fun onMapLoadFail() {
@@ -234,6 +268,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             } else {
                 mBinding?.cbAction?.visibility = View.GONE
             }
+            presenter?.preOpenKeyCharge(requireContext(), mStep)
             handleBottomTip()
         }
 
@@ -317,6 +352,9 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
 
                                     // 全部点都加载完后,设置给 layer 并绘制
                                     if (mStationList.size == itMapInfo.pointList.size) {
+                                        if (stationLayer?.inDraw == true) {
+                                            return@loadBitmapFromUrl
+                                        }
                                         mBinding?.mapview?.loadMap(mapBmp)
                                     }
                                 }
@@ -514,6 +552,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
 
     override fun onPause() {
         super.onPause()
+//        mBinding?.mapview?.release()
         mStep = 0
     }
 

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

@@ -1,15 +1,19 @@
 package com.grkj.iscs.view.fragment
 
 import android.graphics.PointF
+import android.os.Bundle
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.View
 import com.grkj.iscs.BusinessManager
 import com.grkj.iscs.databinding.FragmentSwitchStatusBinding
 import com.grkj.iscs.util.BitmapUtil
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.util.release
 import com.grkj.iscs.view.base.BaseMvpFragment
 import com.grkj.iscs.view.iview.ISwitchStatusView
 import com.grkj.iscs.view.presenter.SwitchStatusPresenter
-import com.grkj.iscs.view.widget.CustomStationLayer
 import com.grkj.iscs.view.widget.CustomSwitchStationLayer
 import com.onlylemi.mapview.library.MapViewListener
 import com.sik.sikcore.thread.ThreadUtils
@@ -19,6 +23,8 @@ class SwitchStatusFragment :
     private var mapRatio: Float = 1f
     private var stationLayer: CustomSwitchStationLayer? = null
     private val mStationList = mutableListOf<CustomSwitchStationLayer.IsolationPoint>()
+    private lateinit var gestureDetector: GestureDetector
+
     override fun initPresenter(): SwitchStatusPresenter {
         return SwitchStatusPresenter()
     }
@@ -27,6 +33,23 @@ class SwitchStatusFragment :
         get() = FragmentSwitchStatusBinding.inflate(layoutInflater)
 
     override fun initView() {
+        gestureDetector = GestureDetector(requireContext(), object :
+            GestureDetector.SimpleOnGestureListener() {
+            override fun onDoubleTap(e: MotionEvent): Boolean {
+                mBinding?.mapview?.currentRotateDegrees = 0f
+                return super.onDoubleTap(e)
+            }
+
+            override fun onFling(
+                e1: MotionEvent?,
+                e2: MotionEvent,
+                velocityX: Float,
+                velocityY: Float
+            ): Boolean {
+                mBinding?.mapview?.currentRotateDegrees = 0f
+                return super.onFling(e1, e2, velocityX, velocityY)
+            }
+        })
         initMap()
     }
 
@@ -48,6 +71,11 @@ class SwitchStatusFragment :
         refreshSwitchStatus()
     }
 
+    override fun onPause() {
+        super.onPause()
+//        mBinding?.mapview?.release()
+    }
+
     private fun getMap(mapId: String) {
         presenter?.getMapInfo(mapId.toLong()) { itMapInfo ->
             // 如果没有图 URL,直接返回
@@ -103,6 +131,9 @@ class SwitchStatusFragment :
 
                     // 全部点都加载完后,设置给 layer 并绘制
                     if (mStationList.size == itMapInfo.pointList.count { it.x != null && it.y != null }) {
+                        if (stationLayer?.inDraw == true) {
+                            return@loadBitmapFromUrl
+                        }
                         mBinding?.mapview?.loadMap(mapBmp)
                     }
                 }
@@ -115,6 +146,10 @@ class SwitchStatusFragment :
      */
     private fun initMap() {
         mBinding?.mapview?.isScaleAndRotateTogether = false
+        mBinding?.mapview?.setOnTouchListener { _, event ->
+            gestureDetector.onTouchEvent(event)
+            false
+        }
         mBinding?.mapview?.setMapViewListener(object : MapViewListener {
             override fun onMapLoadSuccess() {
                 if (stationLayer != null) {
@@ -126,6 +161,7 @@ class SwitchStatusFragment :
                 stationLayer?.setRatio(mapRatio)
                 stationLayer?.stopAnimation()
                 stationLayer?.startAnimation()
+                mBinding?.mapview?.refresh()
             }
 
             override fun onMapLoadFail() {

+ 2 - 0
app/src/main/java/com/grkj/iscs/view/fragment/WorkerFragment.kt

@@ -53,6 +53,8 @@ class WorkerFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> U
             if (isColockerChanged) {
                 presenter?.checkColockerChangedStepJump(requireContext(), mStepDetailList, mStep) {
                     presenter?.updateTicketUser(mPageChangeBO?.ticketId!!, mSelectedList) {
+                        //选择人员之后提前打开充电,加速连接
+                        presenter?.preOpenKeyCharge()
                         if (it) {
                             goBack()
                         }

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

@@ -44,11 +44,16 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
             JobStatistics(Constants.SOP_CLEAN, 0),
             JobStatistics(Constants.SOP_SWITCH_PRODUCT, 0)
         )
-        
-        mBinding?.rvStatistics?.adapter = object : CommonAdapter<JobStatistics>(requireActivity(), R.layout.item_rv_job_management, jobStatisticList) {
+
+        mBinding?.rvStatistics?.adapter = object : CommonAdapter<JobStatistics>(
+            requireActivity(),
+            R.layout.item_rv_job_management,
+            jobStatisticList
+        ) {
             override fun convert(holder: ViewHolder, statistic: JobStatistics, position: Int) {
                 holder.getView<ImageView>(R.id.iv).setImageResource(statistic.sopType.icon)
-                holder.setText(R.id.tv_name,
+                holder.setText(
+                    R.id.tv_name,
                     if (statistic.count == 0) statistic.sopType.title
                     else "${statistic.sopType.title}(${statistic.count})"
                 )
@@ -61,49 +66,84 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
 
     fun refreshPage() {
         presenter?.getMapInfo {
+            LogUtil.i("开始加载地图工作区域:${it?.imageUrl}")
             BitmapUtil.loadBitmapFromUrl(requireContext(), it?.imageUrl!!) {
                 if (it == null) {
                     LogUtil.e("Map pic is null")
                     return@loadBitmapFromUrl
                 }
                 mMapPicWidth = it.width
+                LogUtil.i("地图大小:${it.width}")
                 mBinding?.mapview?.loadMap(it)
-                
+
                 presenter?.getMapPointPage {
                     mPointList.clear()
                     it?.records?.forEach { itPoint ->
-                        mPointList.add(CustomPoint(PointF(itPoint.x!!.toFloat() + 100, itPoint.y!!.toFloat()), itPoint.entityName!!, itPoint.entityId!!, mutableListOf()))
+                        mPointList.add(
+                            CustomPoint(
+                                PointF(
+                                    itPoint.x!!.toFloat() + 100,
+                                    itPoint.y!!.toFloat()
+                                ), itPoint.entityName!!, itPoint.entityId!!, mutableListOf()
+                            )
+                        )
                     }
                     presenter?.getWorkstationTicketList {
                         if (it == null) {
                             return@getWorkstationTicketList
                         }
                         jobStatisticList.forEach { itJob ->
-                            itJob.count = it.count { it.ticketType == itJob.sopType.type.toString() }
+                            itJob.count =
+                                it.count { it.ticketType == itJob.sopType.type.toString() }
                         }
                         mPointList.forEach { itPoint ->
-                            itPoint.ticketList = it.filter { it.workstationId == itPoint.workstationId }.toMutableList()
+                            itPoint.ticketList =
+                                it.filter { it.workstationId == itPoint.workstationId }
+                                    .toMutableList()
                             itPoint.ticketList.take(4).forEachIndexed { index, itTicket ->
                                 if (itPoint.ticketList.size > 3 && index == 3) {
                                     BitmapUtil.loadBitmapFromUrl(
                                         requireContext(),
-                                        SPUtils.getAttributeValue(requireContext(), Constants.getTicketKey(5)),
+                                        SPUtils.getAttributeValue(
+                                            requireContext(),
+                                            Constants.getTicketKey(5)
+                                        ),
                                         R.mipmap.ticket_type_placeholder,
                                         60, 60
                                     ) { itBitmap ->
-                                        itTicket.bitmap = itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(requireContext(), R.mipmap.ticket_type_placeholder, 60, 60)
+                                        itTicket.bitmap =
+                                            itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(
+                                                requireContext(),
+                                                R.mipmap.ticket_type_placeholder,
+                                                60,
+                                                60
+                                            )
                                     }
                                 } else {
                                     if (itTicket.ticketType == null) {
-                                        itTicket.bitmap = BitmapUtil.getResizedBitmapFromMipmap(requireContext(), R.mipmap.ticket_type_placeholder, 60, 60)
+                                        itTicket.bitmap = BitmapUtil.getResizedBitmapFromMipmap(
+                                            requireContext(),
+                                            R.mipmap.ticket_type_placeholder,
+                                            60,
+                                            60
+                                        )
                                     } else {
                                         BitmapUtil.loadBitmapFromUrl(
                                             requireContext(),
-                                            SPUtils.getAttributeValue(requireContext(), Constants.getTicketKey(itTicket.ticketType.toInt())),
+                                            SPUtils.getAttributeValue(
+                                                requireContext(),
+                                                Constants.getTicketKey(itTicket.ticketType.toInt())
+                                            ),
                                             R.mipmap.ticket_type_placeholder,
                                             60, 60
                                         ) { itBitmap ->
-                                            itTicket.bitmap = itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(requireContext(), R.mipmap.ticket_type_placeholder, 60, 60)
+                                            itTicket.bitmap =
+                                                itBitmap ?: BitmapUtil.getResizedBitmapFromMipmap(
+                                                    requireContext(),
+                                                    R.mipmap.ticket_type_placeholder,
+                                                    60,
+                                                    60
+                                                )
                                         }
                                     }
                                 }
@@ -112,7 +152,8 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
                         mBinding?.rvStatistics?.adapter?.notifyDataSetChanged()
 
                         Executor.repeatOnMain({
-                            val isAllBitmapLoaded = mPointList.all { it.ticketList.take(4).all { it.bitmap != null } }
+                            val isAllBitmapLoaded =
+                                mPointList.all { it.ticketList.take(4).all { it.bitmap != null } }
                             if (isAllBitmapLoaded) {
                                 mBinding?.mapview?.refresh()
                             }
@@ -188,5 +229,10 @@ class WorkshopFragment(val changePage: (PageChangeBO) -> Unit) :
 
     data class JobStatistics(var sopType: Constants.SopType, var count: Int)
 
-    data class CustomPoint(val pos: PointF, val name: String, val workstationId: Long, var ticketList: MutableList<WorkstationTicketListRespVO>)
+    data class CustomPoint(
+        val pos: PointF,
+        val name: String,
+        val workstationId: Long,
+        var ticketList: MutableList<WorkstationTicketListRespVO>
+    )
 }

+ 9 - 0
app/src/main/java/com/grkj/iscs/view/iview/IDeviceRegistrationHomeView.kt

@@ -0,0 +1,9 @@
+package com.grkj.iscs.view.iview
+
+import com.grkj.iscs.view.base.IView
+
+/**
+ * 设备录入界面接口
+ */
+interface IDeviceRegistrationHomeView : IView {
+}

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

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

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

@@ -11,19 +11,18 @@ import com.grkj.iscs.extentions.removeLeadingZeros
 import com.grkj.iscs.extentions.toHexStrings
 import com.grkj.iscs.modbus.ModBusController
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_LOCK
-import com.grkj.iscs.model.DictConstants
+import com.grkj.iscs.model.DictAndSystemConstants
+import com.grkj.iscs.model.bo.DockStatusBO
 import com.grkj.iscs.model.bo.LoginUserBO
 import com.grkj.iscs.model.vo.dict.CommonDictRespVO
 import com.grkj.iscs.model.vo.hardware.CabinetSlotsRecord
 import com.grkj.iscs.model.vo.hardware.SlotExDTO
-import com.grkj.iscs.model.vo.user.UserInfoRespVO
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.SPUtils
 import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BasePresenter
-import com.grkj.iscs.view.fragment.DeviceStatusFragment.DockStatusBO
 import com.grkj.iscs.view.fragment.DockTestFragment.DockTestBean
 import com.grkj.iscs.view.iview.IDeviceStatusView
 import com.sik.sikcore.thread.ThreadUtils
@@ -221,8 +220,8 @@ class DeviceStatusPresenter : BasePresenter<IDeviceStatusView>() {
      */
     fun getSlotData(done: () -> Unit) {
         ThreadUtils.runOnIO {
-            slotStatus = NetApi.getDictData(DictConstants.KEY_SLOT_STATUS)
-            slotType = NetApi.getDictData(DictConstants.KEY_SLOT_TYPE)
+            slotStatus = NetApi.getDictData(DictAndSystemConstants.KEY_SLOT_STATUS)
+            slotType = NetApi.getDictData(DictAndSystemConstants.KEY_SLOT_TYPE)
             done()
         }
     }

+ 12 - 0
app/src/main/java/com/grkj/iscs/view/presenter/FaceConfigPresetner.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs.view.presenter
 
 import android.graphics.Bitmap
 import com.grkj.iscs.MyApplication
+import com.grkj.iscs.model.DictAndSystemConstants
 import com.grkj.iscs.model.vo.FileStreamReqParam
 import com.grkj.iscs.model.vo.characteristic.CharacteristicPageRespVO
 import com.grkj.iscs.util.BitmapUtil
@@ -41,4 +42,15 @@ class FaceConfigPresenter : BasePresenter<IFaceConfigView>() {
             }
         }
     }
+
+    /**
+     * 获取人脸数量限制
+     */
+    fun getFaceLimit(callBack: (Int) -> Unit) {
+        NetApi.getIsSystemAttributeByKey(DictAndSystemConstants.KEY_SYS_FACE_LIMIT) {
+            Executor.runOnMain {
+                callBack(it?.sysAttrValue?.toInt() ?: 5)
+            }
+        }
+    }
 }

+ 12 - 0
app/src/main/java/com/grkj/iscs/view/presenter/FingerprintConfigPresenter.kt

@@ -2,6 +2,7 @@ package com.grkj.iscs.view.presenter
 
 import android.graphics.Bitmap
 import com.grkj.iscs.MyApplication
+import com.grkj.iscs.model.DictAndSystemConstants
 import com.grkj.iscs.model.vo.FileStreamReqParam
 import com.grkj.iscs.model.vo.characteristic.CharacteristicPageRespVO
 import com.grkj.iscs.util.BitmapUtil
@@ -41,4 +42,15 @@ class FingerprintConfigPresenter : BasePresenter<IFingerprintConfigView>() {
             }
         }
     }
+
+    /**
+     * 获取指纹数量限制
+     */
+    fun getFingerprintLimit(callBack: (Int) -> Unit) {
+        NetApi.getIsSystemAttributeByKey(DictAndSystemConstants.KEY_SYS_FINGERPRINT_LIMIT) {
+            Executor.runOnMain {
+                callBack(it?.sysAttrValue?.toInt() ?: 5)
+            }
+        }
+    }
 }

+ 69 - 16
app/src/main/java/com/grkj/iscs/view/presenter/HomePresenter.kt

@@ -3,19 +3,28 @@ 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.ble.BleConnectionManager
 import com.grkj.iscs.extentions.serialNo
+import com.grkj.iscs.modbus.DockBean
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.DeviceConst
 import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
+import com.grkj.iscs.model.ISCSDomainData
 import com.grkj.iscs.util.Executor
 import com.grkj.iscs.util.NetApi
 import com.grkj.iscs.util.SPUtils
+import com.grkj.iscs.util.ToastUtils
+import com.grkj.iscs.util.log.LogUtil
 import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.iview.IHomeView
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.delay
 
 class HomePresenter : BasePresenter<IHomeView>() {
 
     fun registerStatusListener() {
         BusinessManager.registerStatusListener(this) { dockBean ->
-            if (!BusinessManager.CAN_RETURN) {
+            if (!BusinessManager.CAN_RETURN || ISCSDomainData.isDeviceRegistration) {
                 return@registerStatusListener
             }
             when (dockBean.type) {
@@ -23,25 +32,36 @@ class HomePresenter : BasePresenter<IHomeView>() {
                     dockBean.getKeyList().forEach { keyBean ->
                         if (keyBean.isExist) {
                             Executor.repeatOnMain({
-                                keyBean.mac?.let {
+                                keyBean.mac?.let { mac ->
                                     BusinessManager.sendLoadingEventMsg(mContext?.getString(R.string.loading_msg_return_key_start))
-                                    BusinessManager.registerConnectListener(
-                                        it
-                                    ) { isDone, bleBean ->
-                                        if (isDone && bleBean != null) {
-                                            Executor.delayOnMain(300) {
-                                                BusinessManager.sendLoadingEventMsg(
-                                                    mContext?.getString(
-                                                        R.string.loading_msg_get_ticket_status_start
-                                                    )
+                                    ThreadUtils.runOnIO {
+                                        suspend fun readJobTicket(mac: String) {
+                                            val isConnect =
+                                                BleConnectionManager.tryConnectWithOptionalCharge(
+                                                    mac
                                                 )
-                                                BusinessManager.getCurrentStatus(
-                                                    4,
-                                                    bleBean.bleDevice
-                                                )
-                                                BusinessManager.getBatteryPower(bleBean.bleDevice)
+                                            if (isConnect) {
+                                                val bleBean = BusinessManager.getBleDeviceByMac(mac)
+                                                Executor.delayOnMain(300) {
+                                                    bleBean?.let {
+                                                        BusinessManager.sendLoadingEventMsg(
+                                                            mContext?.getString(
+                                                                R.string.loading_msg_get_ticket_status_start
+                                                            )
+                                                        )
+                                                        BusinessManager.getCurrentStatus(
+                                                            4,
+                                                            it.bleDevice
+                                                        )
+                                                        BusinessManager.getBatteryPower(it.bleDevice)
+                                                    }
+                                                }
+                                            } else {
+                                                delay(500)
+                                                readJobTicket(mac)
                                             }
                                         }
+                                        readJobTicket(mac)
                                     }
                                 }
                                 return@repeatOnMain keyBean.mac == null
@@ -66,4 +86,37 @@ class HomePresenter : BasePresenter<IHomeView>() {
                 ?.let { SPUtils.saveCabinetId(it.cabinetId ?: "") }
         }
     }
+
+    /**
+     * 更新钥匙数据
+     */
+    fun updateKeyData() {
+        ThreadUtils.runOnIO {
+            ModBusController.dockList.filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
+                .forEach { dock ->
+                    dock.deviceList.filterIsInstance<DockBean.KeyBean>().filter { it.isExist }
+                        .forEach { key ->
+                            key.rfid?.let {
+                                NetApi.getKeyInfo(it) {
+                                    if (it != null && !it.macAddress.isNullOrEmpty()) {
+                                        ModBusController.updateKeyMac(
+                                            dock.addr,
+                                            key.isLeft,
+                                            it.macAddress
+                                        )
+                                        ModBusController.updateKeyReadyStatus(
+                                            it.macAddress,
+                                            false,
+                                            5
+                                        )
+                                    } else {
+                                        LogUtil.e("Get key info fail : $it")
+                                        ToastUtils.tip(R.string.get_key_info_fail)
+                                    }
+                                }
+                            }
+                        }
+                }
+        }
+    }
 }

+ 9 - 1
app/src/main/java/com/grkj/iscs/view/presenter/JobProgressPresenter.kt

@@ -6,12 +6,15 @@ import com.grkj.iscs.R
 import com.grkj.iscs.extentions.serialNo
 import com.grkj.iscs.modbus.DockBean
 import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.modbus.ModBusController.dockList
 import com.grkj.iscs.model.Constants.JOB_STATUS_ACQUIRE_KEY
 import com.grkj.iscs.model.Constants.JOB_STATUS_ACQUIRE_LOCK
 import com.grkj.iscs.model.Constants.JOB_STATUS_NOT_STARTED
 import com.grkj.iscs.model.Constants.USER_TYPE_LOCKER
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_KEY
 import com.grkj.iscs.model.DeviceConst.DEVICE_TYPE_LOCK
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_PORTABLE
 import com.grkj.iscs.model.eventmsg.DeviceExceptionMsg
 import com.grkj.iscs.model.eventmsg.MsgEvent
 import com.grkj.iscs.model.eventmsg.MsgEventConstants.MSG_EVENT_DEVICE_EXCEPTION
@@ -29,6 +32,8 @@ import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.dialog.TipDialog
 import com.grkj.iscs.view.iview.IJobProgressView
 import com.grkj.iscs.view.step_mode.IStepMode
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.delay
 
 class JobProgressPresenter : BasePresenter<IJobProgressView>() {
     private var tipDialog: TipDialog? = null
@@ -75,7 +80,10 @@ class JobProgressPresenter : BasePresenter<IJobProgressView>() {
             ToastUtils.tip(mContext!!.resources.getString(R.string.ticket_id_is_null))
             return
         }
-        NetApi.getTicketDetail(ticketId) {
+        NetApi.getTicketDetail(ticketId) {it,_->
+            if (it==null){
+                return@getTicketDetail
+            }
             Executor.runOnMain {
                 callBack(it)
             }

+ 37 - 1
app/src/main/java/com/grkj/iscs/view/presenter/StepPresenter.kt

@@ -2,6 +2,11 @@ package com.grkj.iscs.view.presenter
 
 import android.content.Context
 import com.grkj.iscs.R
+import com.grkj.iscs.modbus.DockBean
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.modbus.ModBusController.dockList
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_PORTABLE
 import com.grkj.iscs.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs.model.vo.map.MapInfoRespVO
 import com.grkj.iscs.model.vo.ticket.LotoMapRespVO
@@ -13,6 +18,8 @@ import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.iview.IStepView
 import com.grkj.iscs.view.step_mode.IStepMode
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.delay
 
 class StepPresenter : BasePresenter<IStepView>() {
 
@@ -65,7 +72,10 @@ class StepPresenter : BasePresenter<IStepView>() {
             ToastUtils.tip(mContext!!.resources.getString(R.string.ticket_id_is_null))
             return
         }
-        NetApi.getTicketDetail(ticketId) {
+        NetApi.getTicketDetail(ticketId) {it,_->
+            if (it==null){
+                return@getTicketDetail
+            }
             Executor.runOnMain {
                 callBack.invoke(it)
             }
@@ -144,4 +154,30 @@ class StepPresenter : BasePresenter<IStepView>() {
             }
         }
     }
+
+    /**
+     * 预打开钥匙的充电尝试激活
+     */
+    fun preOpenKeyCharge(
+        context: Context,
+        step: Int
+    ) {
+        if (IStepMode.getStepMode(context)?.canPreOpenKeyCharge(step) == true) {
+            ThreadUtils.runOnIO {
+                val keyDockList =
+                    dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+                keyDockList.forEach { dock ->
+                    dock.deviceList.filter { it.isExist }.filterIsInstance<DockBean.KeyBean>()
+                        .forEach { key ->
+                            ModBusController.controlKeyCharge(false, key.isLeft, dock.addr) {
+                                ThreadUtils.runOnIO {
+                                    delay(3000)
+                                    ModBusController.controlKeyCharge(true, key.isLeft, dock.addr)
+                                }
+                            }
+                        }
+                }
+            }
+        }
+    }
 }

+ 32 - 1
app/src/main/java/com/grkj/iscs/view/presenter/WorkerPresenter.kt

@@ -2,7 +2,12 @@ package com.grkj.iscs.view.presenter
 
 import android.content.Context
 import com.grkj.iscs.R
+import com.grkj.iscs.modbus.DockBean
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.modbus.ModBusController.dockList
 import com.grkj.iscs.model.Constants.USER_ROLE_LOCKER
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_KEY
+import com.grkj.iscs.model.DeviceConst.DOCK_TYPE_PORTABLE
 import com.grkj.iscs.model.vo.ticket.StepDetailRespVO
 import com.grkj.iscs.model.vo.ticket.TicketDetailRespVO
 import com.grkj.iscs.model.vo.ticket.TicketUserReqVO
@@ -13,6 +18,8 @@ import com.grkj.iscs.util.ToastUtils
 import com.grkj.iscs.view.base.BasePresenter
 import com.grkj.iscs.view.iview.IWorkerView
 import com.grkj.iscs.view.step_mode.IStepMode
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.delay
 
 class WorkerPresenter : BasePresenter<IWorkerView>() {
     /**
@@ -23,7 +30,10 @@ class WorkerPresenter : BasePresenter<IWorkerView>() {
             ToastUtils.tip(mContext!!.resources.getString(R.string.ticket_id_is_null))
             return
         }
-        NetApi.getTicketDetail(ticketId) {
+        NetApi.getTicketDetail(ticketId) {it,_->
+            if (it==null){
+                return@getTicketDetail
+            }
             Executor.runOnMain {
                 callBack.invoke(it)
             }
@@ -121,4 +131,25 @@ class WorkerPresenter : BasePresenter<IWorkerView>() {
     fun getMinColockerSize(context: Context): Int {
         return IStepMode.getStepMode(context)?.getMinColockerSize() ?: 1
     }
+
+    /**
+     * 预打开钥匙的充电
+     */
+    fun preOpenKeyCharge() {
+        ThreadUtils.runOnIO {
+            val keyDockList =
+                dockList.filter { it.type == DOCK_TYPE_KEY || it.type == DOCK_TYPE_PORTABLE }
+            keyDockList.forEach { dock ->
+                dock.deviceList.filter { it.isExist }.filterIsInstance<DockBean.KeyBean>()
+                    .forEach { key ->
+                        ModBusController.controlKeyCharge(false, key.isLeft, dock.addr) {
+                            ThreadUtils.runOnIO {
+                                delay(3000)
+                                ModBusController.controlKeyCharge(true, key.isLeft, dock.addr)
+                            }
+                        }
+                    }
+            }
+        }
+    }
 }

+ 5 - 0
app/src/main/java/com/grkj/iscs/view/step_mode/IStepMode.kt

@@ -111,6 +111,11 @@ interface IStepMode {
      */
     fun checkCrossJobUnlockData(mTicketDetailData: TicketDetailRespVO?): Boolean
 
+    /**
+     * 当前步骤是否可以打开充电
+     */
+    fun canPreOpenKeyCharge(step: Int): Boolean
+
     companion object {
         /**
          * 根据存储的模式获取模式

+ 7 - 0
app/src/main/java/com/grkj/iscs/view/step_mode/StepMode1.kt

@@ -90,4 +90,11 @@ class StepMode1 : IStepMode {
     override fun checkCrossJobUnlockData(mTicketDetailData: TicketDetailRespVO?): Boolean {
         return false
     }
+
+    override fun canPreOpenKeyCharge(step: Int): Boolean {
+        return when (step) {
+            4, 5, 7 -> true
+            else -> false
+        }
+    }
 }

+ 7 - 0
app/src/main/java/com/grkj/iscs/view/step_mode/StepMode2.kt

@@ -180,4 +180,11 @@ class StepMode2 : IStepMode {
                 ?: mutableListOf())
         } == true
     }
+
+    override fun canPreOpenKeyCharge(step: Int): Boolean {
+        return when(step){
+            4, 5, 7 -> true
+            else -> false
+        }
+    }
 }

+ 291 - 0
app/src/main/java/com/grkj/iscs/view/viewmodel/DeviceRegistrationKeyAndLockViewModel.kt

@@ -0,0 +1,291 @@
+package com.grkj.iscs.view.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.liveData
+import com.clj.fastble.BleManager
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.grkj.iscs.BusinessManager
+import com.grkj.iscs.MyApplication
+import com.grkj.iscs.ble.BleConnectionManager
+import com.grkj.iscs.modbus.DockBean
+import com.grkj.iscs.modbus.ModBusController
+import com.grkj.iscs.model.DeviceConst
+import com.grkj.iscs.model.bo.DockStatusBO
+import com.grkj.iscs.model.bo.LoginUserBO
+import com.grkj.iscs.util.NetApi
+import com.grkj.iscs.util.SPUtils
+import com.grkj.iscs.util.log.LogUtil
+import com.grkj.iscs.view.base.BaseViewModel
+import com.grkj.iscs.view.fragment.DockTestFragment
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.collections.iterator
+import kotlin.coroutines.resume
+
+class DeviceRegistrationKeyAndLockViewModel : BaseViewModel() {
+    var loginUser: LoginUserBO? = null
+    val isLoadComplete: MutableLiveData<Boolean> = MutableLiveData(false)
+    var isStartCheckKey: Boolean = false
+    var isDestroy: Boolean = false
+    var newHardwareKeySize: Int = 0
+    var newHardwareLockSize: Int = 0
+    private val newHardwareKeyBean: MutableMap<Byte, MutableList<DockBean.KeyBean>> = mutableMapOf()
+    private val alreadyUsedMac: MutableList<String> = mutableListOf()
+
+    fun initData(rowList: MutableList<DockStatusBO>) {
+        val dockConfigJson =
+            SPUtils.getDockConfig(MyApplication.Companion.instance?.applicationContext!!)
+        if (!dockConfigJson.isNullOrEmpty()) {
+            val tempList: MutableList<DockTestFragment.DockTestBean> = Gson().fromJson(
+                dockConfigJson,
+                object : TypeToken<MutableList<DockTestFragment.DockTestBean>>() {}.type
+            )
+            if (tempList.isNotEmpty()) {
+                tempList.forEach { dock ->
+                    try {
+                        if (dock.type == DeviceConst.DOCK_TYPE_LOCK) {
+                            dock.deviceList = mutableListOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9)
+                        }
+                        rowList.find { it.row == dock.row.toInt() }?.let {
+                            it.dockList.add(dock)
+                        } ?: let {
+                            rowList.add(DockStatusBO(dock.row.toInt(), mutableListOf(dock)))
+                        }
+                    } catch (e: Exception) {
+                        LogUtil.e("Device status data error : ${e.message}")
+                    }
+                }
+                // 添加空行
+                val maxRow = rowList.maxBy { it.row }.row
+                for (i in 1..maxRow) {
+                    if (rowList.find { it.row == i } == null) {
+                        rowList.add(DockStatusBO(i, mutableListOf()))
+                    }
+                }
+                rowList.sortBy { it.row }
+            }
+        }
+    }
+
+    fun checkNewHardware(device: DockBean.DeviceBean, callback: () -> Unit) {
+        if (device is DockBean.KeyBean) {
+            device.mac = null
+            NetApi.getKeyInfo(device.rfid.toString()) {
+                device.newHardware = it == null
+                device.mac = it?.macAddress
+                callback()
+            }
+        } else if (device is DockBean.LockBean) {
+            NetApi.getLockInfo(device.rfid.toString()) {
+                device.newHardware = it == null
+                callback()
+            }
+        }
+    }
+
+    fun registerInitListener(): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            isStartCheckKey = false
+            newHardwareKeyBean.clear()
+            alreadyUsedMac.clear()
+            isLoadComplete.postValue(false)
+            val allDeviceCloseCmdSend =
+                BleConnectionManager.scanOnlineKeyLockMacAndSwitchModeToClose()
+            LogUtil.i("设备录入-是否所有关闭命令发送成功:${allDeviceCloseCmdSend}")
+            BleManager.getInstance().disconnectAllDevice()
+            BusinessManager.registerInitListener {
+                if (isStartCheckKey) {
+                    return@registerInitListener
+                }
+                isStartCheckKey = true
+                val dockList = ModBusController.dockList
+                ThreadUtils.runOnIO {
+                    val allDevice = ModBusController.dockList.map { it.deviceList }.flatten()
+                        .filter { it is DockBean.KeyBean || it is DockBean.LockBean }
+                        .filter { it.isExist }
+                    var currentCheckDevices = AtomicInteger(0)
+                    allDevice.forEach { device ->
+                        checkNewHardware(device) {
+                            currentCheckDevices.addAndGet(1)
+                            if (currentCheckDevices.get() == allDevice.size) {
+                                checkNewHardwareKey(dockList)
+                            }
+                        }
+                    }
+                    emit(true)
+                }
+            }
+        }
+    }
+
+    private fun checkNewHardwareKey(dockList: MutableList<DockBean>) {
+        ThreadUtils.runOnIO {
+            LogUtil.i("设备录入-重新检测是否是新设备完成")
+            newHardwareKeyBean.putAll(dockList.filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
+                .associate {
+                    it.addr to it.deviceList.filterIsInstance<DockBean.KeyBean>()
+                        .filter { it.newHardware && it.isExist }
+                        .toMutableList()
+                })
+            alreadyUsedMac.addAll(
+                dockList.filter { it.type == DeviceConst.DOCK_TYPE_KEY || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
+                    .map { it.deviceList }.flatten().filterIsInstance<DockBean.KeyBean>()
+                    .filter { it.mac?.isNotEmpty() == true }.mapNotNull { it.mac }
+            )
+            LogUtil.d("设备录入-新设备:${newHardwareKeyBean}")
+            for ((addr, keyBeans) in newHardwareKeyBean) {
+                for (keyBean in keyBeans) {
+                    if (isDestroy) {
+                        return@runOnIO
+                    }
+                    openChargeAndScanMac(addr, keyBean)
+                }
+            }
+            newHardwareKeySize = newHardwareKeyBean.map { it.value }.flatten().size
+            newHardwareLockSize =
+                ModBusController.dockList.filter { it.type == DeviceConst.DOCK_TYPE_LOCK || it.type == DeviceConst.DOCK_TYPE_PORTABLE }
+                    .map { it.deviceList }.flatten().filterIsInstance<DockBean.LockBean>()
+                    .count { it.newHardware && it.isExist }
+            isLoadComplete.postValue(true)
+        }
+    }
+
+    /**
+     * 打开充电并扫描蓝牙
+     */
+    suspend fun openChargeAndScanMac(addr: Byte, keyBean: DockBean.KeyBean): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            LogUtil.i("设备录入-关闭充电:${addr},${keyBean.idx}")
+            ModBusController.controlKeyCharge(false, keyBean.isLeft, addr) {
+                ThreadUtils.runOnIO {
+                    delay(800)
+                    LogUtil.i("设备录入-打开充电:${addr},${keyBean.idx}")
+                    ModBusController.controlKeyLockAndCharge(true, keyBean.isLeft, addr) {
+                        ThreadUtils.runOnIO {
+                            delay(3000)
+                            LogUtil.i("设备录入-开始扫描在线蓝牙Mac")
+                            BleConnectionManager.scanOnlineKeyLockMac { bleDevices ->
+                                LogUtil.i(
+                                    "设备录入-在线的蓝牙设备:${keyBean.rfid},${
+                                        bleDevices?.joinToString(
+                                            ","
+                                        ) { it.mac }
+                                    }"
+                                )
+                                if (isDestroy) {
+                                    cont.cancel()
+                                    return@scanOnlineKeyLockMac
+                                }
+                                if (bleDevices?.isEmpty() == true) {
+                                    ThreadUtils.runOnIO {
+                                        openChargeAndScanMac(addr, keyBean)
+                                    }
+                                } else {
+                                    val bleDevice = bleDevices?.find {
+                                        it.mac !in alreadyUsedMac
+                                    }
+                                    if (bleDevice?.mac?.isEmpty() == true) {
+                                        ThreadUtils.runOnIO {
+                                            openChargeAndScanMac(addr, keyBean)
+                                        }
+                                        return@scanOnlineKeyLockMac
+                                    }
+                                    LogUtil.i(
+                                        "设备录入-没有使用过的mac:${keyBean.rfid},${
+                                            bleDevice?.mac
+                                        }"
+                                    )
+                                    keyBean.mac = bleDevice?.mac!!
+                                    alreadyUsedMac.add(bleDevice.mac)
+                                    ThreadUtils.runOnIO {
+                                        keyBean.mac?.let {
+                                            val connected =
+                                                BleConnectionManager.tryConnectWithOptionalCharge(it)
+                                            if (connected) {
+                                                bleDevice?.let {
+                                                    BleConnectionManager.switchReadyMode(it)
+                                                }
+                                            }
+                                        }
+                                    }
+
+                                    cont.resume(true)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    fun unregisterInitListener() {
+        BusinessManager.unRegisterInitListener()
+    }
+
+    /**
+     * 设备录入挂起任务
+     */
+    private suspend fun deviceInputKeySuspend(keyNfc: String, keyMacAddress: String): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            NetApi.deviceInputKey(keyNfc, keyMacAddress) {
+                cont.resume(it)
+                cont.cancel()
+            }
+        }
+    }
+
+    /**
+     * 设备录入挂起任务
+     */
+    private suspend fun deviceInputLockSuspend(cardNo: String): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            NetApi.deviceInputLock(cardNo) {
+                cont.resume(it)
+                cont.cancel()
+            }
+        }
+    }
+
+    /**
+     * 设备录入(钥匙和挂锁)
+     * //todo 录入待完成
+     */
+    fun deviceInputData(dockData: MutableList<DockStatusBO>): LiveData<Triple<Boolean, Int, Int>> {
+        return liveData(Dispatchers.IO) {
+            val dockList = dockData.map { it.dockList }.flatten()
+            val deviceList =
+                ModBusController.dockList.filter { it.addr in dockList.map { it.address } }
+                    .map { it.deviceList }.flatten()
+            val lockDevice = deviceList.filter { it.type == DeviceConst.DEVICE_TYPE_LOCK }
+                .filterIsInstance<DockBean.LockBean>().filter { it.newHardware == true }
+            val keyDevice = deviceList.filter { it.type == DeviceConst.DEVICE_TYPE_KEY }
+                .filterIsInstance<DockBean.KeyBean>().filter { it.newHardware == true }
+            lockDevice.filter { it.rfid?.isNotEmpty() == true }.forEach { lockDevice ->
+                val isBind = deviceInputLockSuspend(lockDevice.rfid.toString())
+                if (isBind) {
+                    lockDevice.newHardware = false
+                }
+            }
+            keyDevice.filter { it.rfid?.isNotEmpty() == true && it.mac?.isNotEmpty() == true }
+                .forEach { keyDevice ->
+                    val isBind =
+                        deviceInputKeySuspend(keyDevice.rfid.toString(), keyDevice.mac.toString())
+                    if (isBind) {
+                        keyDevice.newHardware = false
+                    }
+                }
+            emit(
+                Triple(
+                    true,
+                    keyDevice.count { it.rfid?.isNotEmpty() == true && it.mac?.isNotEmpty() == true },
+                    lockDevice.count { it.rfid?.isNotEmpty() == true })
+            )
+        }
+    }
+}

+ 53 - 0
app/src/main/java/com/grkj/iscs/view/viewmodel/DeviceRegistrationViewModel.kt

@@ -0,0 +1,53 @@
+package com.grkj.iscs.view.viewmodel
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.liveData
+import com.grkj.iscs.enums.DeviceInputTypeEnum.*
+import com.grkj.iscs.model.ISCSDomainData
+import com.grkj.iscs.util.NetApi
+import com.grkj.iscs.view.base.BaseViewModel
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlin.coroutines.resume
+
+/**
+ * 设备录入
+ */
+class DeviceRegistrationViewModel : BaseViewModel() {
+    /**
+     * 设备录入
+     */
+    fun deviceInputScan(cardNo: String): LiveData<Boolean> {
+        return liveData(Dispatchers.IO) {
+            emit(deviceInputScanSuspend(cardNo))
+        }
+    }
+
+    /**
+     * 设备录入挂起任务
+     */
+    private suspend fun deviceInputScanSuspend(cardNo: String): Boolean {
+        return suspendCancellableCoroutine<Boolean> { cont ->
+            when (ISCSDomainData.deviceInputType) {
+                CARD -> {
+                    NetApi.deviceInputCard(cardNo) {
+                        cont.resume(it)
+                        cont.cancel()
+                    }
+                }
+
+                RFID -> {
+                    NetApi.deviceInputRFID(cardNo) {
+                        cont.resume(it)
+                        cont.cancel()
+                    }
+                }
+
+                else -> {
+                    cont.resume(false)
+                    cont.cancel()
+                }
+            }
+        }
+    }
+}

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

@@ -22,7 +22,7 @@ import kotlin.math.sin
 class CustomStationLayer @JvmOverloads constructor(
     mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
-
+    var inDraw: Boolean = false
     private var listener: MarkIsClickListener? = null
     private var radiusMark = 0f
     private lateinit var paint: Paint
@@ -68,6 +68,10 @@ class CustomStationLayer @JvmOverloads constructor(
     override fun draw(
         canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
     ) {
+        if (inDraw) {
+            return
+        }
+        inDraw = true
         this.currentZoom = currentZoom
         currentDegree = 360 - currentRotateDegrees
 
@@ -77,7 +81,8 @@ class CustomStationLayer @JvmOverloads constructor(
         // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
         canvas.concat(currentMatrix)
         val switchData = ModBusController.getSwitchData()
-        pointList.forEach { point ->
+        val tempPointList = pointList.toList()
+        tempPointList.forEach { point ->
             val switchStatus = switchData
                 .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
             // point.pos.x/y 已经是「图内像素坐标」
@@ -129,6 +134,7 @@ class CustomStationLayer @JvmOverloads constructor(
         }
 
         canvas.restore()
+        inDraw = false
     }
 
     private fun rotatePoint(

+ 9 - 1
app/src/main/java/com/grkj/iscs/view/widget/CustomSwitchStationLayer.kt

@@ -22,6 +22,8 @@ import kotlin.math.sin
 class CustomSwitchStationLayer @JvmOverloads constructor(
     mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
+    var inDraw = false
+
     // 呼吸灯周期(毫秒)
     private val breathePeriod = 1200f
     private val FRAME_INTERVAL = 32L   // 约 30fps
@@ -99,6 +101,10 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
     override fun draw(
         canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
     ) {
+        if (inDraw) {
+            return
+        }
+        inDraw = true
         this.currentZoom = currentZoom
         currentDegree = 360 - currentRotateDegrees
 
@@ -108,7 +114,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
         canvas.concat(currentMatrix)
         val switchData = ModBusController.getSwitchData()
-        pointList.forEach { point ->
+        val tempPointList = pointList.toList()
+        tempPointList.forEach { point ->
             val switchStatus = switchData
                 .find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
             // point.pos.x/y 已经是「图内像素坐标」
@@ -187,6 +194,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
         }
 
         canvas.restore()
+        inDraw = false
     }
 
     private fun rotatePoint(

+ 319 - 0
app/src/main/java/com/onlylemi/mapview/library/MapView.java

@@ -0,0 +1,319 @@
+package com.onlylemi.mapview.library;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Picture;
+import android.graphics.PointF;
+import android.graphics.SurfaceTexture;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.TextureView;
+
+import com.grkj.iscs.util.log.LogUtil;
+import com.onlylemi.mapview.library.layer.MapBaseLayer;
+import com.onlylemi.mapview.library.layer.MapLayer;
+import com.onlylemi.mapview.library.utils.MapMath;
+import com.onlylemi.mapview.library.utils.MapUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Java版:TextureView替代SurfaceView的MapView,实现独立画布
+ * 修复:支持在任意缩放级别下既能放大也能缩小。
+ */
+public class MapView extends TextureView implements TextureView.SurfaceTextureListener {
+
+    private SurfaceTexture surface;
+    private boolean isMapLoadFinish = false;
+    private final List<MapBaseLayer> layers = new ArrayList<>();
+    private MapLayer mapLayer;
+
+    private final Matrix saveMatrix = new Matrix();
+    private final Matrix currentMatrix = new Matrix();
+    private float currentZoom = 1.0f;
+    private float saveZoom = 1.0f;           // 默认初始保存zoom为1
+    private float currentRotateDegrees = 0.0f;
+    private float saveRotateDegrees = 0.0f;
+
+    private float minZoom = 0.5f;
+    private float maxZoom = 3.0f;
+    private boolean isScaleAndRotateTogether = false;
+
+    private final PointF startTouch = new PointF();
+    private final PointF lastMove = new PointF();
+    private final PointF mid = new PointF();
+    private float oldDist = 0f;
+    private float oldDegree = 0f;
+    private int currentTouchState = TOUCH_STATE_NO;
+
+    private MapViewListener mapViewListener;
+
+    public static final int TOUCH_STATE_NO = 0;
+    public static final int TOUCH_STATE_SCROLL = 1;
+    public static final int TOUCH_STATE_SCALE = 2;
+    public static final int TOUCH_STATE_ROTATE = 3;
+    public static final int TOUCH_STATE_TWO_POINTED = 4;
+
+    public MapView(Context context) {
+        this(context, null);
+    }
+
+    public MapView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MapView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+        init();
+    }
+
+    private void init() {
+        setSurfaceTextureListener(this);
+        setOpaque(true);
+        setClickable(true);
+    }
+
+    @Override
+    public void onSurfaceTextureAvailable(SurfaceTexture st, int width, int height) {
+        surface = st;
+        refresh();
+    }
+
+    @Override
+    public void onSurfaceTextureSizeChanged(SurfaceTexture st, int width, int height) {
+    }
+
+    @Override
+    public boolean onSurfaceTextureDestroyed(SurfaceTexture st) {
+        surface = null;
+        return true;
+    }
+
+    @Override
+    public void onSurfaceTextureUpdated(SurfaceTexture st) {
+    }
+
+    public void loadMap(Bitmap bitmap) {
+        loadMap(MapUtils.getPictureFromBitmap(bitmap));
+    }
+
+    public void loadMap(final Picture picture) {
+        isMapLoadFinish = false;
+        new Thread(new Runnable() {
+            @Override
+            public void run() {
+                if (picture != null) {
+                    if (mapLayer == null) {
+                        mapLayer = new MapLayer(MapView.this);
+                        layers.add(mapLayer);
+                    }
+                    mapLayer.setImage(picture);
+                    if (mapViewListener != null) {
+                        mapViewListener.onMapLoadSuccess();
+                    }
+                    isMapLoadFinish = true;
+                    post(new Runnable() {
+                        @Override
+                        public void run() {
+                            refresh();
+                        }
+                    });
+                } else {
+                    if (mapViewListener != null) {
+                        mapViewListener.onMapLoadFail();
+                    }
+                }
+            }
+        }).start();
+    }
+
+    public void refresh() {
+        if (surface == null || !isMapLoadFinish) return;
+        Canvas canvas = lockCanvas();
+        if (canvas != null) {
+            canvas.drawColor(Color.WHITE);
+            for (MapBaseLayer layer : layers) {
+                if (layer.isVisible) {
+                    layer.draw(canvas, currentMatrix, currentZoom, currentRotateDegrees);
+                }
+            }
+            unlockCanvasAndPost(canvas);
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!isMapLoadFinish) return false;
+        int action = event.getAction() & MotionEvent.ACTION_MASK;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                saveMatrix.set(currentMatrix);
+                startTouch.set(event.getX(), event.getY());
+                lastMove.set(event.getX(), event.getY());
+                currentTouchState = TOUCH_STATE_SCROLL;
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN:
+                if (event.getPointerCount() == 2) {
+                    saveMatrix.set(currentMatrix);
+                    saveZoom = currentZoom;
+                    saveRotateDegrees = currentRotateDegrees;
+                    mid.set(midPoint(event));
+                    oldDist = distance(event, mid);
+                    oldDegree = rotation(event, mid);
+                    currentTouchState = TOUCH_STATE_TWO_POINTED;
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (withFloorPlan(event.getX(), event.getY())) {
+                    for (MapBaseLayer layer : layers) {
+                        layer.onTouch(event);
+                    }
+                }
+                currentTouchState = TOUCH_STATE_NO;
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (currentTouchState == TOUCH_STATE_SCROLL) {
+                    currentMatrix.set(saveMatrix);
+                    currentMatrix.postTranslate(event.getX() - startTouch.x,
+                            event.getY() - startTouch.y);
+                } else if (currentTouchState == TOUCH_STATE_TWO_POINTED) {
+                    // 始终按缩放处理,避免旋转误判让缩小失效
+                    float newDist = distance(event, mid);
+                    float scale = newDist / oldDist;
+                    if (scale * saveZoom < minZoom) {
+                        scale = minZoom / saveZoom;
+                    } else if (scale * saveZoom > maxZoom) {
+                        scale = maxZoom / saveZoom;
+                    }
+                    currentZoom = scale * saveZoom;
+                    LogUtil.INSTANCE.i("当前比例:" + currentZoom);
+                    currentMatrix.set(saveMatrix);
+                    currentMatrix.postScale(scale, scale, mid.x, mid.y);
+                }
+                lastMove.set(event.getX(), event.getY());
+                refresh();
+                break;
+        }
+        return true;
+    }
+
+    public void setMapViewListener(MapViewListener listener) {
+        this.mapViewListener = listener;
+    }
+
+    public float[] convertMapXYToScreenXY(float x, float y) {
+        Matrix inv = new Matrix();
+        currentMatrix.invert(inv);
+        float[] pts = {x, y};
+        inv.mapPoints(pts);
+        return pts;
+    }
+
+    public boolean isMapLoadFinish() {
+        return isMapLoadFinish;
+    }
+
+    public void addLayer(MapBaseLayer layer) {
+        layers.add(layer);
+    }
+
+    public List<MapBaseLayer> getLayers() {
+        return layers;
+    }
+
+    public void translate(float x, float y) {
+        currentMatrix.postTranslate(x, y);
+        refresh();
+    }
+
+    public void mapCenterWithPoint(float x, float y) {
+        float[] pts = {x, y};
+        currentMatrix.mapPoints(pts);
+        float dx = getWidth() / 2f - pts[0];
+        float dy = getHeight() / 2f - pts[1];
+        currentMatrix.postTranslate(dx, dy);
+        refresh();
+    }
+
+    public float getCurrentRotateDegrees() {
+        return currentRotateDegrees;
+    }
+
+    public void setCurrentRotateDegrees(float degrees) {
+        mapCenterWithPoint(
+                mapLayer != null ? mapLayer.getImage().getWidth() / 2f : 0f,
+                mapLayer != null ? mapLayer.getImage().getHeight() / 2f : 0f
+        );
+        setCurrentRotateDegrees(degrees, getWidth() / 2f, getHeight() / 2f);
+    }
+
+    public void setCurrentRotateDegrees(float degrees, float px, float py) {
+        currentMatrix.postRotate(degrees - currentRotateDegrees, px, py);
+        currentRotateDegrees = (degrees % 360 + 360) % 360;
+        refresh();
+    }
+
+    public float getCurrentZoom() {
+        return currentZoom;
+    }
+
+    public void setCurrentZoom(float zoom) {
+        setCurrentZoom(zoom, getWidth() / 2f, getHeight() / 2f);
+    }
+
+    public void setCurrentZoom(float zoom, float px, float py) {
+        currentMatrix.postScale(zoom / currentZoom, zoom / currentZoom, px, py);
+        currentZoom = zoom;
+        refresh();
+    }
+
+    public boolean isScaleAndRotateTogether() {
+        return isScaleAndRotateTogether;
+    }
+
+    public void setScaleAndRotateTogether(boolean flag) {
+        this.isScaleAndRotateTogether = flag;
+    }
+
+    public void setMaxZoom(float max) {
+        this.maxZoom = max;
+    }
+
+    public void setMinZoom(float min) {
+        LogUtil.INSTANCE.i("最小缩放比:" + min);
+        this.minZoom = min;
+    }
+
+    private PointF midPoint(MotionEvent event) {
+        return MapMath.getMidPointBetweenTwoPoints(
+                event.getX(0), event.getY(0), event.getX(1), event.getY(1));
+    }
+
+    private float distance(MotionEvent event, PointF mid) {
+        return MapMath.getDistanceBetweenTwoPoints(
+                event.getX(0), event.getY(0), mid.x, mid.y);
+    }
+
+    private float rotation(MotionEvent event, PointF mid) {
+        return MapMath.getDegreeBetweenTwoPoints(
+                event.getX(0), event.getY(0), mid.x, mid.y);
+    }
+
+    public boolean withFloorPlan(float x, float y) {
+        float[] pts = convertMapXYToScreenXY(x, y);
+        Picture img = mapLayer != null ? mapLayer.getImage() : null;
+        return img != null && pts[0] > 0 && pts[0] < img.getWidth() && pts[1] > 0 && pts[1] < img.getHeight();
+    }
+
+    public float getMapWidth() {
+        return mapLayer != null ? mapLayer.getImage().getWidth() : 0f;
+    }
+
+    public float getMapHeight() {
+        return mapLayer != null ? mapLayer.getImage().getHeight() : 0f;
+    }
+}

+ 19 - 0
app/src/main/java/com/onlylemi/mapview/library/MapViewListener.java

@@ -0,0 +1,19 @@
+package com.onlylemi.mapview.library;
+
+/**
+ * MapViewListener
+ *
+ * @author: onlylemi
+ */
+public interface MapViewListener {
+
+    /**
+     * when mapview load complete to callback
+     */
+    void onMapLoadSuccess();
+
+    /**
+     * when mapview load error to callback
+     */
+    void onMapLoadFail();
+}

+ 59 - 0
app/src/main/java/com/onlylemi/mapview/library/layer/MapBaseLayer.java

@@ -0,0 +1,59 @@
+package com.onlylemi.mapview.library.layer;
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.util.TypedValue;
+import android.view.MotionEvent;
+
+import com.onlylemi.mapview.library.MapView;
+
+/**
+ * MapBaseLayer
+ *
+ * @author: onlylemi
+ */
+public abstract class MapBaseLayer {
+
+    // map layer level
+    protected static final int MAP_LEVEL = 0;
+    // location layer level
+    protected static final int LOCATION_LEVEL = Integer.MAX_VALUE;
+
+    // layer show level
+    public int level;
+    // layer is/not show
+    public boolean isVisible = true;
+
+    protected MapView mapView;
+
+    public MapBaseLayer(MapView mapView) {
+        this.mapView = mapView;
+    }
+
+    /**
+     * touch event
+     *
+     * @param event
+     */
+    public abstract void onTouch(MotionEvent event);
+
+    /**
+     * draw event
+     *
+     * @param canvas
+     * @param currentMatrix
+     * @param currentZoom
+     * @param currentRotateDegrees
+     */
+    public abstract void draw(Canvas canvas, Matrix currentMatrix, float currentZoom,
+                              float currentRotateDegrees);
+
+    public void setLevel(int level) {
+        this.level = level;
+    }
+
+    protected float setValue(float value) {
+        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, mapView.getResources()
+                .getDisplayMetrics());
+    }
+}

+ 109 - 0
app/src/main/java/com/onlylemi/mapview/library/layer/MapLayer.java

@@ -0,0 +1,109 @@
+package com.onlylemi.mapview.library.layer;
+
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Picture;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.ViewTreeObserver;
+
+import com.onlylemi.mapview.library.MapView;
+
+/**
+ * MapLayer
+ *
+ * @author: onlylemi
+ */
+public class MapLayer extends MapBaseLayer {
+
+    private static final String TAG = "MapLayer";
+
+    private Picture image;
+    private boolean hasMeasured;
+
+    public MapLayer(MapView mapView) {
+        super(mapView);
+        level = MAP_LEVEL;
+    }
+
+    public void setImage(Picture image) {
+        this.image = image;
+
+        if (mapView.getWidth() == 0) {
+            ViewTreeObserver vto = mapView.getViewTreeObserver();
+            vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+                public boolean onPreDraw() {
+                    if (!hasMeasured) {
+                        initMapLayer();
+                        hasMeasured = true;
+                    }
+                    return true;
+                }
+            });
+        } else {
+            initMapLayer();
+        }
+    }
+
+
+    /**
+     * init map image layer
+     */
+    private void initMapLayer() {
+        float zoom = getInitZoom(mapView.getWidth(), mapView.getHeight(), image.getWidth(), image
+                .getHeight());
+        Log.i(TAG, Float.toString(zoom));
+        mapView.setMinZoom(zoom);
+        mapView.setCurrentZoom(zoom, 0, 0);
+
+        float width = mapView.getWidth() - zoom * image.getWidth();
+        float height = mapView.getHeight() - zoom * image.getHeight();
+
+        mapView.translate(width / 2, height / 2);
+    }
+
+    /**
+     * calculate init zoom
+     *
+     * @param viewWidth
+     * @param viewHeight
+     * @param imageWidth
+     * @param imageHeight
+     * @return
+     */
+    private float getInitZoom(float viewWidth, float viewHeight, float imageWidth,
+                              float imageHeight) {
+        float widthRatio = viewWidth / imageWidth;
+        float heightRatio = viewHeight / imageHeight;
+
+        Log.i(TAG, "widthRatio:" + widthRatio);
+        Log.i(TAG, "widthRatio:" + heightRatio);
+
+        if (widthRatio * imageHeight <= viewHeight) {
+            return widthRatio;
+        } else if (heightRatio * imageWidth <= viewWidth) {
+            return heightRatio;
+        }
+        return 0;
+    }
+
+    @Override
+    public void onTouch(MotionEvent event) {
+
+    }
+
+    @Override
+    public void draw(Canvas canvas, Matrix currentMatrix, float currentZoom, float
+            currentRotateDegrees) {
+        canvas.save();
+        canvas.setMatrix(currentMatrix);
+        if (image != null) {
+            canvas.drawPicture(image);
+        }
+        canvas.restore();
+    }
+
+    public Picture getImage() {
+        return image;
+    }
+}

+ 305 - 0
app/src/main/java/com/onlylemi/mapview/library/utils/MapMath.java

@@ -0,0 +1,305 @@
+package com.onlylemi.mapview.library.utils;
+
+import android.graphics.PointF;
+import android.util.Log;
+
+import com.onlylemi.mapview.library.utils.math.FloydAlgorithm;
+import com.onlylemi.mapview.library.utils.math.GeneticAlgorithm;
+import com.onlylemi.mapview.library.utils.math.TSPNearestNeighbour;
+
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MapMath
+ *
+ * @author onlylemi
+ */
+public final class MapMath {
+
+    private MapMath() {}
+
+    /**
+     * the distance between two points
+     *
+     * @param x1
+     * @param y1
+     * @param x2
+     * @param y2
+     * @return
+     */
+    public static float getDistanceBetweenTwoPoints(float x1, float y1,
+                                                    float x2, float y2) {
+        return (float) Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
+    }
+
+    /**
+     * the distance between two points
+     *
+     * @param start
+     * @param end
+     * @return
+     */
+    public static float getDistanceBetweenTwoPoints(PointF start, PointF end) {
+        return (float) Math.sqrt(Math.pow(end.x - start.x, 2)
+                + Math.pow(end.y - start.y, 2));
+    }
+
+
+    /**
+     * the shortest path between two points (FloydAlgorithm)
+     *
+     * @param begin
+     * @param end
+     * @param matrix adjacency matrix
+     * @return
+     */
+    public static List<Integer> getShortestPathBetweenTwoPoints(int begin,
+                                                                int end, float[][] matrix) {
+        return FloydAlgorithm.getInstance().findCheapestPath(begin, end, matrix);
+    }
+
+    /**
+     * the best path between some points (NearestNeighbour tsp)
+     *
+     * @param matrix adjacency matrix
+     * @return
+     */
+    public static List<Integer> getBestPathBetweenPointsByNearestNeighbour(float[][] matrix) {
+        return TSPNearestNeighbour.getInstance().tsp(matrix);
+    }
+
+    /**
+     * the best path between some points (GeneticAlgorithm tsp)
+     *
+     * @param matrix
+     * @return
+     */
+    public static List<Integer> getBestPathBetweenPointsByGeneticAlgorithm(float[][] matrix) {
+        GeneticAlgorithm ga = GeneticAlgorithm.getInstance();
+        ga.setAutoNextGeneration(true);
+        ga.setMaxGeneration(200);
+        int[] best = ga.tsp(matrix);
+
+        List<Integer> result = new ArrayList<>(best.length);
+        for (int i = 0; i < best.length; i++) {
+            result.add(best[i]);
+        }
+        return result;
+    }
+
+
+    /**
+     * get the angle between two points and the horizontal plane
+     *
+     * @param start
+     * @param end
+     * @return
+     */
+    public static float getDegreeBetweenTwoPointsWithHorizontal(PointF start, PointF end) {
+        float angle = 90.0f;
+        if (start.x != end.x) {
+            angle = (float) Math.toDegrees(Math.atan((end.y - start.y)
+                    / (end.x - start.x)));
+            if (end.x < start.x && end.y >= start.y) {
+                angle = angle + 180.0f;
+            } else if (end.x < start.x && end.y > start.y) {
+                angle = angle - 180.f;
+            }
+        } else {
+            if (start.y < end.y) {
+                angle = 90.0f;
+            } else if (start.y > end.y) {
+                angle = -90.0f;
+            }
+        }
+        return angle;
+    }
+
+    /**
+     * get the angle between two points and the vertical plane
+     *
+     * @param start
+     * @param end
+     * @return
+     */
+    public static float getDegreeBetweenTwoPointsWithVertical(PointF start, PointF end) {
+        float angle = 90.0f;
+        if (start.y != end.y) {
+            angle = -(float) Math.toDegrees(Math.atan((end.x - start.x)
+                    / (end.y - start.y)));
+            if (end.y > start.y && end.x >= start.x) {
+                angle = angle + 180.0f;
+            } else if (end.y > start.y && end.x > start.x) {
+                angle = angle - 180.f;
+            }
+        } else {
+            if (start.x < end.x) {
+                angle = 90.0f;
+            } else if (start.x > end.x) {
+                angle = -90.0f;
+            }
+        }
+        return angle;
+    }
+
+    /**
+     * get degree between two points
+     *
+     * @param x1
+     * @param y1
+     * @param x2
+     * @param y2
+     * @return
+     */
+    public static float getDegreeBetweenTwoPoints(float x1, float y1, float x2, float y2) {
+        double radians = Math.atan2(y1 - y2, x1 - x2);
+        return (float) Math.toDegrees(radians);
+    }
+
+    /**
+     * get degree between two points
+     *
+     * @param start
+     * @param end
+     * @return
+     */
+    public static float getDegreeBetweenTwoPoints(PointF start, PointF end) {
+        return getDegreeBetweenTwoPoints(start.x, start.y, end.x, end.y);
+    }
+
+    /**
+     * The coordinates of the midpoint between two points are obtained
+     *
+     * @param x1
+     * @param y1
+     * @param x2
+     * @param y2
+     * @return
+     */
+    public static PointF getMidPointBetweenTwoPoints(float x1, float y1, float x2, float y2) {
+        return new PointF((x1 + x2) / 2, (y1 + y2) / 2);
+    }
+
+    /**
+     * The coordinates of the midpoint between two points are obtained
+     *
+     * @param start
+     * @param end
+     * @return
+     */
+    public static PointF getMidPointBetweenTwoPoints(PointF start, PointF end) {
+        return getMidPointBetweenTwoPoints(start.x, start.y, end.x, end.y);
+    }
+
+    /**
+     * Get the coordinates of any point between two points
+     *
+     * @param start
+     * @param end
+     * @param value
+     * @return
+     */
+    public static PointF getEveryPointBetweenTwoPoints(PointF start, PointF end, float value) {
+        // y=kx+b
+        float x, y;
+        // with slope
+        if (start.x != end.x) {
+            float k = (end.y - start.y) / (end.x - start.x);
+            float b = end.y - k * end.x;
+
+            if (end.x > start.x) {
+                x = Math.min(end.x, start.x) + (end.x - start.x) * value;
+            } else {
+                x = Math.max(end.x, start.x) + (end.x - start.x) * value;
+            }
+            y = k * x + b;
+        } else { // no slope
+            x = start.x;
+            if (end.y > start.y) {
+                y = Math.min(end.y, start.y) + (end.y - start.y) * value;
+            } else {
+                y = Math.max(end.y, start.y) + (end.y - start.y) * value;
+            }
+        }
+        return new PointF(x, y);
+    }
+
+
+    /**
+     * Get a shortest distance from point to line
+     *
+     * @param point
+     * @param linePoint1 Determine the first point of a straight line
+     * @param linePoint2 Determine the second point of a straight line
+     * @return
+     */
+    public static float getDistanceFromPointToLine(PointF point, PointF linePoint1, PointF
+            linePoint2) {
+        // y = kx + b;
+        // d = |kx-y+b| / √(k^2+1)
+        float d;
+        if (linePoint1.x != linePoint2.x) { // with slope
+            float k = (linePoint2.y - linePoint1.y) / (linePoint2.x - linePoint1.x);
+            float b = linePoint2.y - k * linePoint2.x;
+            d = Math.abs(k * point.x - point.y + b) / (float) Math.sqrt(k * k + 1);
+        } else { // no slope
+            d = Math.abs(point.x - linePoint1.x);
+        }
+        return d;
+    }
+
+    /**
+     * get intersection coordinates from a point to a line
+     *
+     * @param point
+     * @param linePoint1
+     * @param linePoint2
+     * @return
+     */
+    public static PointF getIntersectionCoordinatesFromPointToLine(PointF point, PointF linePoint1, PointF
+            linePoint2) {
+        // y = kx + b;
+        float x, y;
+        if (linePoint1.x != linePoint2.x) { // with slope
+            float k = (linePoint2.y - linePoint1.y) / (linePoint2.x - linePoint1.x);
+            float b = linePoint2.y - k * linePoint2.x;
+            // The equation of point
+            if (k != 0) {
+                float kV = -1 / k;
+                float bV = point.y - kV * point.x;
+                x = (b - bV) / (kV - k);
+                y = kV * x + bV;
+            } else {
+                x = point.x;
+                y = linePoint1.y;
+            }
+        } else { // no slope
+            x = linePoint1.x;
+            y = point.y;
+        }
+        return new PointF(x, y);
+    }
+
+    /**
+     * is/not obtuse angle between a point and a line
+     *
+     * @param point
+     * @param linePoint1
+     * @param linePoint2
+     * @return
+     */
+    public static boolean isObtuseAnglePointAndLine(PointF point, PointF linePoint1, PointF
+            linePoint2) {
+        // A*A + B*B < C*C
+        float p_l1, p_l2, l1_l2;
+        p_l1 = getDistanceBetweenTwoPoints(point, linePoint1);
+        p_l2 = getDistanceBetweenTwoPoints(point, linePoint2);
+        l1_l2 = getDistanceBetweenTwoPoints(linePoint1, linePoint2);
+
+        return ((p_l1 * p_l1 + l1_l2 * l1_l2) < p_l2 * p_l2)
+                || ((p_l2 * p_l2 + l1_l2 * l1_l2) < p_l1 * p_l1);
+    }
+
+}

+ 334 - 0
app/src/main/java/com/onlylemi/mapview/library/utils/MapUtils.java

@@ -0,0 +1,334 @@
+package com.onlylemi.mapview.library.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Picture;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MapUtils
+ *
+ * @author onlylemi
+ */
+public final class MapUtils {
+
+    private static final String TAG = "MapUtils: ";
+
+    private static final float INF = Float.MAX_VALUE;
+    private static int nodesSize;
+    private static int nodesContactSize;
+
+    private MapUtils() {}
+
+    public static void init(int nodessize, int nodescontactsize) {
+        nodesSize = nodessize;
+        nodesContactSize = nodescontactsize;
+    }
+
+    /**
+     * Get the distance between points
+     *
+     * @param nodes
+     * @param list
+     * @return
+     */
+    public static float getDistanceBetweenList(List<PointF> nodes,
+                                               List<Integer> list) {
+        float distance = 0;
+        for (int i = 0; i < list.size() - 1; i++) {
+            distance += MapMath.getDistanceBetweenTwoPoints(nodes.get(list.get(i)),
+                    nodes.get(list.get(i + 1)));
+        }
+        return distance;
+    }
+
+    /**
+     * get degrees between two points(list) with horizontal plane
+     *
+     * @param routeList
+     * @param nodes
+     * @return
+     */
+    public static List<Float> getDegreeBetweenTwoPointsWithHorizontal(List<Integer> routeList,
+                                                                      List<PointF> nodes) {
+        List<Float> routeListDegrees = new ArrayList<>();
+        for (int i = 0; i < routeList.size() - 1; i++) {
+            routeListDegrees.add(MapMath.getDegreeBetweenTwoPointsWithHorizontal(nodes.get
+                            (routeList.get(i)),
+                    nodes.get(routeList.get(i + 1))));
+        }
+        return routeListDegrees;
+    }
+
+    /**
+     * get degrees between two points(list) with vertical plane
+     *
+     * @param routeList
+     * @param nodes
+     * @return
+     */
+    public static List<Float> getDegreeBetweenTwoPointsWithVertical(List<Integer> routeList,
+                                                                    List<PointF> nodes) {
+        List<Float> routeListDegrees = new ArrayList<>();
+        for (int i = 0; i < routeList.size() - 1; i++) {
+            routeListDegrees.add(MapMath.getDegreeBetweenTwoPointsWithVertical(nodes.get
+                            (routeList.get(i)),
+                    nodes.get(routeList.get(i + 1))));
+        }
+        return routeListDegrees;
+    }
+
+    /**
+     * get shortest path between two points
+     *
+     * @param start        start point
+     * @param end          end point
+     * @param nodes        nodes list
+     * @param nodesContact nodesContact list
+     * @return
+     */
+    public static List<Integer> getShortestPathBetweenTwoPoints(int start,
+                                                                int end, List<PointF> nodes,
+                                                                List<PointF> nodesContact) {
+        float[][] matrix = getMatrixBetweenFloorPlanNodes(nodes, nodesContact);
+
+        return MapMath.getShortestPathBetweenTwoPoints(start, end, matrix);
+    }
+
+    /**
+     * get best path between points
+     *
+     * @param points
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static List<Integer> getBestPathBetweenPoints(int[] points, List<PointF> nodes,
+                                                         List<PointF> nodesContact) {
+        // adjacency matrix
+        float[][] matrix = new float[points.length][points.length];
+        for (int i = 0; i < matrix.length; i++) {
+            for (int j = i; j < matrix[i].length; j++) {
+                if (i == j) {
+                    matrix[i][j] = INF;
+                } else {
+                    matrix[i][j] = getDistanceBetweenList(
+                            nodes, getShortestPathBetweenTwoPoints(points[i],
+                                    points[j], nodes, nodesContact));
+                    matrix[j][i] = matrix[i][j];
+                }
+            }
+        }
+
+        // TSP to get best path
+        List<Integer> routeList = new ArrayList<>();
+        List<Integer> result = MapMath.getBestPathBetweenPointsByGeneticAlgorithm(matrix);
+        for (int i = 0; i < result.size() - 1; i++) {
+            int size = routeList.size();
+            routeList.addAll(getShortestPathBetweenTwoPoints(
+                    points[result.get(i)], points[result.get(i + 1)], nodes,
+                    nodesContact));
+            if (i != 0) {
+                routeList.remove(size);
+            }
+        }
+        return routeList;
+    }
+
+
+    /**
+     * get best path between points
+     *
+     * @param pointList
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static List<Integer> getBestPathBetweenPoints(List<PointF> pointList,
+                                                         List<PointF> nodes, List<PointF>
+                                                                 nodesContact) {
+        if (nodesSize != nodes.size()) {
+            int value = nodes.size() - nodesSize;
+            for (int i = 0; i < value; i++) {
+                nodes.remove(nodes.size() - 1);
+            }
+            value = nodesContact.size() - nodesContactSize;
+            for (int i = 0; i < value; i++) {
+                nodesContact.remove(nodesContact.size() - 1);
+            }
+        }
+
+        //find the point on the nearest route
+        int[] points = new int[pointList.size()];
+        for (int i = 0; i < pointList.size(); i++) {
+            addPointToList(pointList.get(i), nodes, nodesContact);
+            points[i] = nodes.size() - 1;
+        }
+
+        return getBestPathBetweenPoints(points, nodes, nodesContact);
+    }
+
+    /**
+     * get shortest distance between two points
+     *
+     * @param start
+     * @param end
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static float getShortestDistanceBetweenTwoPoints(int start, int end,
+                                                            List<PointF> nodes, List<PointF>
+                                                                    nodesContact) {
+        List<Integer> list = getShortestPathBetweenTwoPoints(start, end, nodes,
+                nodesContact);
+        return getDistanceBetweenList(nodes, list);
+    }
+
+    /**
+     * adjacency matrix with points
+     *
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static float[][] getMatrixBetweenFloorPlanNodes(List<PointF> nodes, List<PointF>
+            nodesContact) {
+        // set default is INF
+        float[][] matrix = new float[nodes.size()][nodes.size()];
+        for (int i = 0; i < matrix.length; i++) {
+            for (int j = 0; j < matrix[i].length; j++) {
+                matrix[i][j] = INF;
+            }
+        }
+
+        // set value for matrix
+        for (int i = 0; i < nodesContact.size(); i++) {
+            matrix[(int) nodesContact.get(i).x][(int) nodesContact.get(i).y] = MapMath
+                    .getDistanceBetweenTwoPoints(nodes.get((int) nodesContact.get(i).x),
+                            nodes.get((int) nodesContact.get(i).y));
+
+            matrix[(int) nodesContact.get(i).y][(int) nodesContact.get(i).x] = matrix[(int)
+                    nodesContact
+                            .get(i).x][(int) nodesContact.get(i).y];
+        }
+
+        return matrix;
+    }
+
+    /**
+     * get shortest distance between two points
+     *
+     * @param start
+     * @param end
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static List<Integer> getShortestDistanceBetweenTwoPoints(PointF start, PointF end,
+                                                                    List<PointF> nodes,
+                                                                    List<PointF> nodesContact) {
+        if (nodesSize != nodes.size()) {
+            int value = nodes.size() - nodesSize;
+            for (int i = 0; i < value; i++) {
+                nodes.remove(nodes.size() - 1);
+            }
+            value = nodesContact.size() - nodesContactSize;
+            for (int i = 0; i < value; i++) {
+                nodesContact.remove(nodesContact.size() - 1);
+            }
+        }
+
+        addPointToList(start, nodes, nodesContact);
+        addPointToList(end, nodes, nodesContact);
+
+        return getShortestPathBetweenTwoPoints(nodes.size() - 2, nodes.size() - 1, nodes,
+                nodesContact);
+    }
+
+    /**
+     * get the shortest path from the position point to the target point in the map
+     *
+     * @param position
+     * @param target
+     * @param nodes
+     * @param nodesContact
+     * @return
+     */
+    public static List<Integer> getShortestDistanceBetweenTwoPoints(PointF position, int target,
+                                                                    List<PointF> nodes,
+                                                                    List<PointF> nodesContact) {
+        if (nodesSize != nodes.size()) {
+            int value = nodes.size() - nodesSize;
+            for (int i = 0; i < value; i++) {
+                nodes.remove(nodes.size() - 1);
+            }
+            value = nodesContact.size() - nodesContactSize;
+            for (int i = 0; i < value; i++) {
+                nodesContact.remove(nodesContact.size() - 1);
+            }
+        }
+
+        addPointToList(position, nodes, nodesContact);
+
+        return getShortestPathBetweenTwoPoints(nodes.size() - 1, target, nodes, nodesContact);
+    }
+
+    /**
+     * add point to list
+     *
+     * @param point
+     * @param nodes
+     * @param nodesContact
+     */
+    private static void addPointToList(PointF point, List<PointF> nodes, List<PointF>
+            nodesContact) {
+        if (point != null) {
+            PointF pV = null;
+            int po1 = 0, po2 = 0;
+            float min1 = INF;
+            for (int i = 0; i < nodesContact.size() - 1; i++) {
+                PointF p1 = nodes.get((int) nodesContact.get(i).x);
+                PointF p2 = nodes.get((int) nodesContact.get(i).y);
+                if (!MapMath.isObtuseAnglePointAndLine(point, p1, p2)) {
+                    float minDis = MapMath.getDistanceFromPointToLine(point, p1, p2);
+                    if (min1 > minDis) {
+                        pV = MapMath.getIntersectionCoordinatesFromPointToLine(point, p1, p2);
+                        min1 = minDis;
+                        po1 = (int) nodesContact.get(i).x;
+                        po2 = (int) nodesContact.get(i).y;
+                    }
+                }
+            }
+            // get intersection
+            nodes.add(pV);
+            //Log.i(TAG, "node=" + (nodes.size() - 1) + ", po1=" + po1 + ", po2=" + po2);
+            nodesContact.add(new PointF(po1, nodes.size() - 1));
+            nodesContact.add(new PointF(po2, nodes.size() - 1));
+        }
+    }
+
+    /**
+     * bitmap to picture
+     *
+     * @param bitmap
+     * @return
+     */
+    public static Picture getPictureFromBitmap(Bitmap bitmap) {
+        Picture picture = new Picture();
+        Canvas canvas = picture.beginRecording(bitmap.getWidth(),
+                bitmap.getHeight());
+        canvas.drawBitmap(
+                bitmap,
+                null,
+                new RectF(0f, 0f, (float) bitmap.getWidth(), (float) bitmap
+                        .getHeight()), null);
+        picture.endRecording();
+        return picture;
+    }
+}

+ 87 - 0
app/src/main/java/com/onlylemi/mapview/library/utils/math/FloydAlgorithm.java

@@ -0,0 +1,87 @@
+package com.onlylemi.mapview.library.utils.math;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * FloydAlgorithm
+ *
+ * @author: onlylemi
+ */
+public final class FloydAlgorithm {
+
+    private static final int INF = Integer.MAX_VALUE;
+    private float[][] dist;
+
+    // the shortest path from i to j
+    private int[][] path;
+    private List<Integer> result;
+
+    public static FloydAlgorithm getInstance() {
+        return FloydAlgorithmHolder.instance;
+    }
+
+    private static class FloydAlgorithmHolder {
+        private static FloydAlgorithm instance = new FloydAlgorithm();
+    }
+
+    private void init(float[][] matrix) {
+        dist = null;
+        path = null;
+        result = new ArrayList<>();
+
+        this.dist = new float[matrix.length][matrix.length];
+        this.path = new int[matrix.length][matrix.length];
+    }
+
+    /**
+     * the shortest between begin to end
+     *
+     * @param begin
+     * @param end
+     * @param matrix
+     */
+    public List<Integer> findCheapestPath(int begin, int end, float[][] matrix) {
+        init(matrix);
+
+        floyd(matrix);
+        result.add(begin);
+        findPath(begin, end);
+        result.add(end);
+
+        return result;
+    }
+
+    private void findPath(int i, int j) {
+        int k = path[i][j];
+        if (k == -1)
+            return;
+        findPath(i, k); // recursion
+        result.add(k);
+        findPath(k, j);
+    }
+
+    private void floyd(float[][] matrix) {
+        int size = matrix.length;
+        // initialize dist and path
+        for (int i = 0; i < size; i++) {
+            for (int j = 0; j < size; j++) {
+                path[i][j] = -1;
+                dist[i][j] = matrix[i][j];
+            }
+        }
+        for (int k = 0; k < size; k++) {
+            for (int i = 0; i < size; i++) {
+                for (int j = 0; j < size; j++) {
+                    if (dist[i][k] != INF && dist[k][j] != INF
+                            && dist[i][k] + dist[k][j] < dist[i][j]) {
+                        dist[i][j] = dist[i][k] + dist[k][j];
+                        path[i][j] = k;
+                    }
+                }
+            }
+        }
+
+    }
+
+}

+ 440 - 0
app/src/main/java/com/onlylemi/mapview/library/utils/math/GeneticAlgorithm.java

@@ -0,0 +1,440 @@
+package com.onlylemi.mapview.library.utils.math;
+
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * GeneticAlgorithm
+ *
+ * @author: onlylemi
+ */
+public class GeneticAlgorithm {
+
+    private static final float DEFAULT_CROSSOVER_PROBABILITY = 0.9f; // 默认交叉概率
+    private static final float DEFAULT_MUTATION_PROBABILITY = 0.01f; // 默认突变概率
+    private static final int DEFAULT_POPULATION_SIZE = 30; // 默认种群数量
+    private static final int PREVIOUS = 0;
+    private static final int NEXT = 1;
+
+    private float crossoverProbability = DEFAULT_CROSSOVER_PROBABILITY; // 交叉概率
+    private float mutationProbability = DEFAULT_MUTATION_PROBABILITY; // 突变概率
+
+    private int populationSize = DEFAULT_POPULATION_SIZE; // 种群数量
+    private int mutationTimes = 0; // 变异次数
+    private int currentGeneration = 0; // 当前的一代
+
+    private int maxGeneration = 1000; // 最大代数
+    private int pointNum;
+    private int[][] population; // 种群集
+
+    private float[][] dist; // 点集间的邻接矩阵
+    private int[] bestIndivial; // 最短的结果集
+    private float bestDist; // 最短的距离
+    private int currentBestPosition; // 当前最好个体的位置
+
+    private float currentBestDist; // 当前最好个体的距离
+    private float[] values; // 种群中每个个体的dist
+    private float[] fitnessValues; // 适应度集
+
+    private float[] roulette;
+
+    private boolean isAutoNextGeneration = false;
+
+    private static Random rd;
+
+    public static GeneticAlgorithm getInstance() {
+        return GeneticAlgorithmHolder.instance;
+    }
+
+    private static class GeneticAlgorithmHolder {
+        private static GeneticAlgorithm instance = new GeneticAlgorithm();
+    }
+
+    /**
+     * 点集间的邻接矩阵
+     *
+     * @param matrix
+     * @return
+     */
+    public int[] tsp(float[][] matrix) {
+        this.dist = matrix;
+        pointNum = matrix.length;
+        init();
+
+        if (isAutoNextGeneration) {
+            int i = 0;
+            while (i++ < maxGeneration) {
+                nextGeneration();
+            }
+        }
+        isAutoNextGeneration = false;
+
+        return getBestIndivial();
+    }
+
+    /**
+     * 初始化
+     */
+    private void init() {
+        mutationTimes = 0;
+        currentGeneration = 0;
+        bestIndivial = null;
+        bestDist = 0;
+        currentBestPosition = 0;
+        currentBestDist = 0;
+
+        values = new float[populationSize];
+        fitnessValues = new float[populationSize];
+        roulette = new float[populationSize];
+        population = new int[populationSize][pointNum];
+
+        //initDist(points);
+        // 父代
+        for (int i = 0; i < populationSize; i++) {
+            population[i] = randomIndivial(pointNum);
+        }
+        evaluateBestIndivial();
+    }
+
+    /**
+     * 下一代
+     */
+    public int[] nextGeneration() {
+        currentGeneration++;
+
+        // 选择
+        selection();
+        // 交叉
+        crossover();
+        // 变异
+        mutation();
+        // 评价最好
+        evaluateBestIndivial();
+
+        return getBestIndivial();
+    }
+
+    /**
+     * 选择
+     */
+    private void selection() {
+        int[][] parents = new int[populationSize][pointNum];
+
+        int initnum = 4;
+        parents[0] = population[currentBestPosition]; // 当前种群中最好的个体
+        parents[1] = exchangeMutate(bestIndivial.clone()); // 对最好的个体进行交换变异
+        parents[2] = insertMutate(bestIndivial.clone()); // 对最好的个体进行插入变异
+        parents[3] = bestIndivial.clone(); // 所有代中最好的个体
+
+        setRoulette();
+        for (int i = initnum; i < populationSize; i++) {
+            parents[i] = population[wheelOut((int) Math.random())];
+        }
+        population = parents;
+    }
+
+    /**
+     *
+     */
+    private void setRoulette() {
+        //calculate all the fitness
+        for (int i = 0; i < values.length; i++) {
+            fitnessValues[i] = 1.0f / values[i]; // 适应度为路径长的导数
+        }
+
+        //set the roulette
+        float sum = 0;
+        for (int i = 0; i < fitnessValues.length; i++) {
+            sum += fitnessValues[i];
+        }
+        for (int i = 0; i < roulette.length; i++) {
+            roulette[i] = fitnessValues[i] / sum;
+        }
+        for (int i = 1; i < roulette.length; i++) {
+            roulette[i] += roulette[i - 1];
+        }
+    }
+
+    /**
+     * 模拟转盘,进行子代选取
+     *
+     * @param ran
+     * @return
+     */
+    private int wheelOut(int ran) {
+        for (int i = 0; i < roulette.length; i++) {
+            if (ran <= roulette[i]) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+
+    /**
+     * 交换变异
+     *
+     * @param seq
+     * @return
+     */
+    private int[] exchangeMutate(int[] seq) {
+        mutationTimes++;
+        int m, n;
+        do {
+            m = random(seq.length - 2);
+            n = random(seq.length);
+        } while (m >= n);
+
+        int j = (n - m + 1) >> 1;
+        for (int i = 0; i < j; i++) {
+            int tmp = seq[m + i];
+            seq[m + i] = seq[n - i];
+            seq[n - i] = tmp;
+        }
+        return seq;
+    }
+
+    /**
+     * 插入变异
+     *
+     * @param seq
+     * @return
+     */
+    private int[] insertMutate(int[] seq) {
+        mutationTimes++;
+        int m, n;
+        do {
+            m = random(seq.length >> 1);
+            n = random(seq.length);
+        } while (m >= n);
+
+        int[] s1 = Arrays.copyOfRange(seq, 0, m);
+        int[] s2 = Arrays.copyOfRange(seq, m, n);
+
+        for (int i = 0; i < m; i++) {
+            seq[i + n - m] = s1[i];
+        }
+        for (int i = 0; i < n - m; i++) {
+            seq[i] = s2[i];
+        }
+        return seq;
+    }
+
+    /**
+     * 交叉
+     */
+    private void crossover() {
+        int[] queue = new int[populationSize];
+        int num = 0;
+        for (int i = 0; i < populationSize; i++) {
+            if (Math.random() < crossoverProbability) {
+                queue[num] = i;
+                num++;
+            }
+        }
+        queue = Arrays.copyOfRange(queue, 0, num);
+        queue = shuffle(queue);
+        for (int i = 0; i < num - 1; i += 2) {
+            doCrossover(queue[i], queue[i + 1]);
+        }
+    }
+
+    private void doCrossover(int x, int y) {
+        population[x] = getChild(x, y, PREVIOUS);
+        population[y] = getChild(x, y, NEXT);
+    }
+
+    /**
+     * 根据父代求子代
+     *
+     * @param x
+     * @param y
+     * @param pos
+     * @return
+     */
+    private int[] getChild(int x, int y, int pos) {
+        int[] solution = new int[pointNum];
+        int[] px = population[x].clone();
+        int[] py = population[y].clone();
+
+        int dx = 0, dy = 0;
+        int c = px[random(px.length)];
+        solution[0] = c;
+
+        for (int i = 1; i < pointNum; i++) {
+            int posX = indexOf(px, c);
+            int posY = indexOf(py, c);
+
+            if (pos == PREVIOUS) {
+                dx = px[(posX + px.length - 1) % px.length];
+                dy = py[(posY + py.length - 1) % py.length];
+            } else if (pos == NEXT) {
+                dx = px[(posX + px.length + 1) % px.length];
+                dy = py[(posY + py.length + 1) % py.length];
+            }
+
+            for (int j = posX; j < px.length - 1; j++) {
+                px[j] = px[j + 1];
+            }
+            px = Arrays.copyOfRange(px, 0, px.length - 1);
+            for (int j = posY; j < py.length - 1; j++) {
+                py[j] = py[j + 1];
+            }
+            py = Arrays.copyOfRange(py, 0, py.length - 1);
+
+            c = dist[c][dx] < dist[c][dy] ? dx : dy;
+
+            solution[i] = c;
+        }
+        return solution;
+    }
+
+    /**
+     * 变异
+     */
+    private void mutation() {
+        for (int i = 0; i < populationSize; i++) {
+            if (Math.random() < mutationProbability) {
+                if (Math.random() > 0.5) {
+                    population[i] = insertMutate(population[i]);
+                } else {
+                    population[i] = exchangeMutate(population[i]);
+                }
+                i--;
+            }
+        }
+    }
+
+    /**
+     * 评估最好的个体
+     */
+    private void evaluateBestIndivial() {
+        for (int i = 0; i < population.length; i++) {
+            values[i] = calculateIndivialDist(population[i]);
+        }
+        evaluateBestCurrentDist();
+        if (bestDist == 0 || bestDist > currentBestDist) {
+            bestDist = currentBestDist;
+            bestIndivial = population[currentBestPosition].clone();
+        }
+    }
+
+    /**
+     * 计算个体的距离
+     *
+     * @return
+     */
+    private float calculateIndivialDist(int[] indivial) {
+        float sum = dist[indivial[0]][indivial[indivial.length - 1]];
+        for (int i = 1; i < indivial.length; i++) {
+            sum += dist[indivial[i]][indivial[i - 1]];
+        }
+        return sum;
+    }
+
+    /**
+     * 评估得到最短距离
+     */
+    public void evaluateBestCurrentDist() {
+        currentBestDist = values[0];
+        for (int i = 1; i < populationSize; i++) {
+            if (values[i] < currentBestDist) {
+                currentBestDist = values[i];
+                currentBestPosition = i;
+            }
+        }
+    }
+
+
+    /**
+     * 产生个体(乱序)
+     *
+     * @param n
+     * @return
+     */
+    private int[] randomIndivial(int n) {
+        int[] a = new int[n];
+        for (int i = 0; i < n; i++) {
+            a[i] = i;
+        }
+
+        return shuffle(a);
+    }
+
+    /**
+     * 乱序处理
+     *
+     * @param a
+     * @return
+     */
+    private int[] shuffle(int[] a) {
+        for (int i = 0; i < a.length; i++) {
+            int p = random(a.length);
+            int tmp = a[i];
+            a[i] = a[p];
+            a[p] = tmp;
+        }
+        return a;
+    }
+
+    private int random(int n) {
+        Random ran = rd;
+        if (ran == null) {
+            ran = new Random();
+        }
+        return ran.nextInt(n);
+    }
+
+    private int[] concatAllArray(int[] first, int[]... rest) {
+        int totalLength = first.length;
+        for (int[] array : rest) {
+            totalLength += array.length;
+        }
+        int[] result = Arrays.copyOf(first, totalLength);
+        int offset = first.length;
+        for (int[] array : rest) {
+            System.arraycopy(array, 0, result, offset, array.length);
+            offset += array.length;
+        }
+        return result;
+    }
+
+    private int indexOf(int[] a, int index) {
+        for (int i = 0; i < a.length; i++) {
+            if (a[i] == index) {
+                return i;
+            }
+        }
+        return 0;
+    }
+
+    public int[] getBestIndivial() {
+        int[] best = new int[bestIndivial.length];
+        int pos = indexOf(bestIndivial, 0);
+
+        for (int i = 0; i < best.length; i++) {
+            best[i] = bestIndivial[(i + pos) % bestIndivial.length];
+        }
+        return best;
+    }
+
+    public float getBestDist() {
+        return bestDist;
+    }
+
+    public void setMaxGeneration(int maxGeneration) {
+        this.maxGeneration = maxGeneration;
+    }
+
+    public void setAutoNextGeneration(boolean autoNextGeneration) {
+        isAutoNextGeneration = autoNextGeneration;
+    }
+
+    public int getMutationTimes() {
+        return mutationTimes;
+    }
+
+    public int getCurrentGeneration() {
+        return currentGeneration;
+    }
+}

+ 63 - 0
app/src/main/java/com/onlylemi/mapview/library/utils/math/TSPNearestNeighbour.java

@@ -0,0 +1,63 @@
+package com.onlylemi.mapview.library.utils.math;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.List;
+
+public class TSPNearestNeighbour {
+
+    private static final float INF = Float.MAX_VALUE;
+
+    private int numberOfNodes;
+    private Deque<Integer> stack;
+    private List<Integer> list;
+
+    public TSPNearestNeighbour() {
+        stack = new ArrayDeque<>();
+        list = new ArrayList<>();
+    }
+
+    public static TSPNearestNeighbour getInstance() {
+        return TSPNearestNeighbourHolder.instance;
+    }
+    private static class TSPNearestNeighbourHolder {
+        private static TSPNearestNeighbour instance = new TSPNearestNeighbour();
+
+    }
+
+    public List<Integer> tsp(float[][] matrix) {
+        numberOfNodes = matrix[0].length;
+        int[] visited = new int[numberOfNodes];
+        visited[0] = 1;
+        stack.push(0);
+        int element, dst = 0, i;
+        boolean minFlag = false;
+
+        // System.out.print(0 + "\t");
+        list.add(0);
+        while (!stack.isEmpty()) {
+            element = stack.peek();
+            i = 0;
+            float min = INF;
+            while (i < numberOfNodes) {
+                if (matrix[element][i] < INF && visited[i] == 0 && min > matrix[element][i]) {
+                    min = matrix[element][i];
+                    dst = i;
+                    minFlag = true;
+                }
+                i++;
+            }
+            if (minFlag) {
+                visited[dst] = 1;
+                stack.push(dst);
+                // System.out.print(dst + "\t");
+                list.add(dst);
+                minFlag = false;
+                continue;
+            }
+            stack.pop();
+        }
+        return list;
+    }
+}

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

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <stroke
+        android:width="1dp"
+        android:color="@color/color_d7d2d2" />
+    <corners android:radius="6dp" />
+</shape>

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

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

+ 16 - 0
app/src/main/res/layout/dialog_face_capture.xml

@@ -1,6 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:gravity="center"
@@ -74,6 +75,7 @@
                 android:layout_marginHorizontal="@dimen/common_spacing_small"
                 android:layout_weight="1"
                 android:background="@drawable/face_capture_tip_bg">
+
                 <TextureView
                     android:id="@+id/preview"
                     android:layout_width="match_parent"
@@ -85,6 +87,7 @@
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:scaleType="centerCrop" />
+
             </FrameLayout>
 
             <LinearLayout
@@ -122,6 +125,19 @@
                     android:text="@string/capture_tip_content"
                     android:textColor="@color/black"
                     android:textSize="@dimen/common_text_size_small" />
+
+                <TextView
+                    android:id="@+id/tip_tv"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="10dp"
+                    android:background="@color/common_status_red"
+                    android:gravity="center"
+                    android:text="@string/only_one_person_allowed"
+                    android:textColor="@color/white"
+                    android:textSize="@dimen/common_text_size_big"
+                    android:visibility="gone"
+                    tools:text="请保证画面中只有自己" />
             </LinearLayout>
         </LinearLayout>
     </RelativeLayout>

+ 13 - 0
app/src/main/res/layout/fragment_device_registration_home.xml

@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <androidx.fragment.app.FragmentContainerView
+        android:id="@+id/nav_host_fragment"
+        android:name="androidx.navigation.fragment.NavHostFragment"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        app:defaultNavHost="true"
+        app:navGraph="@navigation/nav_device_input" />
+
+</layout>

+ 66 - 0
app/src/main/res/layout/fragment_device_registration_key_and_lock.xml

@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="@drawable/item_rv_technology_sop_bg_normal"
+        android:padding="@dimen/common_spacing"
+        tools:context=".view.fragment.DeviceStatusFragment">
+
+        <LinearLayout
+            android:id="@+id/rv_dock_layout"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_above="@+id/cb_back"
+            android:layout_marginBottom="10dp"
+            android:orientation="vertical"
+            android:visibility="gone">
+
+            <TextView
+                android:id="@+id/scan_result_tip"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:textColor="@color/color_d7d2d2"
+                android:textSize="@dimen/device_registration_text_size" />
+
+            <androidx.recyclerview.widget.RecyclerView
+                android:id="@+id/rv_dock"
+                style="@style/CommonRecyclerView"
+                android:layout_above="@+id/cb_back"
+                android:layout_marginBottom="@dimen/common_spacing" />
+        </LinearLayout>
+
+        <TextView
+            android:id="@+id/scan_tip"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:gravity="center"
+            android:text="@string/device_registration_scan_key_and_lock_tip"
+            android:textColor="@color/color_d7d2d2"
+            android:textSize="@dimen/device_registration_text_size" />
+
+        <com.grkj.iscs.view.widget.CommonBtn
+            android:id="@+id/cb_rescan_or_input"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_marginRight="@dimen/common_spacing"
+            android:layout_toLeftOf="@+id/cb_back"
+            android:visibility="gone"
+            app:btn_bg="@drawable/common_btn_blue_bg"
+            app:btn_name="@string/rescan" />
+
+        <com.grkj.iscs.view.widget.CommonBtn
+            android:id="@+id/cb_back"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentBottom="true"
+            app:btn_bg="@drawable/common_btn_red_bg"
+            app:btn_name="@string/back" />
+    </RelativeLayout>
+</layout>

+ 27 - 0
app/src/main/res/layout/fragment_device_registration_scan.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+        <TextView
+            android:id="@+id/input_tip"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_centerInParent="true"
+            android:gravity="center"
+            android:textColor="@color/color_d7d2d2"
+            android:textSize="@dimen/device_registration_text_size" />
+
+        <com.grkj.iscs.view.widget.CommonBtn
+            android:id="@+id/cb_back"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentRight="true"
+            android:layout_alignParentBottom="true"
+            app:btn_bg="@drawable/common_btn_red_bg"
+            app:btn_name="@string/back" />
+    </RelativeLayout>
+</layout>

+ 85 - 0
app/src/main/res/layout/fragment_device_registration_type_select.xml

@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:divider="@drawable/divider_horizontal"
+        android:gravity="center_vertical"
+        android:orientation="horizontal"
+        android:paddingHorizontal="@dimen/device_registration_padding_horizontal"
+        android:showDividers="middle">
+
+        <LinearLayout
+            android:id="@+id/key_lock_scan_layout"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@drawable/bg_device_registration_type_select"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="@dimen/device_registration_common_space">
+
+            <ImageView
+                android:layout_width="@dimen/device_registration_common_icon_width"
+                android:layout_height="@dimen/device_registration_common_icon_height"
+                android:src="@mipmap/icon_key_lock_scan" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/device_registration_common_space"
+                android:text="@string/scan_key_and_lock"
+                android:textColor="@color/color_d7d2d2"
+                android:textSize="@dimen/device_registration_common_text_size" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/card_input_layout"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@drawable/bg_device_registration_type_select"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="@dimen/device_registration_common_space">
+
+            <ImageView
+                android:layout_width="@dimen/device_registration_common_icon_width"
+                android:layout_height="@dimen/device_registration_common_icon_height"
+                android:src="@mipmap/icon_card_input" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/device_registration_common_space"
+                android:text="@string/card_registration"
+                android:textColor="@color/color_d7d2d2"
+                android:textSize="@dimen/device_registration_common_text_size" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/rfid_input_layout"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:background="@drawable/bg_device_registration_type_select"
+            android:gravity="center"
+            android:orientation="vertical"
+            android:padding="@dimen/device_registration_common_space">
+
+            <ImageView
+                android:layout_width="@dimen/device_registration_common_icon_width"
+                android:layout_height="@dimen/device_registration_common_icon_height"
+                android:src="@mipmap/icon_rfid_input" />
+
+            <TextView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="@dimen/device_registration_common_space"
+                android:text="@string/rfid_tag_registration"
+                android:textColor="@color/color_d7d2d2"
+                android:textSize="@dimen/device_registration_common_text_size" />
+        </LinearLayout>
+    </LinearLayout>
+</layout>

+ 1 - 0
app/src/main/res/layout/fragment_face_config.xml

@@ -32,6 +32,7 @@
             app:btn_text_color="@color/black" />
 
         <TextView
+            android:id="@+id/face_limit_tv"
             style="@style/CommonTextView"
             android:layout_marginLeft="@dimen/common_spacing"
             android:text="@string/face_config_tip" />

+ 1 - 0
app/src/main/res/layout/fragment_fingerprint_config.xml

@@ -32,6 +32,7 @@
             app:btn_text_color="@color/black" />
 
         <TextView
+            android:id="@+id/fingerprint_limit_tv"
             style="@style/CommonTextView"
             android:layout_marginLeft="@dimen/common_spacing"
             android:text="@string/fingerprint_config_tip" />

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

@@ -162,7 +162,7 @@
                     android:layout_width="match_parent"
                     android:layout_height="match_parent"
                     android:layout_above="@id/rv_statistics"
-                    android:layout_marginTop="@dimen/common_spacing_small" />
+                    android:layout_marginTop="@dimen/common_spacing_small"/>
             </LinearLayout>
         </LinearLayout>
 

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

@@ -10,5 +10,5 @@
     <com.onlylemi.mapview.library.MapView
         android:id="@+id/mapview"
         android:layout_width="match_parent"
-        android:layout_height="match_parent" />
+        android:layout_height="match_parent"/>
 </RelativeLayout>

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

@@ -29,13 +29,14 @@
         <TextView
             android:id="@+id/tv_turn_off"
             style="@style/CommonTextView"
-            android:layout_marginRight="@dimen/common_spacing_small"
             android:background="@color/main_color_dark"
             android:padding="2dp"
             android:text="@string/turn_off" />
         <TextView
             android:id="@+id/tv_read"
             style="@style/CommonTextView"
+            android:layout_marginLeft="@dimen/common_spacing_small"
+            android:visibility="gone"
             android:background="@color/main_color_dark"
             android:padding="2dp"
             android:text="@string/turn_read" />

+ 194 - 0
app/src/main/res/layout/item_rv_key_dock_device_registration.xml

@@ -0,0 +1,194 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/root"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_marginHorizontal="@dimen/common_spacing_small"
+    android:layout_marginVertical="@dimen/common_spacing_smallest"
+    android:orientation="horizontal"
+    android:padding="@dimen/common_spacing_small">
+
+    <LinearLayout
+        android:id="@+id/ll_left"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1">
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/iv_key_1"
+                android:layout_width="50dp"
+                android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
+                android:background="@drawable/dock_key_selector" />
+
+            <TextView
+                android:id="@+id/tv_new_device_1"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/iv_key_1"
+                android:layout_alignLeft="@+id/iv_key_1"
+                android:layout_alignRight="@+id/iv_key_1"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_red_bg"
+                android:text="@string/new_device"
+                android:visibility="invisible" />
+
+            <TextView
+                android:id="@+id/tv_new_device_mac_1"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/tv_new_device_1"
+                android:layout_alignLeft="@+id/iv_key_1"
+                android:layout_alignRight="@+id/iv_key_1"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_green_bg"
+                android:visibility="invisible" />
+
+            <View
+                android:id="@+id/v_buckle_status_1"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_1"
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/iv_key_2"
+                android:layout_width="50dp"
+                android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
+                android:background="@drawable/dock_key_selector" />
+
+            <TextView
+                android:id="@+id/tv_new_device_2"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/iv_key_2"
+                android:layout_alignLeft="@+id/iv_key_2"
+                android:layout_alignRight="@+id/iv_key_2"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_red_bg"
+                android:text="@string/new_device"
+                android:visibility="invisible" />
+
+            <TextView
+                android:id="@+id/tv_new_device_mac_2"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/tv_new_device_2"
+                android:layout_alignLeft="@+id/iv_key_2"
+                android:layout_alignRight="@+id/iv_key_2"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_green_bg"
+                android:visibility="invisible" />
+
+            <View
+                android:id="@+id/v_buckle_status_2"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_2"
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+        </RelativeLayout>
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/ll_right"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_weight="1"
+        android:orientation="horizontal">
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/iv_key_3"
+                android:layout_width="50dp"
+                android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
+                android:background="@drawable/dock_key_selector" />
+
+            <TextView
+                android:id="@+id/tv_new_device_3"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/iv_key_3"
+                android:layout_alignLeft="@+id/iv_key_3"
+                android:layout_alignRight="@+id/iv_key_3"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_red_bg"
+                android:text="@string/new_device"
+                android:visibility="invisible" />
+
+            <TextView
+                android:id="@+id/tv_new_device_mac_3"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/tv_new_device_3"
+                android:layout_alignLeft="@+id/iv_key_3"
+                android:layout_alignRight="@+id/iv_key_3"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_green_bg"
+                android:visibility="invisible" />
+
+            <View
+                android:id="@+id/v_buckle_status_3"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_3"
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+        </RelativeLayout>
+
+        <RelativeLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1">
+
+            <ImageView
+                android:id="@+id/iv_key_4"
+                android:layout_width="50dp"
+                android:layout_height="35dp"
+                android:layout_centerHorizontal="true"
+                android:background="@drawable/dock_key_selector" />
+
+            <TextView
+                android:id="@+id/tv_new_device_4"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/iv_key_4"
+                android:layout_alignLeft="@+id/iv_key_4"
+                android:layout_alignRight="@+id/iv_key_4"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_red_bg"
+                android:text="@string/new_device"
+                android:visibility="invisible" />
+
+            <TextView
+                android:id="@+id/tv_new_device_mac_4"
+                style="@style/CommonTextView"
+                android:layout_below="@+id/tv_new_device_4"
+                android:layout_alignLeft="@+id/iv_key_4"
+                android:layout_alignRight="@+id/iv_key_4"
+                android:layout_marginTop="5dp"
+                android:background="@drawable/common_btn_green_bg"
+                android:visibility="invisible" />
+
+            <View
+                android:id="@+id/v_buckle_status_4"
+                android:layout_width="@dimen/common_status_circle_small"
+                android:layout_height="@dimen/common_status_circle_small"
+                android:layout_toRightOf="@+id/iv_key_4"
+                android:background="@drawable/common_status_circle"
+                android:visibility="invisible" />
+
+        </RelativeLayout>
+    </LinearLayout>
+</LinearLayout>

+ 52 - 0
app/src/main/res/layout/item_rv_lock_dock_child_device_registration.xml

@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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"
+    android:orientation="horizontal">
+
+    <FrameLayout
+        android:id="@+id/root"
+        android:layout_width="20dp"
+        android:layout_height="70dp"
+        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"
+        android:showDividers="middle"
+        android:visibility="invisible">
+
+        <View
+            android:id="@+id/v_buckle_status_close"
+            android:layout_width="@dimen/common_status_circle_medium"
+            android:layout_height="@dimen/common_status_circle_medium"
+            android:background="@drawable/common_status_circle" />
+
+        <View
+            android:id="@+id/v_buckle_status_open"
+            android:layout_width="@dimen/common_status_circle_medium"
+            android:layout_height="@dimen/common_status_circle_medium"
+            android:background="@drawable/common_status_circle" />
+    </LinearLayout>
+
+    <TextView
+        android:id="@+id/tv_new_device"
+        style="@style/CommonTextView"
+        android:layout_below="@+id/root"
+        android:layout_alignLeft="@+id/root"
+        android:layout_alignRight="@+id/root"
+        android:layout_marginTop="5dp"
+        android:background="@drawable/common_btn_red_bg"
+        android:text="@string/new_device"
+        android:visibility="invisible" />
+</RelativeLayout>
+

BIN
app/src/main/res/mipmap-hdpi/end_point.png


BIN
app/src/main/res/mipmap-hdpi/mark_touch.png


BIN
app/src/main/res/mipmap-hdpi/start_point.png


BIN
app/src/main/res/mipmap-xhdpi/compass.png


BIN
app/src/main/res/mipmap-xhdpi/icon_card_input.png


BIN
app/src/main/res/mipmap-xhdpi/icon_key_lock_scan.png


BIN
app/src/main/res/mipmap-xhdpi/icon_rfid_input.png


BIN
app/src/main/res/mipmap-xhdpi/mark.png


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


+ 26 - 0
app/src/main/res/navigation/nav_device_input.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<navigation xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/nav_device_input"
+    app:startDestination="@id/deviceInputTypeSelectFragment">
+
+    <fragment
+        android:id="@+id/deviceInputTypeSelectFragment"
+        android:name="com.grkj.iscs.view.fragment.DeviceRegistrationTypeSelectFragment"
+        android:label="DeviceInputTypeSelectFragment" >
+        <action
+            android:id="@+id/action_deviceInputTypeSelectFragment_to_deviceInputKeyAndLockFragment"
+            app:destination="@id/deviceInputKeyAndLockFragment" />
+        <action
+            android:id="@+id/action_deviceInputTypeSelectFragment_to_deviceInputScanFragment"
+            app:destination="@id/deviceInputScanFragment" />
+    </fragment>
+    <fragment
+        android:id="@+id/deviceInputKeyAndLockFragment"
+        android:name="com.grkj.iscs.view.fragment.DeviceRegistrationKeyAndLockFragment"
+        android:label="DeviceInputKeyAndLockFragment" />
+    <fragment
+        android:id="@+id/deviceInputScanFragment"
+        android:name="com.grkj.iscs.view.fragment.DeviceRegistrationScanFragment"
+        android:label="DeviceInputScanFragment" />
+</navigation>

+ 31 - 3
app/src/main/res/values-en/strings.xml

@@ -224,7 +224,7 @@
     <string name="fingerprint_config">Fingerprint Settings</string>
     <string name="face_config">Face Settings</string>
     <string name="add_fingerprint">Add Fingerprint</string>
-    <string name="fingerprint_config_tip">Up to 3 fingerprints can be added</string>
+    <string name="fingerprint_config_tip">Up to %1$d fingerprints can be added</string>
     <string name="fingerprint">Fingerprint</string>
     <string name="fingerprint_delete_confirm_tip">Confirm to delete %s?</string>
     <string name="fingerprint_scan_tip">Please press fingerprint sensor</string>
@@ -233,7 +233,7 @@
     <string name="exception_level_tip">Please select severity level</string>
     <string name="exception_submit_success_tip">Exception submitted successfully</string>
     <string name="add_face">Add face record</string>
-    <string name="face_config_tip">Up to 1 face can be registered</string>
+    <string name="face_config_tip">Up to %1$d face can be registered</string>
     <string name="recapture">Retake</string>
     <string name="capture_tip_title">Registration Tips</string>
     <string name="capture_tip_content">1. The system will automatically take photos. Please ensure:
@@ -337,7 +337,7 @@
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">The current user does not have the authority to unlock colock</string>
     <string name="all_point_have_other_job_not_finish">all point have other job not finish,do you want to continue?</string>
     <string name="lock_is_not_enough_stop_issue_ticket">lock is not enough, stop issue ticket</string>
-    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="ticket_lost">作业票数据丢失啦!</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>
@@ -354,4 +354,32 @@
     <string name="exception_select_hardware_tip">Please select hardware</string>
     <string name="check_key_and_lock">check key and lock</string>
     <string name="turn_read">read</string>
+    <string name="lock_nfc_lost">挂锁nfc缺失!</string>
+    <string name="key_exception_tag">Key is tag for exception</string>
+    <string name="slot_exception_tag">Slot is tag for exception</string>
+    <string name="lock_exception_tag">Lock is tag for exception</string>
+    <string name="job_already_finished">job already finished</string>
+    <string name="ticket_data_error">job ticket data error</string>
+    <string name="card_invalid">This card is invalid</string>
+    <string name="lock_nfc_not_correct">该nfc无对应的挂锁信息</string>
+    <string name="only_one_person_allowed">Only one person allowed</string>
+    <string name="real_person_verification_required">Real-person verification required</string>
+    <string name="device_registration">Hardware registration</string>
+    <string name="scan_key_and_lock">scan key and lock</string>
+    <string name="card_registration">card enter</string>
+    <string name="rfid_tag_registration">rfid tag enter</string>
+    <string name="device_registration_scan_key_and_lock_tip">wait to scan new key and lock...</string>
+    <string name="device_registration_scan_card_tip">please read card on card reader</string>
+    <string name="device_registration_scan_rfid_tip">please read RFID on card reader</string>
+    <string name="device_registration_recognize_tip">recognize %1$s\"%2$s\",do you want to entered system?</string>
+    <string name="card">Card</string>
+    <string name="rfid">RFID</string>
+    <string name="device_registration_success_tip">%1$s\"%2$s\"has been entered into the system.</string>
+    <string name="device_registration_fail_tip">The %1$s is already in the system and does not need to be entered.</string>
+    <string name="new_device">New</string>
+    <string name="rescan">Rescan</string>
+    <string name="registration_to_system">Enter to system</string>
+    <string name="already_registration_device_tip">%1$d keys and %2$d padlocks have been entered</string>
+    <string name="registration_device_error">entered error</string>
+    <string name="device_registration_scan_result_tip">Identified unrecorded keys (%1$d) and padlocks (%2$d).</string>
 </resources>

+ 31 - 3
app/src/main/res/values-zh/strings.xml

@@ -224,7 +224,7 @@
     <string name="fingerprint_config">指纹设置</string>
     <string name="face_config">人脸设置</string>
     <string name="add_fingerprint">添加指纹</string>
-    <string name="fingerprint_config_tip">最多可以添加3个指纹数据</string>
+    <string name="fingerprint_config_tip">最多可以添加%1$d个指纹数据</string>
     <string name="fingerprint">指纹</string>
     <string name="fingerprint_delete_confirm_tip">确定要删除%s吗?</string>
     <string name="fingerprint_scan_tip">请按压指纹识别区</string>
@@ -233,7 +233,7 @@
     <string name="exception_level_tip">请选择异常等级</string>
     <string name="exception_submit_success_tip">异常提交成功</string>
     <string name="add_face">录入人脸</string>
-    <string name="face_config_tip">最多可以录入1组人脸数据</string>
+    <string name="face_config_tip">最多可以录入%1$d组人脸数据</string>
     <string name="recapture">重拍</string>
     <string name="capture_tip_title">录入提示</string>
     <string name="capture_tip_content">1. 系统将自动拍摄照片,在拍摄过程中请确保:
@@ -337,7 +337,7 @@
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">当前用户无权解除共锁</string>
     <string name="all_point_have_other_job_not_finish">所有隔离点存在其他作业未完成,是否继续拆锁恢复?</string>
     <string name="lock_is_not_enough_stop_issue_ticket">锁具数量不足,停止下发作业票</string>
-    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="ticket_lost">作业票数据丢失啦!</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>
@@ -354,4 +354,32 @@
     <string name="exception_select_hardware_tip">请选择硬件</string>
     <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
     <string name="turn_read">读</string>
+    <string name="lock_nfc_lost">挂锁nfc缺失!</string>
+    <string name="key_exception_tag">该钥匙已被标记异常</string>
+    <string name="slot_exception_tag">该锁仓已被标记异常</string>
+    <string name="lock_exception_tag">该挂锁已被标记异常</string>
+    <string name="job_already_finished">该作业已被结束</string>
+    <string name="ticket_data_error">工作票数据损坏</string>
+    <string name="card_invalid">该卡无效</string>
+    <string name="lock_nfc_not_correct">该nfc无对应的挂锁信息</string>
+    <string name="only_one_person_allowed">请保持单人入镜</string>
+    <string name="real_person_verification_required">请保持真人操作</string>
+    <string name="device_registration">硬件录入</string>
+    <string name="scan_key_and_lock">扫描钥匙挂锁</string>
+    <string name="card_registration">录入卡片</string>
+    <string name="rfid_tag_registration">录入RFID标签</string>
+    <string name="device_registration_scan_key_and_lock_tip">等待扫描新的钥匙和挂锁...</string>
+    <string name="device_registration_scan_card_tip">请在读卡器上刷卡</string>
+    <string name="device_registration_scan_rfid_tip">请在读卡器上读取RFID</string>
+    <string name="device_registration_recognize_tip">已识别%1$s\"%2$s\",确定要录入系统吗?</string>
+    <string name="card">卡片</string>
+    <string name="rfid">RFID</string>
+    <string name="device_registration_success_tip">%1$s\"%2$s\"已录入系统。</string>
+    <string name="device_registration_fail_tip">该%1$s已在系统中,无需录入。</string>
+    <string name="new_device">New</string>
+    <string name="rescan">重新扫描</string>
+    <string name="registration_to_system">录入系统</string>
+    <string name="already_registration_device_tip">已录入%1$d把钥匙与%2$d把挂锁</string>
+    <string name="registration_device_error">录入失败</string>
+    <string name="device_registration_scan_result_tip">已识别未录入的钥匙 (%1$d把)与挂锁(%2$d把)。</string>
 </resources>

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

@@ -43,4 +43,5 @@
     <color name="item_rv_step_bg_ready">#B3FFFFFF</color>
     <color name="common_switch_enable">#91ce93</color>
     <color name="common_switch_disable">#f0f0f0</color>
+    <color name="color_d7d2d2">#d7d2d2</color>
 </resources>

+ 6 - 0
app/src/main/res/values/dimens.xml

@@ -85,4 +85,10 @@
     <dimen name="item_rv_login_height">130dp</dimen>
     <dimen name="item_rv_login_margin">7dp</dimen>
     <dimen name="item_rv_login_icon_size">40dp</dimen>
+    <dimen name="device_registration_text_size">18sp</dimen>
+    <dimen name="device_registration_padding_horizontal">28dp</dimen>
+    <dimen name="device_registration_common_space">5dp</dimen>
+    <dimen name="device_registration_common_icon_width">111dp</dimen>
+    <dimen name="device_registration_common_icon_height">49dp</dimen>
+    <dimen name="device_registration_common_text_size">12sp</dimen>
 </resources>

+ 31 - 3
app/src/main/res/values/strings.xml

@@ -224,7 +224,7 @@
     <string name="fingerprint_config">指纹设置</string>
     <string name="face_config">人脸设置</string>
     <string name="add_fingerprint">添加指纹</string>
-    <string name="fingerprint_config_tip">最多可以添加3个指纹数据</string>
+    <string name="fingerprint_config_tip">最多可以添加%1$d个指纹数据</string>
     <string name="fingerprint">指纹</string>
     <string name="fingerprint_delete_confirm_tip">确定要删除%s吗?</string>
     <string name="fingerprint_scan_tip">请按压指纹识别区</string>
@@ -233,7 +233,7 @@
     <string name="exception_level_tip">请选择异常等级</string>
     <string name="exception_submit_success_tip">异常提交成功</string>
     <string name="add_face">录入人脸</string>
-    <string name="face_config_tip">最多可以录入1组人脸数据</string>
+    <string name="face_config_tip">最多可以录入%1$d组人脸数据</string>
     <string name="recapture">重拍</string>
     <string name="capture_tip_title">录入提示</string>
     <string name="capture_tip_content">1. 系统将自动拍摄照片,在拍摄过程中请确保:
@@ -337,7 +337,7 @@
     <string name="current_user_does_not_have_the_authority_to_unlock_colock">当前用户无权解除共锁</string>
     <string name="all_point_have_other_job_not_finish">所有隔离点存在其他作业未完成,是否继续拆锁恢复?</string>
     <string name="lock_is_not_enough_stop_issue_ticket">锁具数量不足,停止下发作业票</string>
-    <string name="ticket_lost">{"msg":"作业票数据丢失啦!","code":500}</string>
+    <string name="ticket_lost">作业票数据丢失啦!</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>
@@ -354,4 +354,32 @@
     <string name="exception_select_hardware_tip">请选择硬件</string>
     <string name="check_key_and_lock">正在检查钥匙和挂锁</string>
     <string name="turn_read">读</string>
+    <string name="lock_nfc_lost">挂锁nfc缺失!</string>
+    <string name="key_exception_tag">该钥匙已被标记异常</string>
+    <string name="slot_exception_tag">该锁仓已被标记异常</string>
+    <string name="lock_exception_tag">该挂锁已被标记异常</string>
+    <string name="job_already_finished">该作业已被结束</string>
+    <string name="ticket_data_error">工作票数据损坏</string>
+    <string name="card_invalid">该卡无效</string>
+    <string name="lock_nfc_not_correct">该nfc无对应的挂锁信息</string>
+    <string name="only_one_person_allowed">请保持单人入镜</string>
+    <string name="real_person_verification_required">请保持真人操作</string>
+    <string name="device_registration">硬件录入</string>
+    <string name="scan_key_and_lock">扫描钥匙挂锁</string>
+    <string name="card_registration">录入卡片</string>
+    <string name="rfid_tag_registration">录入RFID标签</string>
+    <string name="device_registration_scan_key_and_lock_tip">等待扫描新的钥匙和挂锁...</string>
+    <string name="device_registration_scan_card_tip">请在读卡器上刷卡</string>
+    <string name="device_registration_scan_rfid_tip">请在读卡器上读取RFID</string>
+    <string name="device_registration_recognize_tip">已识别%1$s\"%2$s\",确定要录入系统吗?</string>
+    <string name="card">卡片</string>
+    <string name="rfid">RFID</string>
+    <string name="device_registration_success_tip">%1$s\"%2$s\"已录入系统。</string>
+    <string name="device_registration_fail_tip">该%1$s已在系统中,无需录入。</string>
+    <string name="new_device">New</string>
+    <string name="rescan">重新扫描</string>
+    <string name="registration_to_system">录入系统</string>
+    <string name="already_registration_device_tip">已录入%1$d把钥匙与%2$d把挂锁</string>
+    <string name="registration_device_error">录入失败</string>
+    <string name="device_registration_scan_result_tip">已识别未录入的钥匙 (%1$d把)与挂锁(%2$d把)。</string>
 </resources>

+ 7 - 0
gradle/libs.versions.toml

@@ -15,6 +15,8 @@ log-interceptor = "3.14.9"
 autosize = "v1.2.1"
 fastble = "2.4.0"
 lifecycle-version = "2.9.0"
+nav_version = "2.9.0"
+kotlin_serialization_json = "1.7.3"
 room-version = "2.7.1"
 
 [libraries]
@@ -43,6 +45,11 @@ room-compiler = { group = "androidx.room",name = "room-compiler",version.ref = "
 room-ktx = { group = "androidx.room",name = "room-ktx",version.ref = "room-version" }
 room-testing = { group = "androidx.room",name = "room-testing",version.ref = "room-version" }
 
+android-navigation-fragment = { group = "androidx.navigation", name = "navigation-fragment", version.ref = "nav_version" }
+android-navigation-ui = { group = "androidx.navigation", name = "navigation-ui", version.ref = "nav_version" }
+android-navigation-dynamic-features-fragment = { group = "androidx.navigation", name = "navigation-dynamic-features-fragment", version.ref = "nav_version" }
+kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlin_serialization_json" }
+
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

Nem az összes módosított fájl került megjelenítésre, mert túl sok fájl változott