浏览代码

硬件相关的代码拷贝已经问题处理

周文健 5 月之前
父节点
当前提交
d036a52e35
共有 100 个文件被更改,包括 5266 次插入60 次删除
  1. 13 5
      app/build.gradle.kts
  2. 0 1
      app/logback.xml
  3. 1 1
      app/src/main/AndroidManifest.xml
  4. 1 0
      app/src/main/java/com/grkj/iscs/ISCSApplication.kt
  5. 0 20
      app/src/main/java/com/grkj/iscs/MainActivity.kt
  6. 17 0
      app/src/main/java/com/grkj/iscs/features/login/LoginActivity.kt
  7. 18 0
      app/src/main/java/com/grkj/iscs/features/main/MainActivity.kt
  8. 8 0
      app/src/main/res/layout/activity_login.xml
  9. 18 15
      app/src/main/res/layout/activity_main.xml
  10. 0 6
      app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  11. 0 6
      app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  12. 二进制
      app/src/main/res/mipmap-hdpi/ic_launcher.png
  13. 二进制
      app/src/main/res/mipmap-hdpi/ic_launcher.webp
  14. 二进制
      app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  15. 二进制
      app/src/main/res/mipmap-mdpi/ic_launcher.png
  16. 二进制
      app/src/main/res/mipmap-mdpi/ic_launcher.webp
  17. 二进制
      app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  18. 二进制
      app/src/main/res/mipmap-xhdpi/ic_launcher.png
  19. 二进制
      app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  20. 二进制
      app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  21. 二进制
      app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  22. 二进制
      app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  23. 二进制
      app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  24. 二进制
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  25. 二进制
      app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  26. 二进制
      app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  27. 1 0
      build.gradle.kts
  28. 8 1
      data/build.gradle.kts
  29. 93 0
      data/src/main/java/com/grkj/data/HardwareRepository.kt
  30. 16 0
      data/src/main/java/com/grkj/data/StepRepository.kt
  31. 16 0
      data/src/main/java/com/grkj/data/TicketRepository.kt
  32. 0 4
      domain/src/main/java/com/grkj/domain/MyClass.kt
  33. 10 0
      domain/src/main/java/com/grkj/domain/entity/local/DeviceTakeUpdate.kt
  34. 9 0
      domain/src/main/java/com/grkj/domain/entity/local/LoginUser.kt
  35. 6 0
      domain/src/main/java/com/grkj/domain/entity/local/UpdateKeyReturn.kt
  36. 89 0
      domain/src/main/java/com/grkj/domain/entity/local/WorkTicketGet.kt
  37. 110 0
      domain/src/main/java/com/grkj/domain/entity/local/WorkTicketSend.kt
  38. 10 0
      domain/src/main/java/com/grkj/domain/entity/req/LockPointUpdateReq.kt
  39. 7 0
      domain/src/main/java/com/grkj/domain/entity/req/LockTakeUpdateReq.kt
  40. 41 0
      domain/src/main/java/com/grkj/domain/entity/res/CabinetSlotsRes.kt
  41. 15 0
      domain/src/main/java/com/grkj/domain/entity/res/CommonDictRes.kt
  42. 14 0
      domain/src/main/java/com/grkj/domain/entity/res/KeyInfoRes.kt
  43. 17 0
      domain/src/main/java/com/grkj/domain/entity/res/KeyPageRes.kt
  44. 14 0
      domain/src/main/java/com/grkj/domain/entity/res/LockInfoRes.kt
  45. 16 0
      domain/src/main/java/com/grkj/domain/entity/res/LockPageRes.kt
  46. 21 0
      domain/src/main/java/com/grkj/domain/entity/res/StepDetailRes.kt
  47. 38 0
      domain/src/main/java/com/grkj/domain/entity/res/SystemAttributePageRes.kt
  48. 111 0
      domain/src/main/java/com/grkj/domain/entity/res/TicketDetailRes.kt
  49. 131 0
      domain/src/main/java/com/grkj/domain/entity/res/UserInfoRes.kt
  50. 78 0
      domain/src/main/java/com/grkj/domain/repository/IHardwareRepository.kt
  51. 14 0
      domain/src/main/java/com/grkj/domain/repository/IStepRepository.kt
  52. 13 0
      domain/src/main/java/com/grkj/domain/repository/ITicketRepository.kt
  53. 26 1
      gradle/libs.versions.toml
  54. 3 0
      shared/src/main/java/com/grkj/shared/model/EventBean.kt
  55. 24 0
      ui-base/build.gradle.kts
  56. 0 0
      ui-base/libs/adh_series_sdk.jar
  57. 0 0
      ui-base/libs/arcsoft_face.jar
  58. 0 0
      ui-base/libs/arcsoft_image_util.jar
  59. 0 0
      ui-base/libs/zkandroidcore.jar
  60. 0 0
      ui-base/libs/zkandroidfingerservice.jar
  61. 0 0
      ui-base/libs/zkandroidfpreader.jar
  62. 125 0
      ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt
  63. 113 0
      ui-base/src/main/java/com/grkj/ui_base/base/BaseFragment.kt
  64. 9 0
      ui-base/src/main/java/com/grkj/ui_base/base/BaseViewModel.kt
  65. 620 0
      ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt
  66. 43 0
      ui-base/src/main/java/com/grkj/ui_base/business/DataBusiness.kt
  67. 243 0
      ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt
  68. 11 0
      ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt
  69. 29 0
      ui-base/src/main/java/com/grkj/ui_base/data/Constants.kt
  70. 51 0
      ui-base/src/main/java/com/grkj/ui_base/data/DictConstants.kt
  71. 42 0
      ui-base/src/main/java/com/grkj/ui_base/data/EventConstants.kt
  72. 16 0
      ui-base/src/main/java/com/grkj/ui_base/data/MMKVConstants.kt
  73. 100 0
      ui-base/src/main/java/com/grkj/ui_base/dialog/TipDialog.kt
  74. 202 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ArcSoftUtil.kt
  75. 122 0
      ui-base/src/main/java/com/grkj/ui_base/utils/CRC16.java
  76. 77 0
      ui-base/src/main/java/com/grkj/ui_base/utils/CommonUtils.kt
  77. 298 0
      ui-base/src/main/java/com/grkj/ui_base/utils/Executor.kt
  78. 104 0
      ui-base/src/main/java/com/grkj/ui_base/utils/NetManager.kt
  79. 270 0
      ui-base/src/main/java/com/grkj/ui_base/utils/SPUtils.kt
  80. 16 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleBean.kt
  81. 493 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt
  82. 543 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt
  83. 76 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConst.kt
  84. 13 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleListener.kt
  85. 204 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleUtil.kt
  86. 7 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleGattCallback.kt
  87. 9 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleIndicateCallback.kt
  88. 7 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleScanCallback.kt
  89. 9 0
      ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleWriteCallback.kt
  90. 30 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/CurrentModeEvent.kt
  91. 29 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceExceptionEvent.kt
  92. 29 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceTakeUpdateEvent.kt
  93. 17 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/EventHelper.kt
  94. 33 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/GetTicketStatusEvent.kt
  95. 23 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/LoadingEvent.kt
  96. 26 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/SwitchCollectionUpdateEvent.kt
  97. 24 0
      ui-base/src/main/java/com/grkj/ui_base/utils/event/UpdateTicketProgressEvent.kt
  98. 104 0
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/ByteArray.kt
  99. 52 0
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/Context.kt
  100. 20 0
      ui-base/src/main/java/com/grkj/ui_base/utils/extension/Int.kt

+ 13 - 5
app/build.gradle.kts

@@ -1,6 +1,7 @@
 plugins {
     alias(libs.plugins.android.application)
     alias(libs.plugins.kotlin.android)
+    id("org.jetbrains.kotlin.kapt")
 }
 
 android {
@@ -33,6 +34,9 @@ android {
     kotlinOptions {
         jvmTarget = "11"
     }
+    buildFeatures {
+        dataBinding = true
+    }
 }
 
 dependencies {
@@ -44,16 +48,20 @@ dependencies {
     implementation(libs.androidx.constraintlayout)
     implementation(libs.brv)
     implementation(libs.android.autosize)
-    implementation(libs.dialogx)
+    implementation(libs.viewmodel.ktx)
+    implementation(libs.viewmodel.livedata.ktx)
+    implementation(libs.viewmodel.savestate)
+    kapt(libs.viewmodel.compiler)
+    implementation(libs.android.navigation.fragment)
+    implementation(libs.android.navigation.ui)
+    implementation(libs.android.navigation.dynamic.features.fragment)
+    implementation(libs.kotlinx.serialization.json)
+    implementation(libs.sik.camera)
     implementation(project(":sync"))
     implementation(project(":ui-base"))
     implementation(project(":data"))
     implementation(project(":shared"))
     implementation(project(":domain"))
-    implementation(fileTree(mapOf(
-        "dir" to "libs",
-        "include" to listOf("*.jar", "*.aar")
-    )))
     testImplementation(libs.junit)
     androidTestImplementation(libs.androidx.junit)
     androidTestImplementation(libs.androidx.espresso.core)

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

@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="utf-8"?>
 <configuration
     xmlns="https://tony19.github.io/logback-android/xml"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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

@@ -16,7 +16,7 @@
         android:theme="@style/Theme.ISCS_BASE_APP"
         tools:targetApi="31">
         <activity
-            android:name=".MainActivity"
+            android:name=".features.main.MainActivity"
             android:exported="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />

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

@@ -3,6 +3,7 @@ package com.grkj.iscs
 import android.app.Application
 import com.kongzue.dialogx.DialogX
 import com.sik.sikcore.SIKCore
+import com.sik.sikcore.log.LogUtils
 
 /**
  * 启动入口

+ 0 - 20
app/src/main/java/com/grkj/iscs/MainActivity.kt

@@ -1,20 +0,0 @@
-package com.grkj.iscs
-
-import android.os.Bundle
-import androidx.activity.enableEdgeToEdge
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-
-class MainActivity : AppCompatActivity() {
-    override fun onCreate(savedInstanceState: Bundle?) {
-        super.onCreate(savedInstanceState)
-        enableEdgeToEdge()
-        setContentView(R.layout.activity_main)
-        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
-            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
-            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
-            insets
-        }
-    }
-}

+ 17 - 0
app/src/main/java/com/grkj/iscs/features/login/LoginActivity.kt

@@ -0,0 +1,17 @@
+package com.grkj.iscs.features.login
+
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.ActivityLoginBinding
+import com.grkj.ui_base.base.BaseActivity
+import com.sik.sikcore.SIKCore
+import com.tencent.mmkv.MMKV
+
+class LoginActivity: BaseActivity<ActivityLoginBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.activity_login
+    }
+
+    override fun initView() {
+
+    }
+}

+ 18 - 0
app/src/main/java/com/grkj/iscs/features/main/MainActivity.kt

@@ -0,0 +1,18 @@
+package com.grkj.iscs.features.main
+
+import com.grkj.iscs.R
+import com.grkj.iscs.databinding.ActivityMainBinding
+import com.grkj.ui_base.base.BaseActivity
+
+/**
+ * 首页
+ */
+class MainActivity() : BaseActivity<ActivityMainBinding>() {
+    override fun getLayoutId(): Int {
+        return R.layout.activity_main
+    }
+
+    override fun initView() {
+
+    }
+}

+ 8 - 0
app/src/main/res/layout/activity_login.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<layout xmlns:android="http://schemas.android.com/apk/res/android">
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent">
+
+    </RelativeLayout>
+</layout>

+ 18 - 15
app/src/main/res/layout/activity_main.xml

@@ -1,19 +1,22 @@
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<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"
-    android:id="@+id/main"
-    android:layout_width="match_parent"
-    android:layout_height="match_parent"
-    tools:context=".MainActivity">
+    xmlns:tools="http://schemas.android.com/tools">
 
-    <TextView
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:text="Hello World!"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+    <androidx.constraintlayout.widget.ConstraintLayout
+        android:id="@+id/main"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:context=".features.main.MainActivity">
 
-</androidx.constraintlayout.widget.ConstraintLayout>
+        <TextView
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="Hello World!"
+            app:layout_constraintBottom_toBottomOf="parent"
+            app:layout_constraintEnd_toEndOf="parent"
+            app:layout_constraintStart_toStartOf="parent"
+            app:layout_constraintTop_toTopOf="parent" />
+
+    </androidx.constraintlayout.widget.ConstraintLayout>
+</layout>

+ 0 - 6
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>

+ 0 - 6
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
-    <background android:drawable="@drawable/ic_launcher_background" />
-    <foreground android:drawable="@drawable/ic_launcher_foreground" />
-    <monochrome android:drawable="@drawable/ic_launcher_foreground" />
-</adaptive-icon>

二进制
app/src/main/res/mipmap-hdpi/ic_launcher.png


二进制
app/src/main/res/mipmap-hdpi/ic_launcher.webp


二进制
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp


二进制
app/src/main/res/mipmap-mdpi/ic_launcher.png


二进制
app/src/main/res/mipmap-mdpi/ic_launcher.webp


二进制
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp


二进制
app/src/main/res/mipmap-xhdpi/ic_launcher.png


二进制
app/src/main/res/mipmap-xhdpi/ic_launcher.webp


二进制
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp


二进制
app/src/main/res/mipmap-xxhdpi/ic_launcher.png


二进制
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp


二进制
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp


二进制
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png


二进制
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp


二进制
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 1 - 0
build.gradle.kts

@@ -4,4 +4,5 @@ plugins {
     alias(libs.plugins.kotlin.android) apply false
     alias(libs.plugins.android.library) apply false
     alias(libs.plugins.jetbrains.kotlin.jvm) apply false
+    alias(libs.plugins.kotlin.ksp) apply false
 }

+ 8 - 1
data/build.gradle.kts

@@ -1,6 +1,7 @@
 plugins {
     alias(libs.plugins.android.library)
     alias(libs.plugins.kotlin.android)
+    alias(libs.plugins.kotlin.ksp)
 }
 
 android {
@@ -36,6 +37,12 @@ dependencies {
 
     implementation(libs.androidx.core.ktx)
     implementation(project(":shared"))
-    implementation(project(":domain"))
+    api(project(":domain"))
     testImplementation(libs.junit)
+    // 引入 Room Runtime 和 KTX
+    implementation(libs.androidx.room.runtime)  // 即 androidx.room:room-runtime :contentReference[oaicite:1]{index=1}
+    implementation(libs.androidx.room.ktx)      // 即 androidx.room:room-ktx :contentReference[oaicite:2]{index=2}
+
+    // 注解处理器(编译时生成 DAO/Database 实现)
+    ksp(libs.androidx.room.compiler)           // 即 androidx.room:room-compiler :contentReference[oaicite:3]{index=3}
 }

+ 93 - 0
data/src/main/java/com/grkj/data/HardwareRepository.kt

@@ -0,0 +1,93 @@
+package com.grkj.data
+
+import com.grkj.domain.entity.req.LockPointUpdateReq
+import com.grkj.domain.entity.req.LockTakeUpdateReq
+import com.grkj.domain.entity.res.CabinetSlotsRes
+import com.grkj.domain.entity.res.KeyInfoRes
+import com.grkj.domain.entity.res.KeyPageRes
+import com.grkj.domain.entity.res.LockInfoRes
+import com.grkj.domain.entity.res.LockPageRes
+import com.grkj.domain.repository.IHardwareRepository
+
+/**
+ * 硬件仓储
+ */
+class HardwareRepository : IHardwareRepository {
+    /**
+     * 获取锁信息
+     */
+    override fun getLockInfo(
+        rfid: String,
+        callback: (LockInfoRes?) -> Unit
+    ) {
+
+    }
+
+    /**
+     * 获取钥匙信息
+     */
+    override fun getKeyInfo(
+        rfid: String,
+        callback: (KeyInfoRes?) -> Unit
+    ) {
+
+    }
+
+    /**
+     * 是否可以归还
+     */
+    override fun canReturn(): Boolean {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateKeyTake(
+        taskCode: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateKeyReturn(
+        taskCode: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean, String) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateLockPointBatch(
+        reqs: MutableList<LockPointUpdateReq>,
+        callback: (Boolean, String) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun updateLockTake(
+        lockTakeList: MutableList<LockTakeUpdateReq>,
+        callback: (Boolean) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getIsLockCabinetSlotsPage(callback: (CabinetSlotsRes?) -> Unit) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getIsLockPage(callback: (LockPageRes?) -> Unit) {
+        TODO("Not yet implemented")
+    }
+
+    override fun getIsKeyPage(callback: (KeyPageRes?) -> Unit) {
+        TODO("Not yet implemented")
+    }
+
+    override fun <T> getDictData(
+        dictKey: String,
+        callback: (List<T>) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+}

+ 16 - 0
data/src/main/java/com/grkj/data/StepRepository.kt

@@ -0,0 +1,16 @@
+package com.grkj.data
+
+import com.grkj.domain.entity.res.StepDetailRes
+import com.grkj.domain.repository.IStepRepository
+
+/**
+ * 步骤仓储层实现
+ */
+class StepRepository : IStepRepository {
+    override fun getStepDetail(
+        ticketId: Long,
+        callback: (List<StepDetailRes>?) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+}

+ 16 - 0
data/src/main/java/com/grkj/data/TicketRepository.kt

@@ -0,0 +1,16 @@
+package com.grkj.data
+
+import com.grkj.domain.entity.res.TicketDetailRes
+import com.grkj.domain.repository.ITicketRepository
+
+/**
+ * 工作票仓储实现
+ */
+class TicketRepository : ITicketRepository {
+    override fun getTicketDetail(
+        ticketId: Long,
+        callback: (TicketDetailRes?) -> Unit
+    ) {
+        TODO("Not yet implemented")
+    }
+}

+ 0 - 4
domain/src/main/java/com/grkj/domain/MyClass.kt

@@ -1,4 +0,0 @@
-package com.grkj.domain
-
-class MyClass {
-}

+ 10 - 0
domain/src/main/java/com/grkj/domain/entity/local/DeviceTakeUpdate.kt

@@ -0,0 +1,10 @@
+package com.grkj.domain.entity.local
+
+/**
+ * @param deviceType {@link [com.grkj.iscs.model.bo.DeviceTakeUpdateBO]<class>#[deviceType]}
+ */
+data class DeviceTakeUpdate(
+    val deviceType: Int,    // DeviceConst.DEVICE_TYPE_KEY or DeviceConst.DEVICE_TYPE_LOCK
+    val ticketId: Long,
+    val nfc: String
+)

+ 9 - 0
domain/src/main/java/com/grkj/domain/entity/local/LoginUser.kt

@@ -0,0 +1,9 @@
+package com.grkj.domain.entity.local
+
+data class LoginUser(
+    val userId: Long?,
+    val userName: String?,
+    val keyCode: String?,
+    val roleKeyList: MutableList<String>?,
+    val userCardList: MutableList<String>?
+)

+ 6 - 0
domain/src/main/java/com/grkj/domain/entity/local/UpdateKeyReturn.kt

@@ -0,0 +1,6 @@
+package com.grkj.domain.entity.local
+
+data class UpdateKeyReturn(
+    val ticketId: Long,
+    val keyNfc: String
+)

+ 89 - 0
domain/src/main/java/com/grkj/domain/entity/local/WorkTicketGet.kt

@@ -0,0 +1,89 @@
+package com.grkj.domain.entity.local
+
+import kotlin.collections.forEach
+
+class WorkTicketGet {
+    /**
+     * 权限卡号
+     */
+    var cardNo: String? = null
+    /**
+     * 用户密码
+     */
+    var password: String? = null
+    /**
+     * 工作票数组
+     */
+    var data: MutableList<DataDTO>? = null
+
+    class DataDTO {
+        /**
+         * 工作票号
+         */
+        var taskCode: String? = null
+        /**
+         * 工作票ID
+         */
+        var taskId: String? = null
+        /**
+         * 工作票下挂任务列表
+         */
+        var dataList: MutableList<DataListDTO>? = null
+
+        class DataListDTO {
+            /**
+             * 任务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—已完成
+             */
+            var closed: Int? = null
+            override fun toString(): String {
+                return "DataListDTO(dataId=$dataId, equipRfidNo=$equipRfidNo, infoRfidNo=$infoRfidNo, target=$target, status=$status, closed=$closed)"
+            }
+
+
+        }
+
+        override fun toString(): String {
+            return "DataDTO(taskCode=$taskCode, taskId=$taskId, dataList=$dataList)"
+        }
+
+
+    }
+
+    override fun toString(): String {
+        return "WorkTicketStatusBean(cardNo=$cardNo, password=$password, data=$data)"
+    }
+
+    // 判断是否有closed字段为0的
+    fun hasFinished(): Boolean {
+        data?.forEach {
+            it.dataList?.forEach {
+                if (it.closed == 0) {
+                    return false
+                }
+            }
+        }
+        return true
+    }
+}

+ 110 - 0
domain/src/main/java/com/grkj/domain/entity/local/WorkTicketSend.kt

@@ -0,0 +1,110 @@
+package com.grkj.domain.entity.local
+
+/**
+ * 工作票下发BO
+ */
+data class WorkTicketSend(
+    /**
+     * 权限卡号
+     */
+    var cardNo: String? = null,
+    /**
+     * 用户密码
+     */
+    var password: String? = null,
+    /**
+     * 工作有效期(小时)
+     */
+    var effectiveTime: Int? = null,
+    /**
+     * 工作票数组
+     */
+    var data: MutableList<DataBO>? = null,
+    /**
+     * 挂锁数组
+     */
+    var lockList: MutableList<LockListBO>? = null,
+
+    /**
+     * 辅件数组
+     */
+    var partList: MutableList<PartListBO>? = null
+) {
+    data class DataBO(
+        /**
+         * 工作票号
+         */
+        var taskCode: String? = null,
+        /**
+         * 工作票ID
+         */
+        var taskId: String? = null,
+        /**
+         * 工作票序号
+         */
+        var codeId: Int? = null,
+        /**
+         * 工作票下挂任务列表
+         */
+        var dataList: MutableList<DataListBO>? = null
+    ) {
+        data class DataListBO(
+            /**
+             * 任务ID
+             */
+            var dataId: Int? = null,
+            /**
+             * 工作点位RFID号
+             */
+            var equipRfidNo: String? = null,
+            /**
+             * 工作点位名称
+             */
+            var equipName: String? = null,
+            /**
+             * 锁RFID号(只在创建的时候有)
+             */
+            var infoRfidNo: String? = null,
+            /**
+             * 任务目标 0:挂锁 1:解锁
+             */
+            var target: Int? = null,
+            /**
+             * 前序任务ID
+             */
+            var prevId: Int? = null,
+            /**
+             * 辅件类型编码
+             */
+            var partCode: MutableList<String>? = null
+        )
+    }
+
+    data class LockListBO(
+        /**
+         * 挂锁ID
+         */
+        var lockId: Int? = null,
+        /**
+         * 挂锁RFID
+         */
+        var rfid: String? = null
+    )
+
+    data class PartListBO(
+        /**
+         * 辅件ID
+         */
+        var partId: Int? = null,
+
+        /**
+         * 辅件类型编码
+         */
+        var partCode: String? = null,
+
+        /**
+         * 锁或者辅件的RFID
+         */
+        var rfid: String? = null
+    )
+}

+ 10 - 0
domain/src/main/java/com/grkj/domain/entity/req/LockPointUpdateReq.kt

@@ -0,0 +1,10 @@
+package com.grkj.domain.entity.req
+
+data class LockPointUpdateReq(
+    val ticketId: Long?,
+    val lockNfc: String?,
+    val pointNfc: String?,
+    val keyNfc: String?,
+    val target: Int?,
+    val status: Int?
+)

+ 7 - 0
domain/src/main/java/com/grkj/domain/entity/req/LockTakeUpdateReq.kt

@@ -0,0 +1,7 @@
+package com.grkj.domain.entity.req
+
+data class LockTakeUpdateReq(
+    val ticketId: Long?,
+    val lockNfc: String?,
+    val serialNumber: String?
+)

+ 41 - 0
domain/src/main/java/com/grkj/domain/entity/res/CabinetSlotsRes.kt

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

+ 15 - 0
domain/src/main/java/com/grkj/domain/entity/res/CommonDictRes.kt

@@ -0,0 +1,15 @@
+package com.grkj.domain.entity.res
+
+data class CommonDictRes(
+    val createBy: String?,
+    val createTime: String?,
+    val default: Boolean?,
+    val dictCode: String?,
+    val dictLabel: String?,
+    val dictSort: String?,
+    val dictType: String?,
+    val dictValue: String?,
+    val isDefault: String?,
+    val listClass: String?,
+    val status: String?
+)

+ 14 - 0
domain/src/main/java/com/grkj/domain/entity/res/KeyInfoRes.kt

@@ -0,0 +1,14 @@
+package com.grkj.domain.entity.res
+
+/**
+ * 钥匙信息
+ */
+data class KeyInfoRes(
+    val keyId: Long?,
+    val keyCode: String?,
+    val keyName: String?,
+    val hardwareId: Long?,
+    val keyNfc: String?,
+    val macAddress: String?,
+    val delFlag: String?
+)

+ 17 - 0
domain/src/main/java/com/grkj/domain/entity/res/KeyPageRes.kt

@@ -0,0 +1,17 @@
+package com.grkj.domain.entity.res
+
+data class KeyPageRes(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<KeyPageItem>
+)
+
+data class KeyPageItem(
+    val keyId: String?,
+    val keyNfc: String?,
+    val macAddress: String?,
+    val keyName: String?,
+    val exStatus: String?,
+    val exRemark: String?
+)

+ 14 - 0
domain/src/main/java/com/grkj/domain/entity/res/LockInfoRes.kt

@@ -0,0 +1,14 @@
+package com.grkj.domain.entity.res
+
+/**
+ * 锁信息
+ */
+data class LockInfoRes(
+    val lockId: Long?,
+    val lockCode: String?,
+    val lockName: String?,
+    val lockTypeId: Long?,
+    val hardwareId: Long?,
+    val lockNfc: String?,
+    val delFlag: String?
+)

+ 16 - 0
domain/src/main/java/com/grkj/domain/entity/res/LockPageRes.kt

@@ -0,0 +1,16 @@
+package com.grkj.domain.entity.res
+
+data class LockPageRes(
+    val total: Int,
+    val size: Int,
+    val current: Int,
+    val records: List<LockPageItem>
+)
+
+data class LockPageItem(
+    val lockId: String?,
+    val lockNfc: String?,
+    val lockName: String?,
+    val exStatus: String?,
+    val exRemark: String?
+)

+ 21 - 0
domain/src/main/java/com/grkj/domain/entity/res/StepDetailRes.kt

@@ -0,0 +1,21 @@
+package com.grkj.domain.entity.res
+
+data class StepDetailRes(
+    val stepId: Long?,
+
+    val ticketId: Long?,
+
+    val stepIndex: Int?,
+
+    val stepStatus: String?,
+
+    val stepContent: String?,
+
+    val androidStepContent: String?,
+
+    val lockNum: Int?,
+
+    val userNum: Int?,
+
+    val conflictJobNum: Int?
+)

+ 38 - 0
domain/src/main/java/com/grkj/domain/entity/res/SystemAttributePageRes.kt

@@ -0,0 +1,38 @@
+package com.grkj.domain.entity.res
+
+data class SystemAttributePageRes(
+    val countId: Any,
+    val current: Int,
+    val maxLimit: Any,
+    val optimizeCountSql: Boolean,
+    val orders: MutableList<Any>,
+    val pages: Int,
+    val records: MutableList<Record>,
+    val searchCount: Boolean,
+    val size: Int,
+    val total: Int
+) {
+    data class Record(
+        val sysAttrId: Long?,
+
+        /**
+         * 参数名称
+         */
+        val sysAttrName: String?,
+
+        /**
+         * 参数键名
+         */
+        val sysAttrKey: String?,
+
+        /**
+         * 参数键值
+         */
+        val sysAttrValue: String?,
+
+        /**
+         * 分类
+         */
+        val sysAttrType: String?,
+    )
+}

+ 111 - 0
domain/src/main/java/com/grkj/domain/entity/res/TicketDetailRes.kt

@@ -0,0 +1,111 @@
+package com.grkj.domain.entity.res
+
+data class TicketDetailRes(
+    val ticketId: Long?,
+    val ticketCode: String?,
+    val ticketName: String?,
+    val workshopId: Long?,
+    val workareaId: Long?,
+    val sopId: Long?,
+    val ticketType: String?,
+    val ticketContent: String?,
+    val ticketStatus: String?,
+    val ticketStartTime: String?,
+    val ticketEndTime: String?,
+    val delFlag: String?,
+    val createBy: String?,
+    val ticketKeyVOList: MutableList<JobTicketKeyVO>?,
+    val ticketLockVOList: MutableList<JobTicketLockVO>?,
+    val ticketLocksetVOList: MutableList<JobTicketLocksetVO>?,
+    val ticketUserVOList: MutableList<JobTicketUserVO>?,
+    val ticketPointsVOList: MutableList<JobTicketPointsVO>?,
+    val noUnlockTicketPointsVOSet: MutableList<JobTicketPointsVO>?
+) {
+    data class JobTicketKeyVO(
+        val recordId: Long?,
+        val ticketId: Long?,
+        val keyId: Long?,
+        val fromHardwareId: Long?,
+        val toHardwareId: Long?,
+        val collectTime: String?,
+        val giveBackTime: String?,
+        val keyStatus: String?,
+        val delFlag: String?,
+        val ticketType: Int?
+    )
+
+    data class JobTicketLockVO(
+        val recordId: Long?,
+        val ticketId: Long?,
+        val lockId: Long?,
+        val lockNfc: String?,
+        val fromHardwareId: Long?,
+        val toHardwareId: Long?,
+        val isolationPointId: Long?,
+        val lockStatus: String?,
+        val delFlag: String?
+    )
+
+    data class JobTicketLocksetVO(
+        val recordId: Long?,
+        val jobTicketId: Long?,
+        val pointId: Long?,
+        val locksetId: Long?,
+        val fromHardwareId: Long?,
+        val toHardwareId: Long?,
+        val locksetTypeId: Long?,
+        val locksetStatus: String?,
+        val collectTime: String?,
+        val giveBackTime: String?,
+        val delFlag: String?
+    )
+
+    data class JobTicketUserVO(
+        val recordId: Long?,
+        val ticketId: Long?,
+        val userId: Long?,
+        val userName: String?,
+        val userType: String?,
+        val userRole: String?,
+        val jobStatus: Int?,//作业状态(0未开始,1 取锁具, 2取钥匙, 3待上锁(待共锁),4 已上锁(已共锁),5 已解锁)
+    )
+
+    data class JobTicketPointsVO(
+        val recordId: Long?,
+        val ticketId: Long?,
+        val workshopId: Long?,
+        val workareaId: Long?,
+        val pointId: Long?,
+        val pointStatus: String?,
+        val delFlag: String?,
+        val lockId: Long?,
+        val lockedByKeyId: Long?,
+        val unlockedByKeyId: Long?,
+        val lockTime: String?,
+        val unlockTime: String?,
+        val prePointId: Long?,
+        val pointCode: String?,
+        val pointName: String?,
+        val pointType: String?,
+        val pointTypeName: String?,
+        val pointNfc: String?,
+        val workshopName: String?,
+        val workareaName: String?,
+        val workstationId: Long?,
+        val workstationName: String?,
+        val lotoId: Long?,
+        val lotoName: String?,
+        val powerType: String?,
+        val powerTypeName: String?,
+        val isolationMethod: String?,
+        val pointIcon: String?,
+        val pointPicture: String?,
+        val lockTypeId: Long?,
+        val lockTypeCode: String?,
+        val lockTypeName: String?,
+        val lockTypeIcon: String?,
+        val lockTypeImg: String?,
+        val lockNfc: String?,
+        val locksetTypeId: Long?,
+    )
+}

+ 131 - 0
domain/src/main/java/com/grkj/domain/entity/res/UserInfoRes.kt

@@ -0,0 +1,131 @@
+package com.grkj.domain.entity.res
+
+import java.io.Serializable
+
+data class UserInfoRes(
+    val code: Int?,
+
+    val msg: String?,
+
+    val permissions: MutableList<String>?,
+
+    val roles: MutableList<String>?,
+
+    val userCardList: MutableList<String>?,
+
+    val user: SysUserVO?
+) : Serializable {
+    data class SysUserVO(
+        val userId: Long?,
+
+        val deptId: Long?,
+
+        val userName: String?,
+
+        val nickName: String?,
+
+        val email: String?,
+
+        val phonenumber: String?,
+
+        val sex: String?,
+
+        val avatar: String?,
+
+        val password: String?,
+
+        val keyCode: String?,
+
+        val salt: String?,
+
+        val status: String?,
+
+        val delFlag: String?,
+
+        val loginIp: String?,
+
+        val loginDate: String?,
+
+        val dept: SysDeptVO?,
+
+        val roles: List<SysRole>?,
+
+        val roleIds: MutableList<Long>?,
+
+        val postIds: MutableList<Long>?,
+
+        val workstationIds: MutableList<Long>?,
+
+        val unitIds: MutableList<Long>?,
+
+        val roleId: Long?,
+
+        val roleKey: String?,
+
+        val unitId: Long?,
+
+        val workstationId: Long?,
+
+        val unitName: String?,
+
+        val roleName: String?,
+
+        val userIds: Set<Long>?,
+
+        val b: Boolean?
+    ) : Serializable {
+        data class SysRole(
+            val roleId: Long?,
+
+            val roleName: String?,
+
+            val roleKey: String?,
+
+            val roleSort: String?,
+
+            val dataScope: String?,
+
+            val marsDataScope: String?,
+
+            val menuCheckStrictly: Boolean?,
+
+            val deptCheckStrictly: Boolean?,
+
+            val status: String?,
+
+            val delFlag: String?,
+
+            val flag: Boolean?,
+
+            val menuIds: List<Long>,
+
+            val deptIds: List<Long>,
+
+            val workstationIds: List<Long>
+        ) : Serializable
+
+        data class SysDeptVO(
+            val deptId: Long?,
+
+            val parentId: Long?,
+
+            val ancestors: String?,
+
+            val deptName: String?,
+
+            val orderNum: String?,
+
+            val leader: String?,
+
+            val phone: String?,
+
+            val email: String?,
+
+            val status: String?,
+
+            val delFlag: String?,
+
+            val parentName: String?
+        ) : Serializable
+    }
+}

+ 78 - 0
domain/src/main/java/com/grkj/domain/repository/IHardwareRepository.kt

@@ -0,0 +1,78 @@
+package com.grkj.domain.repository
+
+import com.grkj.domain.entity.req.LockPointUpdateReq
+import com.grkj.domain.entity.req.LockTakeUpdateReq
+import com.grkj.domain.entity.res.CabinetSlotsRes
+import com.grkj.domain.entity.res.CommonDictRes
+import com.grkj.domain.entity.res.KeyInfoRes
+import com.grkj.domain.entity.res.KeyPageRes
+import com.grkj.domain.entity.res.LockInfoRes
+import com.grkj.domain.entity.res.LockPageRes
+
+/**
+ * 硬件相关仓储
+ */
+interface IHardwareRepository {
+    /**
+     * 获取锁信息
+     */
+    fun getLockInfo(rfid: String, callback: (LockInfoRes?) -> Unit)
+
+    /**
+     * 获取钥匙信息
+     */
+    fun getKeyInfo(rfid: String, callback: (KeyInfoRes?) -> Unit)
+
+    /**
+     * 是否可以归还
+     */
+    fun canReturn(): Boolean
+
+    /**
+     * 上报钥匙取出
+     */
+    fun updateKeyTake(taskCode: Long, keyNfc: String, serialNo: String, callback: (Boolean) -> Unit)
+
+    /**
+     * 上报钥匙归还
+     */
+    fun updateKeyReturn(
+        taskCode: Long,
+        keyNfc: String,
+        serialNo: String,
+        callback: (Boolean, String) -> Unit
+    )
+
+    /**
+     * 批量更新隔离点的锁位状态
+     */
+    fun updateLockPointBatch(
+        reqs: MutableList<LockPointUpdateReq>,
+        callback: (Boolean, String) -> Unit
+    )
+
+    /**
+     * 挂锁取出
+     */
+    fun updateLockTake(lockTakeList: MutableList<LockTakeUpdateReq>, callback: (Boolean) -> Unit)
+
+    /**
+     * 获取仓位数据
+     */
+    fun getIsLockCabinetSlotsPage(callback: (CabinetSlotsRes?) -> Unit)
+
+    /**
+     * 获取挂锁列表
+     */
+    fun getIsLockPage(callback: (LockPageRes?) -> Unit)
+
+    /**
+     * 获取钥匙列表
+     */
+    fun getIsKeyPage(callback: (KeyPageRes?) -> Unit)
+
+    /**
+     * 获取字典数据
+     */
+    fun <T> getDictData(dictKey: String, callback: (List<T>) -> Unit)
+}

+ 14 - 0
domain/src/main/java/com/grkj/domain/repository/IStepRepository.kt

@@ -0,0 +1,14 @@
+package com.grkj.domain.repository
+
+import com.grkj.domain.entity.res.StepDetailRes
+
+/**
+ * 步骤相关仓储层
+ */
+interface IStepRepository {
+    /**
+     * 获取步骤详情
+     */
+    fun getStepDetail(ticketId: Long, callback: (List<StepDetailRes>?) -> Unit)
+
+}

+ 13 - 0
domain/src/main/java/com/grkj/domain/repository/ITicketRepository.kt

@@ -0,0 +1,13 @@
+package com.grkj.domain.repository
+
+import com.grkj.domain.entity.res.TicketDetailRes
+
+/**
+ * 工作票仓储
+ */
+interface ITicketRepository {
+    /**
+     * 工作票详情
+     */
+    fun getTicketDetail(ticketId: Long, callback: (TicketDetailRes?) -> Unit)
+}

+ 26 - 1
gradle/libs.versions.toml

@@ -1,6 +1,6 @@
 [versions]
 agp = "8.10.0"
-kotlin = "2.0.21"
+kotlin = "2.1.10"
 coreKtx = "1.10.1"
 junit = "4.13.2"
 junitVersion = "1.1.5"
@@ -17,6 +17,12 @@ sikfontmanager = "1.0.2"
 brv = "1.6.1"
 androidautosize = "v1.2.1"
 dialogx = "0.0.49"
+room = "2.7.1"  # 最新稳定版(Apr 23, 2025) :contentReference[oaicite:0]{index=0}
+lifecycle-version = "2.9.0"
+ksp = "2.1.10-1.0.31"
+nav_version = "2.9.0"
+kotlin_serialization_json = "1.7.3"
+fastble = "2.4.0"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -44,9 +50,28 @@ brv = { group = "com.github.liangjingkanji", name = "brv", version.ref = "brv" }
 dialogx = { group = "com.github.kongzue.DialogX", name = "DialogX", version.ref = "dialogx" }
 android-autosize = { group = "com.github.JessYanCoding", name = "AndroidAutoSize", version.ref = "androidautosize" }
 
+viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycle-version" }
+viewmodel-livedata-ktx = { group = "androidx.lifecycle", name = "lifecycle-livedata-ktx", version.ref = "lifecycle-version" }
+viewmodel-savestate = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-savedstate", version.ref = "lifecycle-version" }
+viewmodel-compiler = { group = "androidx.lifecycle", name = "lifecycle-compiler", version.ref = "lifecycle-version" }
+
+androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
+androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
+androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
+
+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" }
+
+fastble = { group = "com.github.Jasonchenlijian", name = "FastBle", version.ref = "fastble" }
+
 [plugins]
 android-application = { id = "com.android.application", version.ref = "agp" }
 kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
 android-library = { id = "com.android.library", version.ref = "agp" }
 jetbrains-kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "jetbrainsKotlinJvm" }
+kotlin-ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 
+[bundles]
+room = ["androidx-room-runtime", "androidx-room-ktx", "androidx-room-compiler"]

+ 3 - 0
shared/src/main/java/com/grkj/shared/model/EventBean.kt

@@ -0,0 +1,3 @@
+package com.grkj.shared.model
+
+data class EventBean<T>(val code: Int, val data: T)

+ 24 - 0
ui-base/build.gradle.kts

@@ -1,6 +1,7 @@
 plugins {
     alias(libs.plugins.android.library)
     alias(libs.plugins.kotlin.android)
+    id("org.jetbrains.kotlin.kapt")
 }
 
 android {
@@ -30,6 +31,9 @@ android {
     kotlinOptions {
         jvmTarget = "11"
     }
+    buildFeatures {
+        dataBinding = true
+    }
 }
 
 dependencies {
@@ -39,7 +43,27 @@ dependencies {
     implementation(libs.material)
     implementation(libs.androidx.activity)
     implementation(libs.androidx.constraintlayout)
+    implementation(libs.viewmodel.ktx)
+    implementation(libs.viewmodel.livedata.ktx)
+    implementation(libs.viewmodel.savestate)
+    kapt(libs.viewmodel.compiler)
+    implementation(libs.android.navigation.fragment)
+    implementation(libs.android.navigation.ui)
+    implementation(libs.android.navigation.dynamic.features.fragment)
+    implementation(libs.kotlinx.serialization.json)
+    implementation(libs.sik.camera)
     api(libs.sik.extension.android)
+    api(libs.dialogx)
+    api(libs.fastble)
+    api(
+        fileTree(
+            mapOf(
+                "dir" to "libs",
+                "include" to listOf("*.jar", "*.aar")
+            )
+        )
+    )
+    implementation(project(":data"))
     implementation(project(":shared"))
     testImplementation(libs.junit)
 }

+ 0 - 0
app/libs/adh_series_sdk.jar → ui-base/libs/adh_series_sdk.jar


+ 0 - 0
app/libs/arcsoft_face.jar → ui-base/libs/arcsoft_face.jar


+ 0 - 0
app/libs/arcsoft_image_util.jar → ui-base/libs/arcsoft_image_util.jar


+ 0 - 0
app/libs/zkandroidcore.jar → ui-base/libs/zkandroidcore.jar


+ 0 - 0
app/libs/zkandroidfingerservice.jar → ui-base/libs/zkandroidfingerservice.jar


+ 0 - 0
app/libs/zkandroidfpreader.jar → ui-base/libs/zkandroidfpreader.jar


+ 125 - 0
ui-base/src/main/java/com/grkj/ui_base/base/BaseActivity.kt

@@ -0,0 +1,125 @@
+package com.grkj.ui_base.base
+
+import android.os.Bundle
+import com.kongzue.dialogx.dialogs.PopTip
+import com.sik.sikandroid.permission.PermissionUtils
+import androidx.activity.enableEdgeToEdge
+import androidx.appcompat.app.AppCompatActivity
+import androidx.databinding.DataBindingUtil
+import androidx.databinding.ViewDataBinding
+import com.grkj.shared.model.EventBean
+import com.google.android.material.bottomnavigation.BottomNavigationView
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.NavController
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+
+/**
+ * BaseActivity: 支持 ViewBinding, EventBus, 权限管理, 串口 & 蓝牙, 以及 Navigation 多 Graph 切换 & BottomNav
+ */
+abstract class BaseActivity<V : ViewDataBinding> : AppCompatActivity() {
+    protected lateinit var binding: V
+
+    /** 是否启用 Navigation 多 Graph 与 BottomNav */
+    protected open fun enableNavigation(): Boolean = false
+
+    /** NavHostFragment 容器 ID,子类返回对应 fragment 布局 ID */
+    protected open fun navHostFragmentId(): Int = 0
+    protected lateinit var navController: NavController
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        enableEdgeToEdge()
+
+        binding = DataBindingUtil.setContentView(this, getLayoutId())
+        binding.lifecycleOwner = this
+
+        if (!EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().register(this)
+        }
+
+        initView()
+        if (enableNavigation()) setupNavController()
+        initData()
+        initListeners()
+        initObservers()
+    }
+
+    /** 请求权限,基于 PermissionUtils */
+    protected fun requestPermissionsIfNeeded(
+        vararg permissions: String,
+        permissionCallback: PermissionUtils.PermissionCallback
+    ) {
+        PermissionUtils.checkAndRequestPermissions(
+            permissions.toList().toTypedArray(),
+            permissionCallback
+        )
+    }
+
+    /** 初始化 NavController */
+    private fun setupNavController() {
+        val host = supportFragmentManager.findFragmentById(navHostFragmentId()) as NavHostFragment
+        navController = host.navController
+    }
+
+    /** 动态切换 Nav Graph */
+    protected fun replaceNavGraph(graphId: Int) {
+        navController.setGraph(graphId)
+    }
+
+    /** 配置 BottomNavigation 切换 Graph,map: menuItemId -> navGraphId */
+    protected fun setupBottomNavigation(
+        bottomNav: BottomNavigationView,
+        graphMap: Map<Int, Int>
+    ) {
+        bottomNav.setOnItemSelectedListener { item ->
+            graphMap[item.itemId]?.let {
+                replaceNavGraph(it)
+                true
+            } == true
+        }
+    }
+
+    /** EventBus 事件,子类可重写 */
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    protected open fun onEvent(event: EventBean<Any>) {
+    }
+
+    override fun onDestroy() {
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this)
+        }
+        super.onDestroy()
+    }
+
+    /** 显示加载框,子类实现 */
+    protected fun showLoading(msg: String) {
+        // TODO: 子类实现统一 Loading 样式
+    }
+
+    /** 隐藏加载框,子类实现 */
+    protected fun hideLoading() {
+        // TODO: 子类实现
+    }
+
+    /** 通用吐司 */
+    protected fun showToast(msg: String) {
+        PopTip.tip(msg)
+    }
+
+    /** 子类必须实现:布局资源 ID */
+    protected abstract fun getLayoutId(): Int
+
+    /** 子类必须实现:初始化视图 */
+    protected abstract fun initView()
+
+    /** 可选:初始化数据 */
+    protected open fun initData() {}
+
+    /** 可选:初始化监听 */
+    protected open fun initListeners() {}
+
+    /** 可选:初始化 LiveData/事件观察 */
+    protected open fun initObservers() {}
+}

+ 113 - 0
ui-base/src/main/java/com/grkj/ui_base/base/BaseFragment.kt

@@ -0,0 +1,113 @@
+package com.grkj.ui_base.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.shared.model.EventBean
+import com.kongzue.dialogx.dialogs.PopTip
+import com.sik.sikandroid.permission.PermissionUtils
+import org.greenrobot.eventbus.EventBus
+import org.greenrobot.eventbus.Subscribe
+import org.greenrobot.eventbus.ThreadMode
+
+/**
+ * BaseFragment: 支持 ViewBinding, EventBus, 权限管理, 串口 & 蓝牙, 以及 Navigation 切换
+ */
+abstract class BaseFragment<V : ViewDataBinding> : Fragment() {
+
+    protected lateinit var binding: V
+
+    /** 是否启用 Navigation */
+    protected open fun enableNavigation(): Boolean = false
+    protected lateinit var navController: NavController
+
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View {
+        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)
+        }
+
+        // Navigation 初始化
+        if (enableNavigation()) {
+            navController = findNavController()
+        }
+
+        initView()
+        initData()
+        initListeners()
+        initObservers()
+    }
+
+    /** 请求权限,基于 PermissionUtils */
+    protected fun requestPermissionsIfNeeded(
+        vararg permissions: String,
+        callback: PermissionUtils.PermissionCallback
+    ) {
+        PermissionUtils.checkAndRequestPermissions(
+            permissions.toList().toTypedArray(),
+            callback
+        )
+    }
+
+    /** EventBus 事件,子类可重写 */
+    @Subscribe(threadMode = ThreadMode.MAIN)
+    protected open fun onEvent(event: EventBean<Any>) {
+    }
+
+    override fun onDestroyView() {
+        if (EventBus.getDefault().isRegistered(this)) {
+            EventBus.getDefault().unregister(this)
+        }
+        super.onDestroyView()
+    }
+
+    /** 显示加载框,子类实现 */
+    protected fun showLoading(msg: String) {
+        // TODO: 子类实现统一 Loading
+    }
+
+    /** 隐藏加载框,子类实现 */
+    protected fun hideLoading() {
+        // TODO
+    }
+
+    /** 通用吐司 */
+    protected fun showToast(msg: String) {
+        PopTip.tip(msg)
+    }
+
+    /** 获取布局资源 ID */
+    @LayoutRes
+    protected abstract fun getLayoutId(): Int
+
+    /** 初始化视图 */
+    protected abstract fun initView()
+
+    /** 可选:初始化数据 */
+    protected open fun initData() {}
+
+    /** 可选:初始化监听 */
+    protected open fun initListeners() {}
+
+    /** 可选:初始化 LiveData/事件观察 */
+    protected open fun initObservers() {}
+}

+ 9 - 0
ui-base/src/main/java/com/grkj/ui_base/base/BaseViewModel.kt

@@ -0,0 +1,9 @@
+package com.grkj.ui_base.base
+
+import androidx.lifecycle.ViewModel
+
+/**
+ * 界面模型基类
+ */
+class BaseViewModel : ViewModel() {
+}

+ 620 - 0
ui-base/src/main/java/com/grkj/ui_base/business/BleBusinessManager.kt

@@ -0,0 +1,620 @@
+package com.grkj.ui_base.business
+
+import androidx.appcompat.app.AppCompatActivity
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.google.gson.Gson
+import com.grkj.data.HardwareRepository
+import com.grkj.data.StepRepository
+import com.grkj.data.TicketRepository
+import com.grkj.domain.entity.local.DeviceTakeUpdate
+import com.grkj.domain.entity.local.UpdateKeyReturn
+import com.grkj.domain.entity.local.WorkTicketGet
+import com.grkj.domain.entity.local.WorkTicketSend
+import com.grkj.domain.entity.local.WorkTicketSend.LockListBO
+import com.grkj.domain.entity.req.LockPointUpdateReq
+import com.grkj.domain.entity.res.CommonDictRes
+import com.grkj.domain.entity.res.TicketDetailRes
+import com.grkj.domain.repository.IHardwareRepository
+import com.grkj.domain.repository.IStepRepository
+import com.grkj.domain.repository.ITicketRepository
+import com.grkj.ui_base.R
+import com.grkj.ui_base.data.Constants
+import com.grkj.ui_base.data.DictConstants
+import com.grkj.ui_base.dialog.TipDialog
+import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.Executor
+import com.grkj.ui_base.utils.SPUtils
+import com.grkj.ui_base.utils.event.CurrentModeEvent
+import com.grkj.ui_base.utils.event.DeviceExceptionEvent
+import com.grkj.ui_base.utils.event.LoadingEvent
+import com.grkj.ui_base.utils.event.UpdateTicketProgressEvent
+import com.grkj.ui_base.utils.extension.serialNo
+import com.grkj.ui_base.utils.modbus.DeviceConst
+import com.grkj.ui_base.utils.modbus.ModBusController
+import com.grkj.ui_base.utils.ble.BleCmdManager
+import com.grkj.ui_base.utils.ble.BleConnectionManager
+import com.grkj.ui_base.utils.ble.BleConst
+import com.grkj.ui_base.utils.ble.CustomBleWriteCallback
+import com.kongzue.dialogx.dialogs.PopTip
+import com.sik.sikcore.SIKCore
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import org.slf4j.LoggerFactory
+
+/**
+ * 蓝牙业务
+ */
+object BleBusinessManager {
+    private val logger = LoggerFactory.getLogger(BleBusinessManager::class.java)
+
+    /**
+     * 硬件仓储
+     */
+    private val hardwareRepository: IHardwareRepository by lazy { HardwareRepository() }
+
+    /**
+     * 步骤仓储
+     */
+    private val stepRepository: IStepRepository by lazy { StepRepository() }
+
+    /**
+     * 工作票仓储
+     */
+    private val ticketRepository: ITicketRepository by lazy { TicketRepository() }
+
+    /**
+     * 处理工作票完成情况
+     */
+    fun handleTicketStatus(
+        bleDevice: BleDevice, byteArray: ByteArray, isNeedLoading: Boolean = false
+    ) {
+        BleCmdManager.handleTicketStatus(bleDevice, byteArray) { ticketJson ->
+            if (ticketJson.isNullOrEmpty()) {
+                return@handleTicketStatus
+            }
+            if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票完成状态读取完成", true)
+            logger.info("Get ticket status complete : ${bleDevice.mac}")
+            // TD:Ticket Done
+            if (isNeedLoading) LoadingEvent.sendLoadingEvent("TD$ticketJson}", true)
+
+            val workTicketGet = Gson().fromJson(ticketJson, WorkTicketGet::class.java)
+
+            // 判断WorkTicketGet里是否有未完成的
+            if (workTicketGet.hasFinished()) {
+                Executor.delayOnMain(500) {
+                    handleKeyReturn(bleDevice, workTicketGet)
+                }
+            } else {
+                // 当前策略:作业票未完成禁止归还钥匙
+                TipDialog.show(
+                    msg = CommonUtils.getStr(R.string.key_return_tip)!!,
+                    onConfirmClick = {
+                        LoadingEvent.sendLoadingEvent()
+                        PopTip.tip(CommonUtils.getStr(R.string.continue_the_ticket))
+                        BleManager.getInstance().disconnect(bleDevice)
+                        // 打开卡扣,防止初始化的时候选择不处理钥匙导致无法使用
+                        val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+                        val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+                        keyBean?.let {
+                            ModBusController.controlKeyBuckle(true, keyBean.idx, dock.addr)
+                        }
+                    })
+            }
+        }
+    }
+
+    /**
+     * 处理钥匙归还
+     */
+    private fun handleKeyReturn(bleDevice: BleDevice, workTicketGet: WorkTicketGet?) {
+        val dock = ModBusController.getDockByKeyMac(bleDevice.mac)
+        val keyBean = dock?.getKeyList()?.find { it.mac == bleDevice.mac }
+        keyBean?.let {
+            ModBusController.controlKeyBuckle(false, keyBean.idx, dock.addr)
+        }
+        // 上报隔离点状态
+        val keyNfc = ModBusController.getKeyByMac(bleDevice.mac)?.rfid
+        workTicketGet?.data?.forEach { data ->
+            val updateList = mutableListOf<LockPointUpdateReq>()
+            data.dataList?.forEach { dataListDTO ->
+                data.taskCode?.toLong()?.let {
+                    SPUtils.returnKey(it)
+                }
+                val updateVO = LockPointUpdateReq(
+                    data.taskCode?.toLong(),
+                    dataListDTO.infoRfidNo,
+                    dataListDTO.equipRfidNo,
+                    keyNfc!!,
+                    dataListDTO.target,
+                    dataListDTO.status
+                )
+                updateList.add(updateVO)
+            }
+
+            LoadingEvent.sendLoadingEvent()
+            PopTip.tip(R.string.key_return_success)
+            if (hardwareRepository.canReturn()) {
+                // 上报点位钥匙绑定
+                hardwareRepository.updateLockPointBatch(updateList) { isSuccess, msg ->
+                    if (isSuccess || msg == CommonUtils.getStr(R.string.lock_nfc_lost)) {
+                        data.taskCode?.toLong()?.let {
+                            UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(it)
+                        }
+                        // 确认归还,切换为待机模式
+                        switchReadyMode(bleDevice)
+                    } else if (msg != CommonUtils.getStr(R.string.lock_nfc_lost)) {
+                        SPUtils.saveUpdateLockPoint(SIKCore.getApplication(), updateList)
+                    }
+                }
+
+                // 上报钥匙归还
+                hardwareRepository.updateKeyReturn(
+                    data.taskCode?.toLong()!!, keyNfc!!, SIKCore.getApplication().serialNo()
+                ) { isSuccess, msg ->
+                    if (!isSuccess && msg != CommonUtils.getStr(R.string.ticket_lost)) {
+                        SPUtils.saveUpdateKeyReturn(
+                            SIKCore.getApplication(),
+                            UpdateKeyReturn(data.taskCode?.toLong()!!, keyNfc!!)
+                        )
+                    }
+                }
+            } else {
+                SPUtils.saveUpdateLockPoint(SIKCore.getApplication(), updateList)
+                SPUtils.saveUpdateKeyReturn(
+                    SIKCore.getApplication(), UpdateKeyReturn(data.taskCode?.toLong()!!, keyNfc!!)
+                )
+                // 保存待发数据,切换为待机模式
+                switchReadyMode(bleDevice)
+            }
+        }
+    }
+
+    /**
+     * 处理虚拟钥匙取出,如果作业的全部点位已经上锁更新钥匙的状态使用
+     */
+    fun handleVirtualKeyGive(taskCode: Long, keyNfc: String, done: () -> Unit) {
+        // 上报钥匙取出
+        hardwareRepository.updateKeyTake(
+            taskCode,
+            keyNfc,
+            SIKCore.getApplication().serialNo()
+        ) { isSuccess ->
+            if (isSuccess) {
+                done()
+            }
+        }
+    }
+
+    /**
+     * 处理虚拟钥匙归还,如果作业的全部点位已经上锁更新钥匙的状态使用
+     */
+    fun handleVirtualKeyReturn(taskCode: Long, keyNfc: String, done: () -> Unit) {
+        // 上报钥匙归还
+        hardwareRepository.updateKeyReturn(
+            taskCode, keyNfc, SIKCore.getApplication().serialNo()
+        ) { isSuccess, msg ->
+            if (!isSuccess && msg != CommonUtils.getStr(R.string.ticket_lost)
+            ) {
+                SPUtils.saveUpdateKeyReturn(
+                    SIKCore.getApplication(), UpdateKeyReturn(taskCode, keyNfc)
+                )
+            } else {
+                done()
+                UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(taskCode)
+            }
+        }
+    }
+
+    /**
+     * 切换待机模式
+     */
+    fun switchReadyMode(bleDevice: BleDevice) {
+        BleCmdManager.switchMode(
+            BleConst.STATUS_READY,
+            bleDevice,
+            object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    logger.info("switch mode ready success : ${bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    logger.error("switch mode ready fail : ${bleDevice.mac}")
+                    Executor.delayOnMain(300) {
+                        switchReadyMode(bleDevice)
+                    }
+                }
+            })
+    }
+
+    /**
+     * 获取工作票完成情况
+     */
+    private fun getTicketStatus(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        processCallback: ((Boolean) -> Unit)? = null
+    ) {
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始获取工作票", true)
+        BleCmdManager.getTicketStatus(bleDevice, object : CustomBleWriteCallback() {
+            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取成功", true)
+                logger.info("getTicketStatus success")
+            }
+
+            override fun onWriteFailure(exception: BleException?) {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent("工作票获取失败", true)
+                processCallback?.invoke(false)
+                logger.error("getTicketStatus fail")
+            }
+        })
+    }
+
+    /**
+     * 根据当前模式进行处理
+     */
+    private fun handleCurrentMode(currentModeEvent: CurrentModeEvent) {
+        when (currentModeEvent.mode) {
+            // 工作模式
+            0x01.toByte() -> {
+                // 读工作票
+                getTicketStatusBusiness(currentModeEvent.bleBean.bleDevice.mac)
+            }
+            // 待机模式
+            0x02.toByte() -> {
+                // 根据情况看是否需要下发工作票
+                ModBusController.getKeyByMac(currentModeEvent.bleBean.bleDevice.mac)?.let { key ->
+                    // 判断是否有待取的钥匙
+                    val updateBo =
+                        ModbusBusinessManager.mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && key.rfid == it.nfc }
+                    updateBo?.let { itBO ->
+                        stepRepository.getStepDetail(itBO.ticketId) {
+                            var step = 0
+                            it?.filter { it.stepStatus == "1" }
+                                ?.maxByOrNull { it.stepIndex!! }?.stepIndex?.let {
+                                    step = it
+                                }
+                            ticketRepository.getTicketDetail(itBO.ticketId) { ticketDetail ->
+                                val role = ticketDetail?.ticketUserVOList?.find {
+                                    it.userId == SPUtils.getLoginUser(SIKCore.getApplication())?.userId && it.userType == Constants.USER_TYPE_LOCKER
+                                }
+                                if (role == null) {
+                                    PopTip.tip(CommonUtils.getStr(R.string.you_are_not_locker_tip))
+                                    return@getTicketDetail
+                                }
+                                if (step == 4) {    // 上锁工作票
+                                    sendTicketBusiness(
+                                        true,
+                                        currentModeEvent.bleBean.bleDevice.mac,
+                                        ticketDetail,
+                                        ticketDetail.ticketLockVOList?.filter { it.lockStatus != "2" }
+                                            ?.map { it.lockNfc }?.toMutableList(),
+                                        true
+                                    )
+                                } else if (step == 7) { // 解锁工作票
+                                    sendTicketBusiness(
+                                        false,
+                                        currentModeEvent.bleBean.bleDevice.mac,
+                                        ticketDetail,
+                                        null,
+                                        true
+                                    )
+                                }
+                            }
+                        }
+                    } ?: let {
+                        ModBusController.updateKeyReadyStatus(
+                            currentModeEvent.bleBean.bleDevice.mac, true, 4
+                        )
+                        ModBusController.controlKeyBuckle(
+                            false, currentModeEvent.bleBean.bleDevice.mac
+                        )
+                        LoadingEvent.sendLoadingEvent()
+                        //连上之后没有工作票要下发就断开
+                        BleManager.getInstance().disconnect(currentModeEvent.bleBean.bleDevice)
+                    }
+                }
+            }
+            // 故障模式
+            0x03.toByte() -> {
+                // TODO 上报?
+                PopTip.tip(
+                    "${currentModeEvent.bleBean.bleDevice.mac} : " + "${CommonUtils.getStr(R.string.key_is_in_failure_mode)}"
+                )
+            }
+        }
+    }
+
+    /**
+     * 分配钥匙
+     */
+    fun handleGiveKey(deviceTakeUpdateBO: DeviceTakeUpdate) {
+        BleConnectionManager.getBleDeviceByMac(ModBusController.getKeyByRfid(deviceTakeUpdateBO.nfc)?.mac)
+            ?.let {
+                BleConnectionManager.getCurrentStatus(
+                    2,
+                    BleConnectionManager.getBleDeviceByMac(
+                        ModBusController.getKeyByRfid(
+                            deviceTakeUpdateBO.nfc
+                        )?.mac
+                    )!!.bleDevice
+                ) {
+                    if (!it) {
+                        return@getCurrentStatus
+                    }
+                    logger.warn("handleGiveKey timeout")
+                    ModbusBusinessManager.removeDeviceTake(
+                        DeviceConst.DEVICE_TYPE_KEY,
+                        deviceTakeUpdateBO.nfc
+                    )
+                    ModbusBusinessManager.checkEquipCount(0, true) { keyPair, lockMap ->
+                        if (keyPair == null) {
+                            TipDialog.show(
+                                msg = CommonUtils.getStr(R.string.key_take_error_tip).toString(),
+                                onConfirmClick = {
+                                    DeviceExceptionEvent.sendDeviceExceptionEvent(
+                                        DeviceConst.DEVICE_TYPE_KEY,
+                                        deviceTakeUpdateBO.nfc
+                                    )
+                                })
+                        } else {
+                            ModbusBusinessManager.addDeviceTake(
+                                DeviceConst.DEVICE_TYPE_KEY,
+                                deviceTakeUpdateBO.ticketId,
+                                keyPair.second?.rfid!!
+                            )
+                            handleGiveKey(
+                                DeviceTakeUpdate(
+                                    DeviceConst.DEVICE_TYPE_KEY,
+                                    deviceTakeUpdateBO.ticketId,
+                                    keyPair.second?.rfid!!
+                                )
+                            )
+                        }
+                    }
+                }
+            } ?: run {
+            TipDialog.show(
+                msg = CommonUtils.getStr(R.string.key_take_error_tip).toString(),
+                onConfirmClick = {
+                    DeviceExceptionEvent.sendDeviceExceptionEvent(
+                        DeviceConst.DEVICE_TYPE_KEY,
+                        deviceTakeUpdateBO.nfc
+                    )
+                })
+        }
+    }
+
+    /**
+     * 读取工作票完成情况
+     */
+    private fun getTicketStatusBusiness(
+        mac: String, isNeedLoading: Boolean = false
+    ) {
+        BleConnectionManager.registerConnectListener(mac) { isDone, bleBean ->
+            if (isDone) {
+                Executor.delayOnMain(500) {
+                    getTicketStatusWithRetry(bleBean!!.bleDevice, isNeedLoading)
+                }
+            } else {
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent()
+            }
+        }
+    }
+
+    private fun getTicketStatusWithRetry(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        maxRetries: Int = 3,
+        delayMillis: Long = 500
+    ) {
+        var retryCount = 0
+
+        fun attemptSend() {
+            getTicketStatus(bleDevice, isNeedLoading) { sendRst ->
+                if (!sendRst && retryCount < maxRetries) {
+                    retryCount++
+                    // 等待一段时间后再次尝试
+                    Executor.delayOnMain(delayMillis) {
+                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
+                        attemptSend()
+                    }
+                }
+            }
+        }
+
+        attemptSend()
+    }
+
+    /**
+     * 连接一把存在的可连接的钥匙
+     */
+    fun connectExistsKey(exceptKeyMac: String) {
+        ThreadUtils.runOnIO {
+            // —— 串行请求1 & 2 ——
+            val slotsPage = DataBusiness.getSlotsPage()
+            val keyPage = DataBusiness.getKeyPage()
+            // —— 并行加载字典(或按需串行也行) ——
+            val slotStatus =
+                async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_SLOT_STATUS) }
+            val keyStatus =
+                async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_KEY_STATUS) }
+            val slotType =
+                async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_SLOT_TYPE) }
+
+            // 等待字典加载完成
+            val slotStatusList = slotStatus.await()
+            val keyStatusList = keyStatus.await()
+            val slotTypeList = slotType.await()
+            withContext(Dispatchers.Default) {
+                ModBusController.getOneKey(
+                    slotsPage?.records
+                        ?.filter {
+                            it.slotType == slotTypeList.find { d -> d.dictLabel == "钥匙" }?.dictValue &&
+                                    it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
+                        }?.toMutableList() ?: mutableListOf(),
+                    (keyPage?.records
+                        ?.filter { it.exStatus == keyStatusList.find { d -> d.dictLabel == "异常" }?.dictValue }
+                        ?.map { it.keyNfc ?: "" }
+                        ?.toMutableList() ?: mutableListOf()),
+                    mutableListOf(exceptKeyMac)
+                )
+            }
+        }
+    }
+
+    /**
+     * 下发工作票
+     */
+    private fun sendTicketBusiness(
+        isLock: Boolean,
+        mac: String,
+        ticketDetail: TicketDetailRes,
+        lockList: MutableList<String?>?,
+        isNeedLoading: Boolean = false,
+    ) {
+        BleConnectionManager.registerConnectListener(mac) { isDone, bleBean ->
+            if (!isDone) {
+                sendTicketBusiness(isLock, mac, ticketDetail, lockList, isNeedLoading)
+                return@registerConnectListener
+            }
+            if (bleBean == null) {
+//                ToastUtils.tip(R.string.simple_key_is_not_connected)
+                logger.error("sendTicketBusiness fail : $mac, bleBean is null")
+                return@registerConnectListener
+            }
+            // 单bleBean json赋值
+            bleBean.retryCount = 0
+            bleBean.ticketSend = generateTicketSendJson(isLock, ticketDetail, lockList)
+            bleBean.ticketSend?.let { itJson ->
+                sendTicketWithRetry(itJson, bleBean.bleDevice, isNeedLoading)
+            }
+        }
+    }
+
+    /**
+     * 生成下发工作票Json
+     *
+     * @param vo 工作票详情
+     */
+    private fun generateTicketSendJson(
+        isLock: Boolean, vo: TicketDetailRes, lockList: MutableList<String?>?
+    ): String {
+        logger.info("generateTicketSendJson : $lockList")
+        val bo = WorkTicketSend(
+            cardNo = SPUtils.getLoginUser(SIKCore.getApplication())?.userCardList?.get(0),
+        )
+        CommonUtils.getDiffHours(vo.ticketEndTime)?.let {
+            bo.effectiveTime = it
+        }
+        // 有配置则用配置,没用则填充默认密码
+        bo.password =
+            SPUtils.getLoginUser(SIKCore.getApplication())?.keyCode ?: "123456"
+        val dataBO = WorkTicketSend.DataBO(
+            taskCode = vo.ticketId.toString(), codeId = 1
+        )
+        val taskList = ArrayList<WorkTicketSend.DataBO.DataListBO>()
+        vo.ticketPointsVOList?.let { itList ->
+            itList.forEach { pointVO ->
+                if (vo.noUnlockTicketPointsVOSet?.any { it.pointId == pointVO.pointId } == true) {
+                    return@forEach
+                }
+                val task = WorkTicketSend.DataBO.DataListBO(
+                    dataId = pointVO.pointId?.toInt(),
+                    equipRfidNo = pointVO.pointNfc,
+                    equipName = pointVO.pointName,
+                    target = if (isLock) 0 else 1
+                )
+                if (!isLock) {
+                    task.infoRfidNo = pointVO.lockNfc
+                }
+                pointVO.prePointId?.let {
+                    task.prevId = it.toInt()
+                }
+                // TODO partCode待补充
+                taskList.add(task)
+            }
+        }
+        dataBO.dataList = taskList
+        bo.data = mutableListOf(dataBO)
+        if (isLock) {
+            // TODO 挂锁数组
+            if (!lockList.isNullOrEmpty()) {
+                bo.lockList = mutableListOf()
+                lockList.forEachIndexed { index, s ->
+                    if (s.isNullOrEmpty()) {
+                        logger.warn("Lock nfc is null or empty")
+                        return@forEachIndexed
+                    }
+                    bo.lockList?.add(LockListBO(index + 1, s))
+                }
+            }
+        }
+        // TODO partList 待补充
+        val jsonStr = Gson().toJson(bo)
+        logger.info("json : $jsonStr")
+        return jsonStr
+    }
+
+    /**
+     * 带重试的下发工作票,重试次数3,间隔500ms
+     */
+    private fun sendTicketWithRetry(
+        json: String,
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        maxRetries: Int = 3,
+        delayMillis: Long = 500
+    ) {
+        var retryCount = 0
+
+        fun attemptSend() {
+            sendTicket(json, bleDevice, isNeedLoading) { sendRst ->
+                if (!sendRst && retryCount < maxRetries) {
+                    retryCount++
+                    // 等待一段时间后再次尝试
+                    Executor.delayOnMain(delayMillis) {
+                        logger.info("Retry attempt, mac : ${bleDevice.mac}, retryCount : $retryCount")
+                        attemptSend()
+                    }
+                }
+            }
+        }
+
+        attemptSend()
+    }
+
+    private fun sendTicket(
+        jsonStr: String,
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        processCallback: ((Boolean) -> Unit)? = null
+    ) {
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+            CommonUtils.getStr(R.string.start_to_send_ticket),
+            true
+        )
+        BleCmdManager.sendWorkTicket(
+            jsonStr, bleDevice = bleDevice, callback = object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    logger.info("sendTicket success")
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(R.string.sending_ticket),
+                        true
+                    )
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    logger.error("sendTicket fail : ${bleDevice.mac}")
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(R.string.send_ticket_fail),
+                        true
+                    )
+                    processCallback?.invoke(false)
+                }
+            })
+    }
+}

+ 43 - 0
ui-base/src/main/java/com/grkj/ui_base/business/DataBusiness.kt

@@ -0,0 +1,43 @@
+package com.grkj.ui_base.business
+
+import com.grkj.data.HardwareRepository
+import com.grkj.domain.entity.res.CabinetSlotsRes
+import com.grkj.domain.entity.res.KeyPageRes
+import com.grkj.domain.entity.res.LockPageRes
+import com.grkj.domain.repository.IHardwareRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import kotlin.coroutines.resume
+
+/**
+ * 数据业务
+ */
+object DataBusiness {
+    private val hardwareRepository: IHardwareRepository by lazy { HardwareRepository() }
+    // 1. 把 NetApi.get…Page 包成 suspend 函数
+    suspend fun getSlotsPage(): CabinetSlotsRes? = suspendCancellableCoroutine { cont ->
+        hardwareRepository.getIsLockCabinetSlotsPage { slots ->
+            cont.resume(slots)
+        }
+    }
+
+    suspend fun getLocksPage(): LockPageRes? = suspendCancellableCoroutine { cont ->
+        hardwareRepository.getIsLockPage { locks ->
+            cont.resume(locks)
+        }
+    }
+
+    suspend fun getKeyPage(): KeyPageRes? = suspendCancellableCoroutine { cont ->
+        hardwareRepository.getIsKeyPage { keys ->
+            cont.resume(keys)
+        }
+    }
+
+    // 2. 把原本同步的字典查询留在 IO 线程
+    suspend fun <T> fetchDict(key: String): List<T> =suspendCancellableCoroutine { cont ->
+        hardwareRepository.getDictData(key) { dictData ->
+            cont.resume(dictData)
+        }
+    }
+}

+ 243 - 0
ui-base/src/main/java/com/grkj/ui_base/business/ModbusBusinessManager.kt

@@ -0,0 +1,243 @@
+package com.grkj.ui_base.business
+
+import androidx.appcompat.app.AppCompatActivity
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.google.gson.Gson
+import com.grkj.data.HardwareRepository
+import com.grkj.domain.entity.local.DeviceTakeUpdate
+import com.grkj.domain.entity.req.LockTakeUpdateReq
+import com.grkj.domain.entity.res.CommonDictRes
+import com.grkj.domain.entity.res.TicketDetailRes
+import com.grkj.domain.repository.IHardwareRepository
+import com.grkj.ui_base.R
+import com.grkj.ui_base.data.DictConstants
+import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.Executor
+import com.grkj.ui_base.utils.SPUtils
+import com.grkj.ui_base.utils.ble.BleConnectionManager
+import com.grkj.ui_base.utils.ble.BleConst
+import com.grkj.ui_base.utils.event.DeviceTakeUpdateEvent
+import com.grkj.ui_base.utils.event.LoadingEvent
+import com.grkj.ui_base.utils.event.UpdateTicketProgressEvent
+import com.grkj.ui_base.utils.extension.serialNo
+import com.grkj.ui_base.utils.modbus.DeviceConst
+import com.grkj.ui_base.utils.modbus.DockBean
+import com.grkj.ui_base.utils.modbus.ModBusController
+import com.kongzue.dialogx.dialogs.PopTip
+import com.sik.sikcore.SIKCore
+import com.sik.sikcore.thread.ThreadUtils
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.withContext
+import org.slf4j.LoggerFactory
+import kotlin.collections.get
+
+/**
+ * 硬件业务管理
+ */
+object ModbusBusinessManager {
+    private val logger = LoggerFactory.getLogger(ModbusBusinessManager::class.java)
+
+    /**
+     * 硬件仓储
+     */
+    private val hardwareRepository: IHardwareRepository by lazy { HardwareRepository() }
+
+    // 设备待取列表(需要报给后台的列表,等实际取完再上报)
+    val mDeviceTakeList = mutableListOf<DeviceTakeUpdate>()
+
+    /**
+     * 添加待更新取出状态的设备
+     */
+    fun addDeviceTake(deviceType: Int, ticketId: Long, nfc: String?) {
+        logger.info("addDeviceTake : $deviceType - $ticketId - $nfc")
+        mDeviceTakeList.removeIf { it.deviceType == deviceType && it.nfc == nfc }
+        mDeviceTakeList.add(DeviceTakeUpdate(deviceType, ticketId, nfc!!))
+    }
+
+    /**
+     * 移除设备取出
+     */
+    fun removeDeviceTake(deviceType: Int, nfc: String?) {
+        logger.info("removeDeviceTake : $deviceType - $nfc")
+        mDeviceTakeList.removeIf { it.deviceType == deviceType && it.nfc == nfc }
+    }
+
+    /**
+     * 处理设备取出
+     */
+    private fun handleDeviceTake(deviceTakeUpdateBO: DeviceTakeUpdateEvent, rfid: String? = null) {
+        logger.info("$deviceTakeUpdateBO")
+        when (deviceTakeUpdateBO.deviceType) {
+            // 钥匙
+            0 -> {
+                mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.nfc == deviceTakeUpdateBO.nfc }
+                    ?.let { info ->
+                        LoadingEvent.sendLoadingEvent()
+                        SPUtils.takeKey(info.ticketId)
+                        hardwareRepository.updateKeyTake(
+                            info.ticketId, info.nfc, SIKCore.getApplication().serialNo()!!
+                        ) { isSuccess ->
+                            if (isSuccess) {
+                                mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.nfc == info.nfc }
+                                UpdateTicketProgressEvent.sendUpdateTicketProgressEvent(info.ticketId)
+                                //钥匙取出之后重新再连一把钥匙待机
+                                ModBusController.getKeyByRfid(
+                                    info.nfc
+                                )?.mac?.let {
+                                    BleConnectionManager.unregisterConnectListener(it)
+                                }
+                                //待机数不够就再连一把,但不能是原来那把
+                                if (BleManager.getInstance().allConnectedDevice.size < BleConst.MAX_KEY_STAND_BY) {
+                                    ModBusController.getKeyByRfid(
+                                        info.nfc
+                                    )?.mac?.let {
+                                        BleBusinessManager.connectExistsKey(
+                                            it
+                                        )
+                                    }
+                                }
+                            }
+                        }
+                    } ?: LoadingEvent.sendLoadingEvent()
+            }
+            // 挂锁
+            1 -> {
+                mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.nfc == deviceTakeUpdateBO.nfc }
+                    ?.let { info ->
+                        hardwareRepository.updateLockTake(
+                            mutableListOf(
+                                LockTakeUpdateReq(
+                                    info.ticketId, info.nfc, SIKCore.getApplication().serialNo()!!
+                                )
+                            )
+                        ) { isSuccess ->
+                            Executor.runOnMain {
+                                if (isSuccess == false) {
+                                    logger.error("Lock take report fail")
+                                    PopTip.tip(CommonUtils.getStr(R.string.lock_take_report_fail))
+                                    SPUtils.saveTicketTakeLockException(info.ticketId)
+                                    mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.nfc == info.nfc }
+                                    mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
+                                    LoadingEvent.sendLoadingEvent()
+                                    return@runOnMain
+                                }
+                                // 检查是不是要发钥匙了
+                                mDeviceTakeList.removeIf { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.nfc == info.nfc }
+                                // 检查当前工作票是否取完挂锁
+                                if (mDeviceTakeList.any { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.ticketId == info.ticketId }) {
+                                    logger.info("Waiting all locks to take out")
+                                    LoadingEvent.sendLoadingEvent(
+                                        SIKCore.getApplication().getString(
+                                            R.string.take_out_lock_tip,
+                                            mDeviceTakeList.count { it.deviceType == DeviceConst.DEVICE_TYPE_LOCK && it.ticketId == info.ticketId }),
+                                        true
+                                    )
+                                    PopTip.tip(R.string.take_out_rest_locks)
+                                    return@runOnMain
+                                } else {
+                                    logger.info("All locks are taken")
+                                    LoadingEvent.sendLoadingEvent()
+                                }
+                                if (SPUtils.getTicketTakeLockException(info.ticketId)) {
+                                    PopTip.tip(R.string.current_ticket_report_lock_take_exception_tip)
+                                    return@runOnMain
+                                }
+                                // 检查有无当前工作票的钥匙
+                                mDeviceTakeList.find { it.deviceType == DeviceConst.DEVICE_TYPE_KEY && it.ticketId == info.ticketId }
+                                    ?.let { itKey ->
+                                        LoadingEvent.sendLoadingEvent(
+                                            CommonUtils.getStr(R.string.ble_connecting), true
+                                        )
+                                        BleBusinessManager.handleGiveKey(itKey)
+                                    }
+                            }
+                        }
+                    }
+            }
+        }
+    }
+
+
+    // 3. 重写 checkEquipCount
+    fun checkEquipCount(
+        needLockCount: Int,
+        isNeedKey: Boolean,
+        callBack: (Pair<Byte, DockBean.KeyBean?>?, MutableMap<Byte, MutableList<DockBean.LockBean>>) -> Unit
+    ) {
+        // 你可以改成接收 CoroutineScope 或者直接在全局 Scope 启动
+        ThreadUtils.runOnMain {
+            LoadingEvent.sendLoadingEvent(CommonUtils.getStr(R.string.check_key_and_lock))
+            try {
+                // —— 串行请求1 & 2 ——
+                val slotsPage = DataBusiness.getSlotsPage()
+                val locksPage = DataBusiness.getLocksPage()
+
+                // —— 并行加载字典(或按需串行也行) ——
+                val lockStatus =
+                    async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_PAD_LOCK_STATUS) }
+                val slotStatus =
+                    async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_SLOT_STATUS) }
+                val slotType =
+                    async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_SLOT_TYPE) }
+                val keyStatus =
+                    async { DataBusiness.fetchDict<CommonDictRes>(DictConstants.KEY_KEY_STATUS) }
+
+                // 等待字典加载完成
+                val lockStatusList = lockStatus.await()
+                val slotStatusList = slotStatus.await()
+                val slotTypeList = slotType.await()
+                val keyStatusList = keyStatus.await()
+
+                // —— 在 Default 线程做计算密集操作 ——
+                val lockMap = withContext(Dispatchers.Default) {
+                    ModBusController.getLocks(
+                        needLockCount,
+                        slotsPage?.records?.filter {
+                            it.slotType == slotTypeList.find { d -> d.dictLabel == "锁" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
+                        }?.toMutableList() ?: mutableListOf(),
+                        locksPage?.records?.filter { it.exStatus == lockStatusList.find { d -> d.dictLabel == "异常" }?.dictValue }
+                            ?.map { it.lockNfc ?: "" }?.toMutableList() ?: mutableListOf()
+                    )
+                }
+
+                val actualLockCount = lockMap.values.sumBy { it.size }
+                // 如果锁不够,提前清空并立刻返回
+                if (actualLockCount < needLockCount) {
+                    PopTip.tip(
+                        R.string.lock_is_not_enough
+                    )
+                    callBack(null, mutableMapOf())
+                    return@runOnMain
+                }
+
+                // —— 如果需钥匙,再请求并计算 ——
+                var keyPair: Pair<Byte, DockBean.KeyBean?>? = null
+                if (isNeedKey) {
+                    val keyPage = DataBusiness.getKeyPage()
+                    keyPair = withContext(Dispatchers.Default) {
+                        ModBusController.getOneKey(
+                            slotsPage?.records?.filter {
+                                it.slotType == slotTypeList.find { d -> d.dictLabel == "钥匙" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
+                            }?.toMutableList() ?: mutableListOf(),
+                            keyPage?.records?.filter { it.exStatus == keyStatusList.find { d -> d.dictLabel == "异常" }?.dictValue }
+                                ?.map { it.keyNfc ?: "" }?.toMutableList() ?: mutableListOf()
+                        )
+                    }
+                    if (keyPair == null) {
+                        PopTip.tip(R.string.no_available_key)
+                    }
+                }
+                // —— 全部计算完毕,在主线程一次性回调 ——
+                callBack(keyPair, lockMap)
+
+            } catch (e: Exception) {
+                // 根据需求处理异常,或把异常信息也通过 callback 返回
+                LoadingEvent.sendLoadingEvent()
+                e.printStackTrace()
+                PopTip.tip("检查设备异常:${e.message}")
+            }
+        }
+    }
+}

+ 11 - 0
ui-base/src/main/java/com/grkj/ui_base/config/ISCSConfig.kt

@@ -0,0 +1,11 @@
+package com.grkj.ui_base.config
+
+/**
+ * 锁柜配置
+ */
+object ISCSConfig {
+    /**
+     * 是否是测试模式
+     */
+    var isTestMode: Boolean = false
+}

+ 29 - 0
ui-base/src/main/java/com/grkj/ui_base/data/Constants.kt

@@ -0,0 +1,29 @@
+package com.grkj.ui_base.data
+
+import android.Manifest
+
+/**
+ * 常量参数
+ */
+object Constants {
+    /**
+     * 需要的权限
+     */
+    val needPermission = listOf<String>(
+        Manifest.permission.BLUETOOTH,
+        Manifest.permission.BLUETOOTH_ADMIN,
+        Manifest.permission.BLUETOOTH_CONNECT,
+        Manifest.permission.BLUETOOTH_SCAN,
+        Manifest.permission.BLUETOOTH_ADVERTISE,
+        Manifest.permission.BLUETOOTH_PRIVILEGED,
+        Manifest.permission.READ_EXTERNAL_STORAGE,
+        Manifest.permission.WRITE_EXTERNAL_STORAGE
+    )
+
+    const val USER_TYPE_LOCKER = "0"                // 上锁人
+    const val USER_TYPE_COLOCKER = "1"              // 共锁人
+    /*************************  虹软ArcSoft  *************************/
+    const val APP_ID = "FTN3G4pk8n2RKwjD955sRapRjbYQFefwhHd4sBZMYEz6"
+    const val SDK_KEY = "BjJomNU2bQc2SYhT7NNqwvFd3WD4wTqecifNcDRuKD5G"
+
+}

+ 51 - 0
ui-base/src/main/java/com/grkj/ui_base/data/DictConstants.kt

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

+ 42 - 0
ui-base/src/main/java/com/grkj/ui_base/data/EventConstants.kt

@@ -0,0 +1,42 @@
+package com.grkj.ui_base.data
+
+/**
+ * 通知常量
+ */
+object EventConstants {
+    //---------------------------通用通知------------------------
+    /**
+     * 通知代码
+     */
+    const val EVENT_LOADING_CODE: Int = 100_000_001
+
+    /**
+     * 更新作业票
+     */
+    const val EVENT_UPDATE_TICKET_PROGRESS: Int = 100_000_002
+
+    //---------------------------作业票------------------------
+    const val EVENT_GET_TICKET_STATUS: Int = 100_001_001
+
+
+    //---------------------------开关采集板------------------------
+    const val EVENT_SWITCH_COLLECTION_UPDATE: Int = 100_002_001
+
+    //---------------------------硬件------------------------
+    /**
+     * 硬件异常
+     */
+    const val EVENT_DEVICE_EXCEPTION: Int = 100_003_001
+
+    /**
+     * 当前模式
+     */
+    const val EVENT_CURRENT_MODE: Int = 100_003_002
+
+    /**
+     * 硬件取出
+     */
+    const val EVENT_DEVICE_TAKE: Int = 100_003_003
+
+
+}

+ 16 - 0
ui-base/src/main/java/com/grkj/ui_base/data/MMKVConstants.kt

@@ -0,0 +1,16 @@
+package com.grkj.ui_base.data
+
+/**
+ * 持久化存储的KEY
+ */
+object MMKVConstants {
+    /**
+     * 基座配置
+     */
+    const val KEY_DOCK_CONFIG = "key_dock_config"
+
+    /**
+     * 串口配置
+     */
+    const val KEY_PORT_CONFIG = "key_port_config"
+}

+ 100 - 0
ui-base/src/main/java/com/grkj/ui_base/dialog/TipDialog.kt

@@ -0,0 +1,100 @@
+package com.grkj.ui_base.dialog
+
+import android.content.Context
+import android.view.View
+import com.grkj.ui_base.R
+import com.grkj.ui_base.databinding.DialogTipBinding
+import com.kongzue.dialogx.dialogs.CustomDialog
+import com.kongzue.dialogx.interfaces.OnBindView
+import com.sik.sikcore.activity.ActivityTracker
+
+class TipDialog : OnBindView<CustomDialog>(R.layout.dialog_tip) {
+    enum class DialogType {
+        INFO,//通知
+        WARN,//警告
+        ERROR//错误
+    }
+
+    private var onConfirmClick: () -> Unit = {}
+    private var onCancelClick: () -> Unit = {}
+
+    private lateinit var binding: DialogTipBinding
+    override fun onBind(dialog: CustomDialog?, contentView: View) {
+        binding = DialogTipBinding.bind(contentView)
+    }
+
+    /**
+     * 设置标题
+     */
+    fun setTitle(title: String) {
+
+    }
+
+    /**
+     * 设置提示内容
+     */
+    fun setMessage(msg: String) {
+
+    }
+
+    /**
+     * 设置弹窗类型
+     */
+    fun setDialogType(dialogType: DialogType) {
+
+    }
+
+    /**
+     * 是否显示取消 设置倒计时必显示
+     */
+    fun showCancel(showCancel: Boolean) {
+
+    }
+
+    /**
+     * 设置倒计时
+     */
+    fun setCountDownTime(countDownTime: Int) {
+
+    }
+
+    /**
+     * 设置确定事件
+     */
+    fun setOnConfirmClickListener(onConfirmClick: () -> Unit) {
+        this.onConfirmClick = onConfirmClick
+    }
+
+    /**
+     * 设置取消事件
+     */
+    fun setOnCancelClickListener(onCancelClick: () -> Unit) {
+        this.onCancelClick = onConfirmClick
+    }
+
+
+    companion object {
+        /**
+         * 显示
+         */
+        fun show(
+            title: String = "操作提醒",
+            msg: String,
+            dialogType: DialogType = DialogType.INFO,
+            showCancel: Boolean = true,
+            countDownTime: Int = -1,
+            onConfirmClick: () -> Unit = {},
+            onCancelClick: () -> Unit = {}
+        ) {
+            CustomDialog.show(TipDialog().apply {
+                setTitle(title)
+                setMessage(msg)
+                setDialogType(dialogType)
+                showCancel(showCancel)
+                setCountDownTime(countDownTime)
+                setOnConfirmClickListener(onConfirmClick)
+                setOnCancelClickListener(onCancelClick)
+            })
+        }
+    }
+}

+ 202 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ArcSoftUtil.kt

@@ -0,0 +1,202 @@
+package com.grkj.ui_base.utils
+
+import android.Manifest
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Point
+import android.hardware.Camera
+import android.util.DisplayMetrics
+import android.view.View
+import android.view.WindowManager
+import com.arcsoft.face.ActiveFileInfo
+import com.arcsoft.face.AgeInfo
+import com.arcsoft.face.ErrorInfo
+import com.arcsoft.face.Face3DAngle
+import com.arcsoft.face.FaceEngine
+import com.arcsoft.face.FaceInfo
+import com.arcsoft.face.GenderInfo
+import com.arcsoft.face.LivenessInfo
+import com.arcsoft.face.enums.DetectFaceOrientPriority
+import com.arcsoft.face.enums.DetectMode
+import com.grkj.ui_base.R
+import com.grkj.ui_base.data.Constants
+import com.grkj.ui_base.utils.face.arcsoft.CameraHelper
+import com.grkj.ui_base.utils.face.arcsoft.CameraListener
+import com.grkj.ui_base.utils.face.arcsoft.NV21ToBitmap
+import com.kongzue.dialogx.dialogs.PopTip
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * 虹软工具类
+ */
+object ArcSoftUtil {
+    private val logger: Logger = LoggerFactory.getLogger(ArcSoftUtil::class.java)
+    private var cameraHelper: CameraHelper? = null
+    private var previewSize: Camera.Size? = null
+    private val rgbCameraId = Camera.CameraInfo.CAMERA_FACING_BACK
+    private var faceEngine: FaceEngine? = null
+    private var afCode = -1
+    private val processMask: Int =
+        FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS
+
+    private const val ACTION_REQUEST_PERMISSIONS: Int = 0x001
+    var isActivated = false
+
+    /**
+     * 所需的所有权限信息
+     */
+    private val NEEDED_PERMISSIONS: Array<String?> = arrayOf(
+        Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE
+    )
+
+    fun checkActiveStatus(context: Context) {
+        val activeCode = FaceEngine.activeOnline(context, Constants.APP_ID, Constants.SDK_KEY)
+        when (activeCode) {
+            ErrorInfo.MOK -> {
+                isActivated = true
+                logger.info("checkActiveStatus : active success")
+            }
+
+            ErrorInfo.MERR_ASF_ALREADY_ACTIVATED -> {
+                isActivated = true
+                logger.info("checkActiveStatus : already activated")
+            }
+
+            else -> {
+                isActivated = false
+                logger.error("checkActiveStatus : active failed $activeCode")
+                PopTip.tip(CommonUtils.getStr(R.string.face_active_fail))
+            }
+        }
+        val activeFileInfo = ActiveFileInfo()
+        val res = FaceEngine.getActiveFileInfo(context, activeFileInfo)
+        if (res == ErrorInfo.MOK) {
+            logger.info("checkActiveStatus:  getActiveFileInfo: $activeFileInfo")
+        }
+    }
+
+    fun initEngine(context: Context) {
+        faceEngine = FaceEngine()
+        afCode = faceEngine!!.init(
+            context,
+            DetectMode.ASF_DETECT_MODE_VIDEO,
+            DetectFaceOrientPriority.valueOf("ASF_OP_0_ONLY"),
+            16,
+            20,
+            FaceEngine.ASF_FACE_DETECT or FaceEngine.ASF_AGE or FaceEngine.ASF_FACE3DANGLE or FaceEngine.ASF_GENDER or FaceEngine.ASF_LIVENESS
+        )
+        logger.info("initEngine:  init: $afCode")
+        if (afCode != ErrorInfo.MOK) {
+            PopTip.tip(CommonUtils.getStr(R.string.face_active_fail))
+        }
+    }
+
+    fun unInitEngine() {
+        if (afCode == 0) {
+            afCode = faceEngine!!.unInit()
+            logger.info("unInitEngine: $afCode")
+        }
+    }
+
+    fun initCamera(
+        context: Context, windowManager: WindowManager, preview: View, callBack: (Bitmap?) -> Unit
+    ) {
+        val metrics = DisplayMetrics()
+        windowManager.defaultDisplay.getMetrics(metrics)
+
+        val cameraListener: CameraListener = object : CameraListener {
+            override fun onCameraOpened(
+                camera: Camera, cameraId: Int, displayOrientation: Int, isMirror: Boolean
+            ) {
+                logger.info("onCameraOpened: $cameraId  $displayOrientation $isMirror")
+                previewSize = camera.parameters.previewSize
+            }
+
+
+            override fun onPreview(nv21: ByteArray?, camera: Camera?) {
+                val faceInfoList: List<FaceInfo> = ArrayList()
+                var code = faceEngine!!.detectFaces(
+                    nv21,
+                    previewSize!!.width,
+                    previewSize!!.height,
+                    FaceEngine.CP_PAF_NV21,
+                    faceInfoList
+                )
+                if (code == ErrorInfo.MOK && faceInfoList.size > 0) {
+                    code = faceEngine!!.process(
+                        nv21,
+                        previewSize!!.width,
+                        previewSize!!.height,
+                        FaceEngine.CP_PAF_NV21,
+                        faceInfoList,
+                        processMask
+                    )
+                    if (code != ErrorInfo.MOK) {
+                        return
+                    }
+                } else {
+                    return
+                }
+
+                val ageInfoList: List<AgeInfo> = ArrayList()
+                val genderInfoList: List<GenderInfo> = ArrayList()
+                val face3DAngleList: List<Face3DAngle> = ArrayList()
+                val faceLivenessInfoList: List<LivenessInfo> = ArrayList()
+                val ageCode = faceEngine!!.getAge(ageInfoList)
+                val genderCode = faceEngine!!.getGender(genderInfoList)
+                val face3DAngleCode = faceEngine!!.getFace3DAngle(face3DAngleList)
+                val livenessCode = faceEngine!!.getLiveness(faceLivenessInfoList)
+
+                // 有其中一个的错误码不为ErrorInfo.MOK,return
+                if ((ageCode or genderCode or face3DAngleCode or livenessCode) != ErrorInfo.MOK) {
+                    return
+                }
+
+                // 自己加的,必须有活体检测
+                if (faceLivenessInfoList.none { it.liveness == LivenessInfo.ALIVE }) {
+                    return
+                }
+                val bitmap = NV21ToBitmap(context).nv21ToBitmap(
+                    nv21, previewSize!!.width, previewSize!!.height
+                )
+                logger.info("识别结果 : ${bitmap == null} - $faceInfoList")
+                callBack(bitmap)
+            }
+
+            override fun onCameraClosed() {
+                logger.info("onCameraClosed: ")
+            }
+
+            override fun onCameraError(e: Exception) {
+                logger.info("onCameraError: " + e.message)
+            }
+
+            override fun onCameraConfigurationChanged(cameraID: Int, displayOrientation: Int) {
+                logger.info("onCameraConfigurationChanged: $cameraID  $displayOrientation")
+            }
+        }
+        cameraHelper = CameraHelper.Builder()
+            .previewViewSize(Point(preview.measuredWidth, preview.measuredHeight))
+            .rotation(windowManager.defaultDisplay.rotation)
+            .specificCameraId(rgbCameraId ?: Camera.CameraInfo.CAMERA_FACING_FRONT).isMirror(false)
+            .previewOn(preview).cameraListener(cameraListener).build()
+        cameraHelper!!.init()
+        cameraHelper!!.start()
+    }
+
+    fun start(
+        context: Context, windowManager: WindowManager, preview: View, callBack: (Bitmap?) -> Unit
+    ) {
+        initEngine(context)
+        initCamera(context, windowManager, preview, callBack)
+    }
+
+    fun stop() {
+        if (cameraHelper != null) {
+            cameraHelper!!.release()
+            cameraHelper = null
+        }
+        unInitEngine()
+    }
+}

+ 122 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/CRC16.java

@@ -0,0 +1,122 @@
+package com.grkj.ui_base.utils;
+
+public class CRC16 {
+
+    private static final byte[] auchCRCHi = { 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x01,
+            (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00, (byte) 0xC1,
+            (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80,
+            (byte) 0x41, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x00, (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40, (byte) 0x01, (byte) 0xC0,
+            (byte) 0x80, (byte) 0x41, (byte) 0x00, (byte) 0xC1, (byte) 0x81,
+            (byte) 0x40, (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41,
+            (byte) 0x01, (byte) 0xC0, (byte) 0x80, (byte) 0x41, (byte) 0x00,
+            (byte) 0xC1, (byte) 0x81, (byte) 0x40 };
+
+    private static final byte[] auchCRCLo = { (byte) 0x00, (byte) 0xC0, (byte) 0xC1,
+            (byte) 0x01, (byte) 0xC3, (byte) 0x03, (byte) 0x02, (byte) 0xC2,
+            (byte) 0xC6, (byte) 0x06, (byte) 0x07, (byte) 0xC7, (byte) 0x05,
+            (byte) 0xC5, (byte) 0xC4, (byte) 0x04, (byte) 0xCC, (byte) 0x0C,
+            (byte) 0x0D, (byte) 0xCD, (byte) 0x0F, (byte) 0xCF, (byte) 0xCE,
+            (byte) 0x0E, (byte) 0x0A, (byte) 0xCA, (byte) 0xCB, (byte) 0x0B,
+            (byte) 0xC9, (byte) 0x09, (byte) 0x08, (byte) 0xC8, (byte) 0xD8,
+            (byte) 0x18, (byte) 0x19, (byte) 0xD9, (byte) 0x1B, (byte) 0xDB,
+            (byte) 0xDA, (byte) 0x1A, (byte) 0x1E, (byte) 0xDE, (byte) 0xDF,
+            (byte) 0x1F, (byte) 0xDD, (byte) 0x1D, (byte) 0x1C, (byte) 0xDC,
+            (byte) 0x14, (byte) 0xD4, (byte) 0xD5, (byte) 0x15, (byte) 0xD7,
+            (byte) 0x17, (byte) 0x16, (byte) 0xD6, (byte) 0xD2, (byte) 0x12,
+            (byte) 0x13, (byte) 0xD3, (byte) 0x11, (byte) 0xD1, (byte) 0xD0,
+            (byte) 0x10, (byte) 0xF0, (byte) 0x30, (byte) 0x31, (byte) 0xF1,
+            (byte) 0x33, (byte) 0xF3, (byte) 0xF2, (byte) 0x32, (byte) 0x36,
+            (byte) 0xF6, (byte) 0xF7, (byte) 0x37, (byte) 0xF5, (byte) 0x35,
+            (byte) 0x34, (byte) 0xF4, (byte) 0x3C, (byte) 0xFC, (byte) 0xFD,
+            (byte) 0x3D, (byte) 0xFF, (byte) 0x3F, (byte) 0x3E, (byte) 0xFE,
+            (byte) 0xFA, (byte) 0x3A, (byte) 0x3B, (byte) 0xFB, (byte) 0x39,
+            (byte) 0xF9, (byte) 0xF8, (byte) 0x38, (byte) 0x28, (byte) 0xE8,
+            (byte) 0xE9, (byte) 0x29, (byte) 0xEB, (byte) 0x2B, (byte) 0x2A,
+            (byte) 0xEA, (byte) 0xEE, (byte) 0x2E, (byte) 0x2F, (byte) 0xEF,
+            (byte) 0x2D, (byte) 0xED, (byte) 0xEC, (byte) 0x2C, (byte) 0xE4,
+            (byte) 0x24, (byte) 0x25, (byte) 0xE5, (byte) 0x27, (byte) 0xE7,
+            (byte) 0xE6, (byte) 0x26, (byte) 0x22, (byte) 0xE2, (byte) 0xE3,
+            (byte) 0x23, (byte) 0xE1, (byte) 0x21, (byte) 0x20, (byte) 0xE0,
+            (byte) 0xA0, (byte) 0x60, (byte) 0x61, (byte) 0xA1, (byte) 0x63,
+            (byte) 0xA3, (byte) 0xA2, (byte) 0x62, (byte) 0x66, (byte) 0xA6,
+            (byte) 0xA7, (byte) 0x67, (byte) 0xA5, (byte) 0x65, (byte) 0x64,
+            (byte) 0xA4, (byte) 0x6C, (byte) 0xAC, (byte) 0xAD, (byte) 0x6D,
+            (byte) 0xAF, (byte) 0x6F, (byte) 0x6E, (byte) 0xAE, (byte) 0xAA,
+            (byte) 0x6A, (byte) 0x6B, (byte) 0xAB, (byte) 0x69, (byte) 0xA9,
+            (byte) 0xA8, (byte) 0x68, (byte) 0x78, (byte) 0xB8, (byte) 0xB9,
+            (byte) 0x79, (byte) 0xBB, (byte) 0x7B, (byte) 0x7A, (byte) 0xBA,
+            (byte) 0xBE, (byte) 0x7E, (byte) 0x7F, (byte) 0xBF, (byte) 0x7D,
+            (byte) 0xBD, (byte) 0xBC, (byte) 0x7C, (byte) 0xB4, (byte) 0x74,
+            (byte) 0x75, (byte) 0xB5, (byte) 0x77, (byte) 0xB7, (byte) 0xB6,
+            (byte) 0x76, (byte) 0x72, (byte) 0xB2, (byte) 0xB3, (byte) 0x73,
+            (byte) 0xB1, (byte) 0x71, (byte) 0x70, (byte) 0xB0, (byte) 0x50,
+            (byte) 0x90, (byte) 0x91, (byte) 0x51, (byte) 0x93, (byte) 0x53,
+            (byte) 0x52, (byte) 0x92, (byte) 0x96, (byte) 0x56, (byte) 0x57,
+            (byte) 0x97, (byte) 0x55, (byte) 0x95, (byte) 0x94, (byte) 0x54,
+            (byte) 0x9C, (byte) 0x5C, (byte) 0x5D, (byte) 0x9D, (byte) 0x5F,
+            (byte) 0x9F, (byte) 0x9E, (byte) 0x5E, (byte) 0x5A, (byte) 0x9A,
+            (byte) 0x9B, (byte) 0x5B, (byte) 0x99, (byte) 0x59, (byte) 0x58,
+            (byte) 0x98, (byte) 0x88, (byte) 0x48, (byte) 0x49, (byte) 0x89,
+            (byte) 0x4B, (byte) 0x8B, (byte) 0x8A, (byte) 0x4A, (byte) 0x4E,
+            (byte) 0x8E, (byte) 0x8F, (byte) 0x4F, (byte) 0x8D, (byte) 0x4D,
+            (byte) 0x4C, (byte) 0x8C, (byte) 0x44, (byte) 0x84, (byte) 0x85,
+            (byte) 0x45, (byte) 0x87, (byte) 0x47, (byte) 0x46, (byte) 0x86,
+            (byte) 0x82, (byte) 0x42, (byte) 0x43, (byte) 0x83, (byte) 0x41,
+            (byte) 0x81, (byte) 0x80, (byte) 0x40 };
+
+    public static int crc16(byte[] puchMsg, int from, int to) {
+        byte uchCRCHi = (byte) 0xFF;
+        byte uchCRCLo = (byte) 0xFF;
+        for (int i = from; i < to; i++) {
+            int uIndex = (uchCRCHi ^ puchMsg[i]) & 0xff;
+            uchCRCHi = (byte) (uchCRCLo ^ auchCRCHi[uIndex]);
+            uchCRCLo = auchCRCLo[uIndex];
+        }
+        return ((((int) uchCRCHi) << 8 | (((int) uchCRCLo) & 0xff))) & 0xffff;
+    }
+
+}

+ 77 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/CommonUtils.kt

@@ -0,0 +1,77 @@
+package com.grkj.ui_base.utils
+
+import android.Manifest.permission.ACCESS_COARSE_LOCATION
+import android.Manifest.permission.ACCESS_FINE_LOCATION
+import android.Manifest.permission.BLUETOOTH_ADVERTISE
+import android.Manifest.permission.BLUETOOTH_CONNECT
+import android.Manifest.permission.BLUETOOTH_SCAN
+import android.content.Context
+import android.os.Build
+import androidx.appcompat.app.AppCompatActivity
+import com.sik.sikcore.SIKCore
+import java.text.SimpleDateFormat
+import java.util.Locale
+import kotlin.let
+import kotlin.text.isNullOrEmpty
+import kotlin.text.toRegex
+
+object CommonUtils {
+
+    val hexRegex = "^[0-9A-Fa-f]+$".toRegex()
+
+    /**
+     * dip转像素
+     */
+    fun dip2px(dpValue: Float): Int {
+        val density = SIKCore.getApplication().resources.displayMetrics.density
+        return (dpValue * density + 0.5f).toInt()
+    }
+
+    /**
+     * 像素转dip
+     */
+    fun px2dip(pxValue: Float): Float {
+        val density = SIKCore.getApplication().resources.displayMetrics.density
+        return pxValue / density
+    }
+
+    /**
+     * 获取资源文本
+     */
+    fun getStr(textId: Int, ctx: Context? = null): String? {
+        return ctx?.resources?.getString(textId) ?: let {
+            SIKCore.getApplication().applicationContext?.resources?.getString(textId)
+        }
+    }
+
+    /**
+     * 计算时差
+     */
+    fun getDiffHours(dateString: String?): Int? {
+        if (dateString.isNullOrEmpty()) {
+            return null
+        }
+        val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault())
+        val date = format.parse(dateString)
+        if (date != null) {
+            // 将 Date 对象转换为时间戳(毫秒)
+            val timestamp = date.time
+            // 获取当前时间的时间戳
+            val currentTimestamp = System.currentTimeMillis()
+            // 计算时间差(毫秒)
+            val timeDifferenceInMillis = currentTimestamp - timestamp
+            // 将时间差转换为小时数,向下取整
+            val hoursDifference = (timeDifferenceInMillis / (1000 * 60 * 60)).toInt()
+
+            return hoursDifference
+        }
+        return null
+    }
+
+    /**
+     * 是否是Hex
+     */
+    fun isValidHex(hexString: String): Boolean {
+        return hexRegex.matches(hexString)
+    }
+}

+ 298 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/Executor.kt

@@ -0,0 +1,298 @@
+package com.grkj.ui_base.utils
+
+import android.os.Handler
+import android.os.Looper
+import kotlinx.coroutines.*
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.io.PrintWriter
+import java.io.StringWriter
+import kotlin.run
+
+object Executor {
+
+    private val mainScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
+    private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+
+    val mainHandler: Handler = Handler(Looper.getMainLooper())
+
+    @Deprecated("不再使用,仅为兼容保留,避免误用 Handler")
+    val ioHandler: Handler
+        get() = throw UnsupportedOperationException("ioHandler 已被协程替代")
+
+    fun runOnMain(run: Runnable) {
+        mainScope.launch {
+            run.run()
+        }
+    }
+
+    fun delayOnMain(delayMills: Long, run: Runnable) {
+        mainScope.launch {
+            delay(delayMills)
+            run.run()
+        }
+    }
+
+    fun repeatOnMain(
+        run: () -> Boolean,
+        intervalMills: Long,
+        immediately: Boolean = true
+    ) {
+        mainScope.launch {
+            if (immediately && !run()) return@launch
+            while (isActive) {
+                delay(intervalMills)
+                if (!run()) break
+            }
+        }
+    }
+
+    fun runOnIO(run: Runnable) {
+        val traces = Thread.currentThread().stackTrace
+        ioScope.launch {
+            runWithTraces(traces, run)
+        }
+    }
+
+    fun runOnIO(run: Runnable, runnableTimeoutMillis: Long) {
+        val traces = Thread.currentThread().stackTrace
+        ioScope.launch {
+            withTimeoutOrNull(runnableTimeoutMillis.toLong()) {
+                runWithTraces(traces, run)
+            } ?: printTimeoutTraces(traces)
+        }
+    }
+
+    fun delayOnIO(
+        run: Runnable,
+        delayMills: Long,
+        runnableTimeoutMillis: Long = 10_000
+    ) {
+        val traces = Thread.currentThread().stackTrace
+        ioScope.launch {
+            delay(delayMills)
+            withTimeoutOrNull(runnableTimeoutMillis.toLong()) {
+                runWithTraces(traces, run)
+            } ?: printTimeoutTraces(traces)
+        }
+    }
+
+    fun delayOnIO(
+        delayMills: Long,
+        runnableTimeoutMillis: Long = 10_000,
+        run: Runnable
+    ) {
+        delayOnIO(run, delayMills, runnableTimeoutMillis)
+    }
+
+    fun repeatOnIO(
+        run: () -> Boolean,
+        intervalMills: Long,
+        immediately: Boolean = true,
+        runnableTimeoutMillis: Long = 5000
+    ) {
+        val traces = Thread.currentThread().stackTrace
+        ioScope.launch {
+            suspend fun safeRun(): Boolean {
+                return withTimeoutOrNull(runnableTimeoutMillis) {
+                    run()
+                } ?: run {
+                    printTimeoutTraces(traces)
+                    false
+                }
+            }
+
+            if (immediately && !safeRun()) return@launch
+            while (isActive) {
+                delay(intervalMills)
+                if (!safeRun()) break
+            }
+        }
+    }
+
+    private suspend fun runWithTraces(
+        traces: Array<StackTraceElement>,
+        run: Runnable
+    ) {
+        try {
+            run.run()
+        } catch (e: Exception) {
+            printTimeoutTraces(traces, e)
+        }
+    }
+
+    private fun printTimeoutTraces(
+        traces: Array<StackTraceElement>,
+        e: Throwable? = null
+    ) {
+        val writer = StringWriter()
+        val print = PrintWriter(writer)
+        for (traceElement in traces) {
+            print.println("\tat $traceElement")
+        }
+        e?.printStackTrace(print)
+//        Logger.w("Executor", "执行器超时(10秒):\n${writer}")
+    }
+
+    private fun repeat(
+        handler: Handler,
+        run: () -> Boolean,
+        intervalMills: Long,
+        immediately: Boolean = true
+    ) {
+        if (immediately) {
+            if (run()) {
+                handler.postDelayed({
+                    repeat(handler, run, intervalMills, immediately)
+                }, intervalMills)
+            }
+        } else {
+            handler.postDelayed({
+                if (run()) {
+                    repeat(handler, run, intervalMills, immediately)
+                }
+            }, intervalMills)
+        }
+    }
+}
+
+//package com.grkj.iscs.util
+//
+//import android.os.Handler
+//import android.os.Looper
+//import java.io.PrintWriter
+//import java.io.StringWriter
+//import java.util.concurrent.CountDownLatch
+//
+//object Executor {
+//
+//    private var io: Handler? = null
+//    private val main: Handler = Handler(Looper.getMainLooper())
+//
+//    private val latch = CountDownLatch(1)
+//
+//    init {
+//        val thread = Thread {
+//            Looper.prepare()
+//            io = Handler()
+//            latch.countDown()
+//            Looper.loop()
+//        }
+//        thread.isDaemon = true
+//        thread.start()
+//    }
+//
+//    val mainHandler: Handler
+//        get() = main
+//
+//    val ioHandler: Handler
+//        get() {
+//            if (io == null) {
+//                latch.await()
+//            }
+//            return io!!
+//        }
+//
+//    fun runOnMain(run: Runnable) {
+//        main.post(run)
+//    }
+//
+//    fun delayOnMain(delayMills: Long, run: Runnable) {
+//        main.postDelayed(run, delayMills)
+//    }
+//
+//    fun repeatOnMain(run: () -> Boolean, intervalMills: Long, immediately: Boolean = true) {
+//        repeat(main, run, intervalMills, immediately)
+//    }
+//
+//    fun runOnIO(run: Runnable) {
+//        val traces = Thread.currentThread().stackTrace
+//        ioHandler.post {
+//            runWithTraces(traces, run)
+//        }
+//    }
+//
+//    fun runOnIO(run: Runnable, runnableTimeoutMillis: Long = 5000) {
+//        val traces = Thread.currentThread().stackTrace
+//        ioHandler.post {
+//            runWithTraces(traces, run, runnableTimeoutMillis)
+//        }
+//    }
+//
+//    fun delayOnIO(run: Runnable, delayMills: Long, runnableTimeoutMillis: Long = 10_000) {
+//        val traces = Thread.currentThread().stackTrace
+//        ioHandler.postDelayed({
+//            runWithTraces(traces, run, runnableTimeoutMillis)
+//        }, delayMills)
+//    }
+//
+//    fun delayOnIO(delayMills: Long, runnableTimeoutMillis: Long = 10_000, run: Runnable) {
+//        val traces = Thread.currentThread().stackTrace
+//        ioHandler.postDelayed({
+//            runWithTraces(traces, run, runnableTimeoutMillis)
+//        }, delayMills)
+//    }
+//
+//    fun repeatOnIO(run: () -> Boolean, intervalMills: Long, immediately: Boolean = true, runnableTimeoutMillis: Long = 5_000) {
+//        val traces = Thread.currentThread().stackTrace
+//        repeat(ioHandler, {
+//            var blocked = true
+//            val thread = Thread.currentThread()
+//            delayOnMain(runnableTimeoutMillis) {
+//                // 如果线程被阻塞达到 1 秒,则打断唤醒
+//                if (blocked) {
+//                    printTimeoutTraces(traces)
+//                    thread.interrupt()
+//                }
+//            }
+//            val res = run()
+//            blocked = false
+//            return@repeat res
+//        }, intervalMills, immediately)
+//    }
+//
+//    private fun runWithTraces(traces: Array<StackTraceElement>, run: Runnable, runnableTimeoutMillis: Long = 10_000) {
+//        var blocked = true
+//        val thread = Thread.currentThread()
+//        delayOnMain(runnableTimeoutMillis) {
+//            // 如果线程被阻塞达到 1 秒,则打断唤醒
+//            if (blocked) {
+//                printTimeoutTraces(traces)
+//                thread.interrupt()
+//            }
+//        }
+//        run.run()
+//        blocked = false
+//    }
+//
+//    private fun printTimeoutTraces(traces: Array<StackTraceElement>) {
+//        val writer = StringWriter()
+//        val print = PrintWriter(writer)
+//        for (traceElement in traces) {
+//            print.println("\tat $traceElement")
+//        }
+////        Logger.w("Executor", "执行器超时(10秒):\n${writer}")
+//    }
+//
+//    private fun repeat(
+//        handler: Handler,
+//        run: () -> Boolean,
+//        intervalMills: Long,
+//        immediately: Boolean = true
+//    ) {
+//        if (immediately) {
+//            if (run()) {
+//                handler.postDelayed({
+//                    repeat(handler, run, intervalMills, immediately)
+//                }, intervalMills)
+//            }
+//        } else {
+//            handler.postDelayed({
+//                if (run()) {
+//                    repeat(handler, run, intervalMills, immediately)
+//                }
+//            }, intervalMills)
+//        }
+//    }
+//
+//}

+ 104 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/NetManager.kt

@@ -0,0 +1,104 @@
+package com.grkj.ui_base.utils
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.Network
+import android.net.NetworkRequest
+import android.os.Build
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+/**
+ * 网络管理
+ */
+class NetManager private constructor(ctx: Context) {
+
+    @Volatile
+    private var available = false
+
+    val liveData = MutableLiveData<Boolean>()
+
+    private val latch = CountDownLatch(1)
+
+    private val callback = object : ConnectivityManager.NetworkCallback() {
+
+        override fun onAvailable(network: Network) {
+            liveData.postValue(true)
+            available = true
+            latch.countDown()
+        }
+
+        override fun onUnavailable() {
+            liveData.postValue(false)
+            available = false
+            latch.countDown()
+        }
+
+        override fun onLost(network: Network) {
+            liveData.postValue(false)
+            available = false
+            latch.countDown()
+        }
+
+    }
+
+    init {
+        (ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager?)
+            ?.run {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+                    registerDefaultNetworkCallback(callback)
+                } else {
+                    registerNetworkCallback(NetworkRequest.Builder().build(), callback)
+                }
+            }
+    }
+
+    /**
+     * 当前是否有网络,不能在主线程使用
+     */
+    @WorkerThread
+    fun isAvailable(): Boolean {
+        liveData.value?.let {
+            return it
+        }
+        latch.await(100, TimeUnit.MILLISECONDS)
+        return available
+    }
+
+    companion object {
+
+        @Volatile
+        private var instance: NetManager? = null
+
+        fun getInstance(ctx: Context): NetManager {
+            return instance ?: synchronized(this) {
+                instance ?: NetManager(ctx)
+                    .also {
+                        instance = it
+                    }
+            }
+        }
+
+    }
+
+}
+
+abstract class OnceNetObserver : Observer<Boolean> {
+
+    private var observed = false
+
+    final override fun onChanged(available: Boolean) {
+        synchronized(this) {
+            if (available && !observed) {
+                onNetAvailable()
+                observed = true
+            }
+        }
+    }
+
+    abstract fun onNetAvailable()
+
+}

+ 270 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/SPUtils.kt

@@ -0,0 +1,270 @@
+package com.grkj.ui_base.utils
+
+import android.content.Context
+import com.google.gson.Gson
+import com.google.gson.reflect.TypeToken
+import com.grkj.domain.entity.local.LoginUser
+import com.grkj.domain.entity.local.UpdateKeyReturn
+import com.grkj.domain.entity.req.LockPointUpdateReq
+import com.grkj.domain.entity.res.SystemAttributePageRes
+import com.grkj.domain.entity.res.UserInfoRes
+import com.sik.sikcore.extension.getMMKVData
+import com.sik.sikcore.extension.saveMMKVData
+import com.tencent.mmkv.MMKV
+
+object SPUtils {
+
+    private const val SP_NAME = "iscs"
+    private const val SP_CONFIG_NAME = "iscs_config"
+    private const val SP_DATA = "iscs_data"
+    private const val SP_ATTRIBUTE = "iscs_attribute"
+
+    private const val KEY_LOGIN_USER_CARD_ID = "card_id"
+    private const val KEY_LOGIN_USER_CARD_CODE = "card_code"
+    private const val KEY_LOGIN_USER_HARDWARE_ID = "hardware_id"
+    private const val KEY_LOGIN_USER_CARD_NFC = "card_nfc"
+    private const val KEY_LOGIN_USER_CARD_TYPE = "card_type"
+    private const val KEY_LOGIN_USER_USER_ID = "user_id"
+    private const val KEY_LOGIN_USER_USER_NAME = "user_name"
+    private const val KEY_LOGIN_USER_KEY_CODE = "key_code"
+    private const val KEY_LOGIN_USER_ROLE_KEY = "role_key"
+
+    private const val KEY_DOCK_CONFIG = "dock_config"
+    private const val KEY_PORT_CONFIG = "port_config"
+
+    private const val KEY_STEP_MODE = "step_mode"
+
+    private const val KEY_DATA_UPDATE_LOCK_POINT = "update_lock_point"
+    private const val KEY_DATA_UPDATE_KEY_RETURN = "update_key_return"
+
+    private const val KEY_SYSTEM_ATTRIBUTE = "system_attribute"
+
+    private const val KEY_TICKET_TAKE_LOCK_EXCEPTION = "ticket_take_lock_exception"
+
+    fun getLoginUser(context: Context): LoginUser? {
+        val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
+        if (sp.getLong(KEY_LOGIN_USER_USER_ID, -1) == -1L) {
+            return null
+        }
+        return LoginUser(
+            userId = sp.getLong(KEY_LOGIN_USER_USER_ID, 0),
+            userName = sp.getString(KEY_LOGIN_USER_USER_NAME, null),
+            keyCode = sp.getString(KEY_LOGIN_USER_KEY_CODE, null),
+            roleKeyList = sp.getString(KEY_LOGIN_USER_ROLE_KEY, null)?.split(",")?.toMutableList(),
+            userCardList = sp.getString(KEY_LOGIN_USER_CARD_NFC, null)?.split(",")?.toMutableList()
+        )
+    }
+
+    fun setLoginUser(context: Context, userInfoRespVO: UserInfoRes) {
+        val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        userInfoRespVO.user?.userId?.let {
+            edit.putLong(KEY_LOGIN_USER_USER_ID, it)
+        }
+        userInfoRespVO.user?.userName?.let {
+            edit.putString(KEY_LOGIN_USER_USER_NAME, it)
+        }
+        userInfoRespVO.user?.keyCode?.let {
+            edit.putString(KEY_LOGIN_USER_KEY_CODE, it)
+        }
+        userInfoRespVO.roles?.let {
+            edit.putString(KEY_LOGIN_USER_ROLE_KEY, it.toString().replace("[", "").replace("]", ""))
+        }
+        userInfoRespVO.userCardList?.let {
+            edit.putString(KEY_LOGIN_USER_CARD_NFC, it.toString().replace("[", "").replace("]", ""))
+        }
+        edit.commit()
+    }
+
+    fun clearLoginUser(context: Context): Boolean {
+        return try {
+            val sp = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
+            val edit = sp.edit()
+            edit.clear()
+            edit.apply()
+            true
+        } catch (e: Exception) {
+            false
+        }
+    }
+
+    fun saveDockConfig(context: Context, config: String) {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putString(KEY_DOCK_CONFIG, config)
+        edit.commit()
+    }
+
+    fun getDockConfig(context: Context): String? {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        return sp.getString(KEY_DOCK_CONFIG, null)
+    }
+
+    fun savePortConfig(context: Context, config: String) {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putString(KEY_PORT_CONFIG, config)
+        edit.commit()
+    }
+
+    fun getPortConfig(context: Context): String? {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        return sp.getString(KEY_PORT_CONFIG, null)
+    }
+
+    fun saveStepMode(context: Context, mode: Int) {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putInt(KEY_STEP_MODE, mode)
+        edit.commit()
+    }
+
+    fun getStepMode(context: Context): Int {
+        val sp = context.getSharedPreferences(SP_CONFIG_NAME, Context.MODE_PRIVATE)
+        return sp.getInt(KEY_STEP_MODE, 0)
+    }
+
+    fun saveUpdateLockPoint(context: Context, list: MutableList<LockPointUpdateReq>) {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        val tempList = getUpdateLockPoint(context)
+        tempList.addAll(list)
+        edit.putString(KEY_DATA_UPDATE_LOCK_POINT, Gson().toJson(tempList))
+        edit.apply()
+    }
+
+    fun getUpdateLockPoint(context: Context): MutableList<LockPointUpdateReq> {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val listJson = sp.getString(KEY_DATA_UPDATE_LOCK_POINT, null)
+        if (listJson.isNullOrEmpty()) {
+            return mutableListOf()
+        }
+        try {
+            val tempList: MutableList<LockPointUpdateReq> = Gson().fromJson(
+                listJson,
+                object : TypeToken<MutableList<LockPointUpdateReq>>() {}.type
+            )
+            return tempList
+        } catch (e: Exception) {
+            return mutableListOf()
+        }
+    }
+
+    fun clearUpdateLockPoint(context: Context) {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putString(KEY_DATA_UPDATE_LOCK_POINT, null)
+        edit.apply()
+    }
+
+    fun saveUpdateKeyReturn(context: Context, returnBO: UpdateKeyReturn) {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        val tempList = getUpdateKeyReturn(context)
+        tempList.add(returnBO)
+        edit.putString(KEY_DATA_UPDATE_KEY_RETURN, Gson().toJson(tempList))
+        edit.apply()
+    }
+
+    fun getUpdateKeyReturn(context: Context): MutableList<UpdateKeyReturn> {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val listJson = sp.getString(KEY_DATA_UPDATE_KEY_RETURN, null)
+        if (listJson.isNullOrEmpty()) {
+            return mutableListOf()
+        }
+        try {
+            val tempList: MutableList<UpdateKeyReturn> =
+                Gson().fromJson(
+                    listJson,
+                    object : TypeToken<MutableList<UpdateKeyReturn>>() {}.type
+                )
+            return tempList
+        } catch (e: Exception) {
+            return mutableListOf()
+        }
+    }
+
+    fun clearUpdateKeyReturn(context: Context) {
+        val sp = context.getSharedPreferences(SP_DATA, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putString(KEY_DATA_UPDATE_KEY_RETURN, null)
+        edit.apply()
+    }
+
+    fun saveSystemAttribute(
+        context: Context,
+        recordList: MutableList<SystemAttributePageRes.Record>
+    ) {
+        val sp = context.getSharedPreferences(SP_ATTRIBUTE, Context.MODE_PRIVATE)
+        val edit = sp.edit()
+        edit.putString(KEY_SYSTEM_ATTRIBUTE, Gson().toJson(recordList))
+        edit.commit()
+    }
+
+    fun getSystemAttribute(context: Context): MutableList<SystemAttributePageRes.Record> {
+        val sp = context.getSharedPreferences(SP_ATTRIBUTE, Context.MODE_PRIVATE)
+        val listJson = sp.getString(KEY_SYSTEM_ATTRIBUTE, null)
+        if (listJson.isNullOrEmpty()) {
+            return mutableListOf()
+        }
+        try {
+            val tempList: MutableList<SystemAttributePageRes.Record> = Gson().fromJson(
+                listJson,
+                object : TypeToken<MutableList<SystemAttributePageRes.Record>>() {}.type
+            )
+            return tempList
+        } catch (e: Exception) {
+            return mutableListOf()
+        }
+    }
+
+    fun getAttributeValue(context: Context, key: String?): String? {
+        val list = getSystemAttribute(context)
+        if (list.isEmpty()) {
+            return null
+        }
+        return list.find { it.sysAttrKey == key }?.sysAttrValue
+    }
+
+    /**
+     * 取出钥匙
+     */
+    fun takeKey(ticketId: Long) {
+        "${ticketId}_TakeKey".saveMMKVData(true)
+    }
+
+    /**
+     * 作业是否已经取过钥匙
+     */
+    fun isKeyTake(ticketId: Long): Boolean {
+        return "${ticketId}_TakeKey".getMMKVData(false)
+    }
+
+    /**
+     * 归还钥匙
+     */
+    fun returnKey(ticketId: Long) {
+        MMKV.defaultMMKV().remove("${ticketId}_TakeKey")
+    }
+
+    /**
+     * 保存作业获取锁异常状态
+     */
+    fun saveTicketTakeLockException(ticketId: Long) {
+        "${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}".saveMMKVData(true)
+    }
+
+    /**
+     * 获取作业获取锁异常状态
+     */
+    fun getTicketTakeLockException(ticketId: Long): Boolean {
+        return "${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}".getMMKVData(false)
+    }
+
+    /**
+     * 重置作业获取锁异常状态
+     */
+    fun resetTicketTakeLockException(ticketId: Long) {
+        MMKV.defaultMMKV().remove("${ticketId}${KEY_TICKET_TAKE_LOCK_EXCEPTION}")
+    }
+}

+ 16 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleBean.kt

@@ -0,0 +1,16 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.data.BleDevice
+import java.io.File
+
+/**
+ * 钥匙模型
+ */
+data class BleBean(
+    var bleDevice: BleDevice,
+    var token: ByteArray? = null,
+    var fileSend: File? = null,
+    var ticketSend: String? = null,             // 下发的工作票
+    var ticketStatus: ByteArray = byteArrayOf(), // 从钥匙拿的工作票
+    var retryCount: Int = 0
+)

+ 493 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleCmdManager.kt

@@ -0,0 +1,493 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.grkj.ui_base.utils.event.GetTicketStatusEvent
+import com.grkj.ui_base.utils.extension.crc16
+import com.grkj.ui_base.utils.extension.toByteArray
+import com.grkj.ui_base.utils.extension.toHexStrings
+import com.sik.sikcore.thread.ThreadUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+
+/**
+ * 指令操作类
+ */
+object BleCmdManager {
+    private val logger: Logger = LoggerFactory.getLogger(BleCmdManager::class.java)
+
+    /**
+     * 拼接时间戳
+     */
+    private fun assembleTimeStamp(byteArray: ByteArray): ByteArray {
+        return byteArray + getTimeStamp()
+    }
+
+    /**
+     * 拼接时间戳 + token
+     */
+    private fun assembleData(bleBean: BleBean, byteArray: ByteArray): ByteArray? {
+        bleBean.token?.let {
+            return assembleTimeStamp(byteArray) + it
+        } ?: run {
+            // TODO 有问题,一直循环
+//            getToken(bleBean.bleDevice.mac, object : CustomBleWriteCallback() {
+//                override fun onPrompt(promptStr: String?) {}
+//
+//                override fun onConnectPrompt(promptStr: String?) {}
+//
+//                override fun onDisConnectPrompt(promptStr: String?) {}
+//
+//                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {}
+//
+//                override fun onWriteFailure(exception: BleException?) {}
+//
+//            })
+//            // TODO 临时方案
+//            Thread.sleep(100)
+//            return assembleData(bleBean, byteArray)
+
+            return null
+        }
+    }
+
+    private fun getTimeStamp(): ByteArray {
+        val tempArr = (System.currentTimeMillis() / 1000).toByteArray()
+        val timeStampArr = byteArrayOf(tempArr[0], tempArr[1], tempArr[2], tempArr[3])
+        return timeStampArr
+    }
+
+    /**
+     * 获取令牌
+     */
+    fun getToken(mac: String?, callback: CustomBleWriteCallback?) {
+        logger.info("$mac")
+        BleConnectionManager.getBleDeviceByMac(mac)?.bleDevice?.let {
+            logger.info("Get token : $mac")
+            BleUtil.Companion.instance?.write(
+                it,
+                cmd = assembleTimeStamp(BleConst.REQ_GET_TOKEN),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 令牌处理
+     *
+     * @param callBack 是否成功
+     */
+    fun handleToken(
+        bleDevice: BleDevice,
+        byteArray: ByteArray,
+        callBack: ((Boolean) -> Unit)? = null
+    ) {
+        logger.info("handleToken : ${byteArray.toHexStrings()}")
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.token = byteArrayOf(byteArray[11], byteArray[12], byteArray[13], byteArray[14])
+            callBack?.invoke(true)
+            logger.info("Token 赋值 ${it.token?.toHexStrings()} : ${bleDevice.mac}")
+        } ?: let {
+            callBack?.invoke(false)
+        }
+    }
+
+    /**
+     * 工作模式切换
+     *
+     * @param mode 0x01:工作模式 0x02:待机模式
+     */
+    fun switchMode(mode: ByteArray, bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_SWITCH_MODE + mode),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 工作模式切换结果
+     * job : 0x01:工作模式 0x02:待机模式
+     * res : 0x01:成功 0x02:失败
+     */
+    fun handleSwitchModeResult(byteArray: ByteArray, callBack: ((Byte, Byte) -> Unit)? = null) {
+        logger.info("handleSwitchModeResult : ${byteArray.toHexStrings()}")
+        val job = byteArray[4]
+        val res = byteArray[5]
+        callBack?.invoke(job, res)
+    }
+
+    /**
+     * 工作票下发
+     */
+    fun sendWorkTicket(
+        json: String,
+        idx: Int = 0,
+        bleDevice: BleDevice,
+        callback: CustomBleWriteCallback?
+    ) {
+        logger.info("sendWorkTicket : $idx")
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.ticketSend = json
+        }
+        val totalLength = json.toByteArray().size
+        val totalPackets = (totalLength + 128 - 1) / 128
+        val total = totalPackets.toByteArray()
+
+        val data = if (idx == totalPackets - 1) {
+            json.toByteArray().copyOfRange(idx * 128, totalLength)
+        } else {
+            json.toByteArray().copyOfRange(idx * 128, (idx + 1) * 128)
+        }
+        val jsonInfo =
+            total + idx.toByteArray() + data.crc16(0, data.size) + data.size.toByteArray() + data
+        logger.debug(
+            "debug1 : ${total.size} : ${idx.toByteArray().size} : ${
+                data.crc16(
+                    0,
+                    data.size
+                ).size
+            } : ${data.size.toByteArray().size} : ${data.size}"
+        )
+        logger.debug("debug2 : ${(jsonInfo.size + 1).toByteArray(1).size} : ${0x02.toByteArray(1).size} : ${jsonInfo.size}")
+
+        val cmd =
+            BleConst.REQ_SEND_WORK_TICKET + (jsonInfo.size + 1).toByteArray(1) + 0x02.toByteArray(1) + jsonInfo
+
+        logger.debug("debug3 : ${cmd.toHexStrings()}")
+
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                writeUUID = BleConst.WRITE_UUID,
+                cmd = assembleData(it, cmd),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 工作票下发结果
+     * res:0x00:成功 0x01:失败 0x02:传输超时 0x0D:当前IDX超出范围 0x0E:当前数据CRC校验失败 0x14:JSON结构错误 0x63:未知错误
+     *
+     * @param callBack 是否成功、结果
+     */
+    fun handleWorkTicketResult(
+        bleBean: BleBean,
+        byteArray: ByteArray,
+        callBack: ((Boolean, Byte?) -> Unit)? = null
+    ) {
+        logger.info("handleWorkTicketResult : ${byteArray.toHexStrings()}")
+        val idx = byteArray[4] + byteArray[5]
+        val total = byteArray[6] + byteArray[7]
+        val res = byteArray[8]
+        when (res) {
+            0x00.toByte() -> logger.info("Work ticket send success")
+            0x01.toByte() -> logger.info("Work ticket send fail")
+            0x02.toByte() -> logger.info("Work ticket send timeout")
+            0x0D.toByte() -> logger.info("Work ticket send idx out of range")
+            0x0E.toByte() -> logger.info("Work ticket send data crc error")
+            0x14.toByte() -> logger.info("Work ticket send json error")
+        }
+        logger.info("idx : $idx : $total : $res")
+        if (idx != total - 1) {
+            if (res == 0x00.toByte() || res == 0x02.toByte()) {
+                // TODO 要判断res
+                sendWorkTicket(
+                    BleConnectionManager.getBleDeviceByMac(bleBean.bleDevice.mac)?.ticketSend!!,
+                    if (res == 0x00.toByte()) idx + 1 else idx,
+                    bleBean.bleDevice,
+                    object : CustomBleWriteCallback() {
+                        override fun onWriteSuccess(
+                            current: Int,
+                            total: Int,
+                            justWrite: ByteArray?
+                        ) {
+                        }
+
+                        override fun onWriteFailure(exception: BleException?) {}
+                    })
+            } else {
+                callBack?.invoke(false, res)
+            }
+        } else {
+            if (res == 0x00.toByte()) {
+                logger.info("Work ticket is done")
+                bleBean.retryCount = 0
+                callBack?.invoke(true, res)
+            } else {
+                callBack?.invoke(false, res)
+            }
+        }
+    }
+
+    /**
+     * 获取设备当前状态
+     */
+    fun getCurrentStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_CURRENT_STATUS),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 处理设备当前状态
+     * 0x01:工作模式 0x02:待机模式 0x03:故障状态
+     */
+    fun handleCurrentStatus(byteArray: ByteArray, callBack: ((Byte) -> Unit)? = null) {
+        logger.info("handleCurrentStatus : ${byteArray.toHexStrings()}")
+        val job = byteArray[4]
+        when (job) {
+            0x01.toByte() -> {
+                logger.info("handleCurrentStatus : 工作模式")
+            }
+
+            0x02.toByte() -> {
+                logger.info("handleCurrentStatus : 待机模式")
+            }
+
+            0x03.toByte() -> {
+                logger.info("handleCurrentStatus : 故障状态")
+            }
+        }
+        callBack?.invoke(job)
+    }
+
+    /**
+     * 获取工作票完成情况
+     */
+    fun getTicketStatus(bleDevice: BleDevice, callback: CustomBleWriteCallback?) {
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_WORK_TICKET_RESULT),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 处理工作票完成情况
+     */
+    fun handleTicketStatus(
+        bleDevice: BleDevice,
+        byteArray: ByteArray,
+        callBack: ((String?) -> Unit)? = null
+    ) {
+        // TODO 需要有超时重传机制
+        logger.info("handleTicketStatus : ${byteArray.toHexStrings()}")
+
+        val total = byteArray[4] + byteArray[5]
+        val idx = byteArray[6] + byteArray[7]
+        val crc = byteArray[8] + byteArray[9]
+        val size = byteArray[10].toUByte() + byteArray[11].toUByte()
+        logger.info("工作票数据 : $total : $idx : $size")
+        // 数据组装
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            it.ticketStatus += byteArray.copyOfRange(12, 12 + size.toInt())
+        }
+        // TODO 缺少res处理
+        if (idx != total - 1) {
+            getTicketStatusPart(
+                idx.toByteArray(),
+                total.toByteArray(),
+                byteArrayOf(0x00.toByte()),
+                bleDevice,
+                object : CustomBleWriteCallback() {
+                    override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                        logger.info("getTicketStatusPart success")
+                    }
+
+                    override fun onWriteFailure(exception: BleException?) {
+                        logger.error("getTicketStatusPart fail")
+                        GetTicketStatusEvent.sendGetTicketStatusEvent(false, bleDevice)
+                    }
+                })
+        } else {
+            BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+                logger.info("工作票完成接收 : ${String(it.ticketStatus)}")
+                callBack?.invoke(String(it.ticketStatus))
+                // TODO 清空ticket
+                it.ticketStatus = byteArrayOf()
+            } ?: let {
+                callBack?.invoke(null)
+            }
+        }
+    }
+
+    /**
+     * 获取工作票完成情况分包
+     */
+    private fun getTicketStatusPart(
+        idx: ByteArray,
+        total: ByteArray,
+        res: ByteArray,
+        bleDevice: BleDevice,
+        callback: CustomBleWriteCallback?
+    ) {
+        BleConnectionManager.getBleDeviceByMac(bleDevice.mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_WORK_TICKET_RESULT_PART + idx + total + res),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 获取钥匙电量
+     */
+    fun getPower(mac: String?, callback: CustomBleWriteCallback?) {
+        BleConnectionManager.getBleDeviceByMac(mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_POWER_STATUS),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 处理钥匙电量
+     * bat:电量百分比,范围 0-100,单位:%
+     * chg:0x01:未充电 0x02:充电中 0x03:充满
+     */
+    private fun handlePowerStatus(byteArray: ByteArray) {
+        logger.info("handlePowerStatus : ${byteArray.toHexStrings()}")
+        val bat = byteArray[4].toInt()
+        val chg = byteArray[5]
+        logger.info("钥匙电量 : $bat%")
+        when (chg) {
+            0x01.toByte() -> {
+                logger.info("充电状态:未充电")
+            }
+
+            0x02.toByte() -> {
+                logger.info("充电状态:充电中")
+            }
+
+            0x03.toByte() -> {
+                logger.info("充电状态:充满")
+            }
+        }
+    }
+
+    /**
+     * 发送文件
+     * type: 1:固件文件 2:点位PNG文件
+     * FLNM:文件名(不含扩展名)
+     * FLSZ:文件大小(字节)
+     * FLCRC:文件的CRC-16
+     * PGTOTAL:文件总包数
+     * PGIDX:当前包idx
+     * PGCRC:当前包CRC-16
+     * PGSZ:当前包长度(字节)
+     * PGDATA:当前包数据
+     */
+    fun sendFile(
+        type: Int,
+        file: File,
+        idx: Int = 0,
+        mac: String?,
+        callback: CustomBleWriteCallback?
+    ) {
+        ThreadUtils.runOnIO {
+            logger.info("sendFile : $idx")
+
+            BleConnectionManager.getBleDeviceByMac(mac)?.let {
+                it.fileSend = file
+            }
+
+            val pgtotal = (file.readBytes().size + 128 - 1) / 128
+            if (idx == pgtotal) {
+                logger.info("Send finish")
+                return@runOnIO
+            }
+
+            val flnm = file.name.substringBeforeLast(".").toByteArray(8)
+            val flsz = file.readBytes().size.toByteArray(4)
+            val flcrc = file.readBytes().crc16()
+            val pgdata = if (idx == pgtotal - 1) {
+                file.readBytes().copyOfRange(idx * 128, file.readBytes().size - 1)
+            } else {
+                file.readBytes().copyOfRange(idx * 128, (idx + 1) * 128)
+            }
+            val pgsz = pgdata.size.toByteArray()
+            val pgcrc = pgdata.crc16()
+
+            val fileInfo =
+                byteArrayOf(type.toByte()) + flnm + flsz + flcrc + pgtotal.toByteArray() + idx.toByteArray() + pgcrc + pgsz + pgdata
+            val cmd =
+                BleConst.REQ_TRANSFER_FILE + (fileInfo.size + 1).toByteArray(1) + 0x01.toByteArray(1) + fileInfo
+
+            BleConnectionManager.getBleDeviceByMac(mac)?.let {
+                BleUtil.Companion.instance?.write(
+                    it.bleDevice,
+                    writeUUID = BleConst.WRITE_UUID,
+                    cmd = assembleData(it, cmd),
+                    writeCallback = callback
+                )
+            }
+            Thread.sleep(50)
+            sendFile(type, file, idx + 1, mac, callback)
+        }
+    }
+
+    /**
+     * 处理发送分包文件响应
+     * type: 0x01:固件文件 0x02:点位PNG文件
+     */
+    private fun handleFileRsp(bleBean: BleBean, byteArray: ByteArray) {
+        logger.info("handleFileRsp : ${byteArray.toHexStrings()}")
+        val type = byteArray[4]
+        val res = byteArray[17]
+        val total = byteArray[15] + byteArray[16]
+        val idx = byteArray[13] + byteArray[14]
+        if (idx != total - 1 && (res == 0x00.toByte() || res == 0x02.toByte())) {
+            // 不用等回复再发
+        }
+    }
+
+    /**
+     * 获取版本
+     */
+    fun getVersion(mac: String?, callback: CustomBleWriteCallback?) {
+        BleConnectionManager.getBleDeviceByMac(mac)?.let {
+            BleUtil.Companion.instance?.write(
+                it.bleDevice,
+                cmd = assembleData(it, BleConst.REQ_GET_VERSION),
+                writeCallback = callback
+            )
+        }
+    }
+
+    /**
+     * 处理软件/硬件版本
+     */
+    private fun handleVersion(byteArray: ByteArray) {
+        val sofVersion = parseVersion(byteArray[4])
+        val hardVersion = parseVersion(byteArray[5])
+        logger.info("$sofVersion : $hardVersion")
+    }
+
+    /**
+     * 版本解析
+     */
+    private fun parseVersion(byte: Byte): String {
+        // 将 Byte 转换为 Int 以便更容易进行位操作
+        val intValue = byte.toInt()
+        // 提取高 4 位作为主版本号
+        val majorVersion = (intValue and 0xF0) ushr 4
+        // 提取低 4 位作为次版本号
+        val minorVersion = intValue and 0x0F
+
+        return "V$majorVersion.$minorVersion"
+    }
+}

+ 543 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConnectionManager.kt

@@ -0,0 +1,543 @@
+package com.grkj.ui_base.utils.ble
+
+import android.bluetooth.BluetoothGatt
+import com.clj.fastble.BleManager
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.grkj.ui_base.R
+import com.grkj.ui_base.config.ISCSConfig
+import com.grkj.ui_base.utils.modbus.ModBusController
+import com.grkj.ui_base.utils.CommonUtils
+import com.grkj.ui_base.utils.event.LoadingEvent
+import com.grkj.ui_base.utils.extension.startsWith
+import com.grkj.ui_base.utils.extension.toHexStrings
+import com.sik.sikcore.activity.ActivityTracker
+import com.sik.sikcore.thread.ThreadUtils
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * BLE 连接管理工具:保持原有扫描、连接、监听、取 Token 流程,
+ * 并增加“最大待机数”功能,超出后断开最旧连接,保证同时在线设备数不超过阈值。
+ */
+object BleConnectionManager {
+    private val logger: Logger = LoggerFactory.getLogger(BleConnectionManager::class.java)
+
+    @JvmStatic
+    @Volatile
+    // 已连接的蓝牙钥匙集合
+    var deviceList: MutableList<BleBean> = mutableListOf()
+
+    /**
+     * 最大待机连接数,超过则断开最旧设备。
+     * 默认为业务常量 MAX_KEY_STAND_BY,可根据需求调整。
+     */
+    @Volatile
+    var maxStandbyCount: Int = BleConst.MAX_KEY_STAND_BY
+
+    @Volatile
+    var maxConnectCount: Int = BleConst.MAX_KEY_CONNECT_COUNT
+
+    // 原有回调管理
+    private val connectListeners = mutableListOf<ConnectListener>()
+    private var isPreparing: Boolean = false
+
+    /**
+     * 当前正在连接的mac
+     */
+    @Volatile
+    private var currentConnectingMac: String? = null
+
+    /**
+     * 有问题的钥匙的列表 - rfid
+     */
+    var mExceptionKeyList = mutableListOf<String>()
+
+    /**
+     * 蓝牙的通信返回监听
+     */
+    @Volatile
+    private var bleIndicateListeners: HashMap<Any, BleIndicateListener> = hashMapOf()
+
+    /**
+     * 基础的蓝牙通信返回监听
+     */
+    private val baseIndicateListeners: BleIndicateListener = object : BleIndicateListener {
+        override fun handleRsp(
+            bleBean: BleBean,
+            byteArray: ByteArray,
+            isNeedLoading: Boolean,
+            prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+        ) {
+            when {
+                // 获取令牌
+                byteArray.startsWith(BleConst.RSP_GET_TOKEN) -> BleCmdManager.handleToken(
+                    bleBean.bleDevice, byteArray
+                ) { isSuccess ->
+                    if (isSuccess) {
+                        prepareDoneCallBack?.invoke(true, bleBean)
+                    }
+                }
+            }
+        }
+
+    }
+
+    /**
+     * 注册连接监听:
+     * - 如果设备已在 deviceList 且拥有 token,立即回调并返回
+     * - 如果 mac 已在待连接队列或正在连接,忽略重复请求
+     * - 否则将 mac 添加到队列并触发连接流程
+     */
+    fun registerConnectListener(mac: String, callBack: ((Boolean, BleBean?) -> Unit)? = null) {
+        logger.info("registerConnectListener : $mac")
+        // 已连接且已获取 token
+        deviceList.find { it.bleDevice.mac == mac && it.token != null }?.let { bean ->
+            callBack?.invoke(true, bean)
+            return
+        }
+        // 重复注册检查
+        if (connectListeners.any { it.mac == mac } || currentConnectingMac == mac) {
+            logger.warn("忽略重复注册 mac: $mac")
+            return
+        }
+        // 加入队列并启动连接
+        fun checkAndConnect() {
+            if (BleManager.getInstance().allConnectedDevice.size < maxConnectCount) {
+                connectListeners.add(ConnectListener(mac, callBack))
+                connectKey()
+            } else {
+                ThreadUtils.runOnIODelayed(500) {
+                    checkAndConnect()
+                }
+            }
+        }
+        checkAndConnect()
+    }
+
+    /**
+     * 连接监听反注册
+     */
+    fun unregisterConnectListener(mac: String, bleBean: BleBean? = null) {
+        logger.info("unregisterConnectListener : $mac")
+        connectListeners.removeAll { it.mac == mac }
+    }
+
+    /**
+     * 添加蓝牙通信返回监听
+     */
+    fun addBLeIndicateListener(tag: Any, bleIndicateListener: BleIndicateListener) {
+        bleIndicateListeners.put(tag, bleIndicateListener)
+    }
+
+    /**
+     * 移除蓝牙通信返回监听
+     */
+    fun removeBleIndicateListener(tag: Any) {
+        bleIndicateListeners.remove(tag)
+    }
+
+    /**
+     * 检查是否能进行蓝牙连接准备的下一步,防止未准备完但是已经取消订阅
+     */
+    private fun checkProcess(mac: String?): Boolean {
+        val canProcess = connectListeners.any { it.mac == mac }
+        if (!canProcess) LoadingEvent.sendLoadingEvent(null, false)
+        return canProcess
+    }
+
+    /**
+     * 连接钥匙,单个mac走完prepare再进行下一个
+     */
+    private fun connectKey() {
+        if (connectListeners.isEmpty()) return
+        if (isPreparing || BleManager.getInstance().allConnectedDevice.size >= maxStandbyCount) {
+            ThreadUtils.runOnMainDelayed(1000) { connectKey() }
+            return
+        }
+        val listener = connectListeners.first()
+        currentConnectingMac = listener.mac
+        isPreparing = true
+        if (ActivityTracker.getCurrentActivity() == null) {
+            logger.warn("Ignore connectKey : ${listener.mac} no current activity")
+            isPreparing = false
+            currentConnectingMac = null
+            return
+        }
+        prepareBle(
+            listener.mac, false
+        ) { isDone, bleBean ->
+            ThreadUtils.runOnMain {
+                isPreparing = false
+                currentConnectingMac = null
+                if (!isDone) {
+                    // 判断是否仍然待连,防止拿走;移到末尾,防止循环影响
+                    if (checkProcess(listener.mac)) {
+                        unregisterConnectListener(listener.mac)
+                        ThreadUtils.runOnMainDelayed(2000) {
+                            registerConnectListener(
+                                listener.mac, listener.callBack
+                            )
+                        }
+                    }
+                    return@runOnMain
+                }
+                // 判断是否仍然待连,防止拿走
+                // TODO 暂时只处理准备成功
+                if (connectListeners.contains(listener)) {
+                    listener.callBack?.invoke(true, bleBean)
+                    unregisterConnectListener(listener.mac)
+                }
+                if (connectListeners.isNotEmpty()) connectKey()
+            }
+        }
+    }
+
+    /**
+     * @param loadingCallBack 是否显示loading、loading文字、流程是否结束
+     * @param prepareDoneCallBack 蓝牙连接是否成功、蓝牙连接对象
+     */
+    private fun prepareBle(
+        mac: String,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(mac)) {
+            logger.error("Prepare is canceled : $mac")
+            return
+        }
+        ThreadUtils.runOnMain {
+            doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+        }
+    }
+
+    private fun doScanBle(
+        mac: String,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        logger.info("doScanBle:$mac")
+        if (!checkProcess(mac)) {
+            logger.error("Prepare is canceled : $mac")
+            return
+        }
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent("正在扫描设备...", true)
+        BleUtil.Companion.instance?.scan(object : CustomBleScanCallback() {
+            override fun onPrompt(promptStr: String?) {
+                // 蓝牙未启动重试
+                BleManager.getInstance().enableBluetooth()
+                doScanBle(mac, isNeedLoading, prepareDoneCallBack)
+            }
+
+            override fun onScanStarted(success: Boolean) {
+                logger.info("onScanStarted:${success}")
+                if (!success) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+
+            override fun onScanning(bleDevice: BleDevice?) {
+                logger.info("onScanning:${bleDevice?.mac}")
+                bleDevice?.let {
+                    doConnect(it, isNeedLoading, prepareDoneCallBack)
+                }
+            }
+
+            override fun onScanFinished(scanResultList: MutableList<BleDevice>?) {
+                logger.info("onScanFinished: $mac - ${scanResultList?.none { it.mac == mac }}")
+                if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
+                // 没有扫描到
+                if (scanResultList?.none { it.mac == mac } == true) {
+                    logger.warn("$mac is not scanned")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            }
+        })
+    }
+
+    /**
+     * 连接蓝牙设备
+     */
+    private fun doConnect(
+        bleDevice: BleDevice,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        logger.info("doConnect : ${bleDevice.mac}")
+        if (!checkProcess(bleDevice.mac)) {
+            logger.error("Prepare is canceled : ${bleDevice.mac}")
+            return
+        }
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+            CommonUtils.getStr(R.string.ble_connecting), true
+        )
+        BleManager.getInstance().disconnect(bleDevice)
+        BleUtil.Companion.instance?.connectBySelect(
+            bleDevice, object : CustomBleGattCallback() {
+                override fun onPrompt(promptStr: String?) {
+                    logger.info(promptStr)
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        promptStr, false
+                    )
+                }
+
+                override fun onStartConnect() {}
+
+                override fun onConnectFail(bleDevice: BleDevice?, exception: BleException?) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        CommonUtils.getStr(R.string.ble_connect_fail), false
+                    )
+                    logger.error("onConnectFail : ${bleDevice?.mac} - ${exception?.description}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice?, gatt: BluetoothGatt?, status: Int
+                ) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        null, false
+                    )
+                    logger.info("onConnectSuccess : ${bleDevice?.mac}")
+                    bleDevice?.let {
+                        deviceList.removeIf { it.bleDevice.mac == bleDevice.mac }
+                        val bleBean = BleBean(it)
+                        deviceList.add(bleBean)
+//todo 移除异常钥匙                        removeExceptionKey(it.mac)
+                        // 设置MTU
+                        ThreadUtils.runOnMainDelayed(200) {
+                            if (!checkProcess(bleDevice.mac)) {
+                                logger.error("Prepare is canceled : ${bleDevice.mac}")
+                                return@runOnMainDelayed
+                            }
+                            BleUtil.Companion.instance?.setMtu(it)
+                        }
+                        // 监听
+                        ThreadUtils.runOnMainDelayed(500) {
+                            indicate(bleBean, isNeedLoading, prepareDoneCallBack)
+                        }
+                    }
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice?,
+                    gatt: BluetoothGatt?,
+                    status: Int
+                ) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent(
+                        null, false
+                    )
+                    logger.info("onDisConnected : ${device?.mac} - $isActiveDisConnected")
+                    getBleDeviceByMac(device?.mac)?.let {
+                        deviceList.remove(it)
+                    }
+                    bleDevice.mac?.let { itMac ->
+                        unregisterConnectListener(itMac)
+                    }
+                    if (!isActiveDisConnected) {
+                        // 测试模式下不重连
+                        if (ISCSConfig.isTestMode) {
+                            return
+                        }
+                        // 断开和重连之间最好间隔一段时间,否则可能会出现长时间连接不上的情况
+                        ThreadUtils.runOnMainDelayed(300) {
+                            registerConnectListener(bleDevice.mac) { isDone, bleBean ->
+                                if (isDone && bleBean != null) {
+                                    ThreadUtils.runOnMainDelayed(300) {
+                                        getCurrentStatus(6, bleBean.bleDevice)
+                                    }
+                                }
+                            }
+                        }
+                    } else {
+                        ModBusController.updateKeyReadyStatus(bleDevice.mac, false, 3)
+                    }
+                }
+            })
+    }
+
+    /**
+     * 获取当前钥匙的状态
+     */
+    fun getCurrentStatus(
+        from: Int,
+        bleDevice: BleDevice,
+        retryCount: Int = 3,
+        timeoutCallBack: ((Boolean) -> Unit)? = null
+    ) {
+        logger.info("getCurrentStatus - ${bleDevice.mac} - from : $from")
+        var isTimeout = true
+        // 加1秒防止早于onWriteFailure开始处理导致多次处理
+        ThreadUtils.runOnMainDelayed((BleUtil.OPERATE_TIMEOUT + 1).toLong()) {
+            if (isTimeout) {
+                logger.error("getCurrentStatus timeout : mac = ${bleDevice.mac}, retryCount = $retryCount")
+                if (retryCount > 0) {
+                    ThreadUtils.runOnMainDelayed(1000) {
+                        getCurrentStatus(from, bleDevice, retryCount - 1, timeoutCallBack)
+                    }
+                } else {
+                    ModBusController.getKeyByMac(bleDevice.mac)?.rfid?.let {
+                        addExceptionKey(it)
+                        timeoutCallBack?.invoke(true)
+                    }
+                }
+            }
+        }
+        BleCmdManager.getCurrentStatus(bleDevice, object : CustomBleWriteCallback() {
+            override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                logger.info("getCurrentStatus success : ${bleDevice.mac}")
+                isTimeout = false
+                timeoutCallBack?.invoke(false)
+            }
+
+            override fun onWriteFailure(exception: BleException?) {
+                logger.info("getCurrentStatus fail : ${bleDevice.mac}")
+                isTimeout = false
+                ThreadUtils.runOnMainDelayed(1000) {
+                    getCurrentStatus(from, bleDevice, timeoutCallBack = timeoutCallBack)
+                }
+            }
+        })
+    }
+
+    /**
+     * 获取电池电量
+     */
+    fun getBatteryPower(bleDevice: BleDevice) {
+        logger.info("获取电池电量:${bleDevice.mac}")
+        BleCmdManager.getPower(bleDevice.mac, object : CustomBleWriteCallback() {
+            override fun onWriteSuccess(p0: Int, p1: Int, p2: ByteArray?) {
+                logger.info("发送获取电池电量命令成功:${bleDevice.mac}")
+            }
+
+            override fun onWriteFailure(p0: BleException?) {
+                ThreadUtils.runOnIODelayed(500) {
+                    logger.info("发送获取电池电量命令失败:${bleDevice.mac}")
+                    getBatteryPower(bleDevice)
+                }
+            }
+        })
+    }
+
+    /**
+     * 添加有问题的钥匙
+     */
+    fun addExceptionKey(rfid: String) {
+        logger.warn("addExceptionKey: $rfid")
+        if (mExceptionKeyList.contains(rfid)) {
+            return
+        }
+        mExceptionKeyList.add(rfid)
+    }
+
+    /**
+     * 移除有问题的钥匙
+     */
+    fun removeExceptionKey(key: String) {
+        logger.info("removeExceptionKey: $key")
+        mExceptionKeyList.remove(key)
+    }
+
+    fun getBleDeviceByMac(mac: String?): BleBean? {
+        return deviceList.find { it.bleDevice.mac == mac }
+    }
+
+    /**
+     * 监听蓝牙设备
+     */
+    private fun indicate(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            logger.error("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始监听......", true)
+        bleBean?.let {
+            var isIndicateSuccess = false
+            BleUtil.Companion.instance?.indicate(
+                it.bleDevice, indicateCallback = object : CustomBleIndicateCallback() {
+                    override fun onPrompt(promptStr: String?) {
+                        logger.info("indicate onPrompt : $promptStr")
+                    }
+
+                    override fun onConnectPrompt(promptStr: String?) {
+                        logger.info("indicate onConnectPrompt : $promptStr")
+                    }
+
+                    override fun onDisConnectPrompt(promptStr: String?) {
+                        logger.info("indicate onDisConnectPrompt : $promptStr")
+                    }
+
+                    override fun onIndicateSuccess() {
+                        logger.info("onIndicateSuccess")
+                        isIndicateSuccess = true
+                        getToken(bleBean, isNeedLoading, prepareDoneCallBack)
+                    }
+
+                    override fun onIndicateFailure(exception: BleException?) {
+                        if (isNeedLoading) LoadingEvent.sendLoadingEvent(null, false)
+                        logger.error("onIndicateFailure : ${bleBean.bleDevice.mac} - ${exception?.description}")
+                        ThreadUtils.runOnIODelayed(500) {
+                            if (isIndicateSuccess) {
+                                return@runOnIODelayed
+                            }
+                            prepareDoneCallBack?.invoke(false, null)
+                        }
+                    }
+
+                    override fun onCharacteristicChanged(data: ByteArray?) {
+                        logger.info("onCharacteristicChanged : ${data?.toHexStrings()}")
+                        if (bleIndicateListeners.isEmpty) {
+                            bleIndicateListeners.put(this, baseIndicateListeners)
+                        }
+                        data?.let { itData ->
+                            bleIndicateListeners.forEach { listener ->
+                                listener.value.handleRsp(
+                                    it, itData, isNeedLoading, prepareDoneCallBack
+                                )
+                            }
+                        }
+                    }
+                })
+        }
+    }
+
+    /**
+     * 获取蓝牙钥匙token
+     */
+    private fun getToken(
+        bleBean: BleBean?,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    ) {
+        if (!checkProcess(bleBean?.bleDevice?.mac)) {
+            logger.error("Prepare is canceled : ${bleBean?.bleDevice?.mac}")
+            return
+        }
+        if (isNeedLoading) LoadingEvent.sendLoadingEvent("开始获取token...", true)
+        bleBean?.let {
+            BleCmdManager.getToken(it.bleDevice.mac, object : CustomBleWriteCallback() {
+                override fun onWriteSuccess(current: Int, total: Int, justWrite: ByteArray?) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent("token获取成功", true)
+                    logger.info("getToken success : ${bleBean.bleDevice.mac}")
+                }
+
+                override fun onWriteFailure(exception: BleException?) {
+                    if (isNeedLoading) LoadingEvent.sendLoadingEvent("token获取失败", true)
+                    logger.error("getToken fail : ${bleBean.bleDevice.mac}")
+                    prepareDoneCallBack?.invoke(false, null)
+                }
+            })
+        }
+    }
+
+
+    // 蓝牙连接准备监听
+    data class ConnectListener(
+        val mac: String, val callBack: ((Boolean, BleBean?) -> Unit)? = null
+    )
+}

+ 76 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleConst.kt

@@ -0,0 +1,76 @@
+package com.grkj.ui_base.utils.ble
+
+/**
+ * 指令,均为未加密或解密后的
+ */
+object BleConst {
+
+    const val BLE_LOCAL_NAME = "keyLock"
+
+    const val MAX_KEY_STAND_BY: Int = 1
+    const val MAX_KEY_CONNECT_COUNT: Int = 2
+
+    const val MTU = 500
+
+    const val SERVICE_UUID = "0000FEE7-0000-1000-8000-00805F9B34FB"
+    const val INDICATE_UUID = "0000FED1-0000-1000-8000-00805F9B34FB"
+    const val WRITE_UUID = "0000FED2-0000-1000-8000-00805F9B34FB"
+
+    val STATUS_WORK = byteArrayOf(0x01)     // 工作模式
+    val STATUS_READY = byteArrayOf(0x02)    // 待机模式
+
+    /**
+     * byteArrayOf不可变,可以使用 mutableListOf 来创建一个可变的列表,然后使用 toByteArray 方法
+     * byteArray也有toList()方法
+     */
+    // 获取令牌,需增加4字节的时间戳,总长8个字节长度
+    val REQ_GET_TOKEN = byteArrayOf(0x01, 0x01, 0x05, 0x00)
+
+    // 获取令牌响应,最后4个是token,总长15个字节长度
+    val RSP_GET_TOKEN = byteArrayOf(0x01, 0x02, 0x04)
+
+    // 设备工作模式切换
+    val REQ_SWITCH_MODE = byteArrayOf(0x02, 0x01, 0x02, 0x01)
+
+    // 工作模式切换响应
+    val RSP_SWITCH_MODE = byteArrayOf(0x02, 0x02, 0x03, 0x01)
+
+    // 工作票下发
+    val REQ_SEND_WORK_TICKET = byteArrayOf(0x02, 0x01)
+
+    // 工作票下发响应
+    val RSP_SEND_WORK_TICKET = byteArrayOf(0x02, 0x02, 0x06, 0x02)
+
+    // 获取设备当前状态
+    val REQ_CURRENT_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x01)
+
+    // 获取当前设备响应
+    val RSP_CURRENT_STATUS = byteArrayOf(0x03, 0x02, 0x02, 0x01)
+
+    // 获取设备工作票完成情况
+    val REQ_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x01, 0x01, 0x02)
+
+    // 获取设备工作票完成情况响应
+    val RSP_WORK_TICKET_RESULT = byteArrayOf(0x03, 0x02)
+
+    // 获取设备工作票完成情况分包
+    val REQ_WORK_TICKET_RESULT_PART = byteArrayOf(0x03, 0x01, 0x06, 0x02)
+
+    // 获取钥匙电量
+    val REQ_POWER_STATUS = byteArrayOf(0x03, 0x01, 0x01, 0x03)
+
+    // 获取钥匙电量响应
+    val RSP_POWER_STATUS = byteArrayOf(0x03, 0x02, 0x03, 0x03)
+
+    // 传输文件
+    val REQ_TRANSFER_FILE = byteArrayOf(0x06, 0x01)
+
+    // 传输文件完成响应
+    val RSP_TRANSFER_FILE = byteArrayOf(0x06, 0x02)
+
+    // 获取固件版本号
+    val REQ_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x01, 0x02, 0x01, 0x01)
+
+    // 获取固件版本号响应
+    val RSP_GET_VERSION = byteArrayOf(0xEE.toByte(), 0x02, 0x03, 0x01)
+}

+ 13 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleListener.kt

@@ -0,0 +1,13 @@
+package com.grkj.ui_base.utils.ble
+
+interface BleIndicateListener {
+    /**
+     * 处理返回
+     */
+    fun handleRsp(
+        bleBean: BleBean,
+        byteArray: ByteArray,
+        isNeedLoading: Boolean = false,
+        prepareDoneCallBack: ((Boolean, BleBean?) -> Unit)?
+    )
+}

+ 204 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/BleUtil.kt

@@ -0,0 +1,204 @@
+package com.grkj.ui_base.utils.ble
+
+import android.app.Application
+import android.bluetooth.BluetoothGatt
+import android.os.Build
+import android.util.Log
+import com.clj.fastble.BleManager
+import com.clj.fastble.callback.BleGattCallback
+import com.clj.fastble.callback.BleMtuChangedCallback
+import com.clj.fastble.data.BleDevice
+import com.clj.fastble.exception.BleException
+import com.clj.fastble.scan.BleScanRuleConfig
+import com.grkj.ui_base.utils.extension.toHexStrings
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+
+/**
+ * 蓝牙工具类
+ */
+class BleUtil private constructor() {
+    private val logger: Logger = LoggerFactory.getLogger(BleUtil::class.java)
+
+    companion object {
+        var instance: BleUtil? = null
+            get() {
+                if (field == null) field = BleUtil()
+                return field
+            }
+            private set
+
+        const val OPERATE_TIMEOUT = 10 * 1000
+    }
+
+    fun initBle(application: Application?) {
+        try {
+            BleManager.getInstance().init(application)
+            BleManager.getInstance().enableLog(false).setConnectOverTime(10 * 1000L)
+                .setReConnectCount(3, 300) // 设置重新连接次数和间隔时间,默认为0次,不重连
+                .setSplitWriteNum(500).operateTimeout =
+                OPERATE_TIMEOUT // 设置操作readRssi、setMtu、write、read、notify、indicate的超时时间(毫秒)
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+                //Android 12及以上不允许添加过滤器
+                val bleScanRuleConfig = BleScanRuleConfig.Builder()
+//                .setServiceUuids(arrayOf(UUID.fromString(BLUETOOTH_SERVICEUUID)))
+                    .setDeviceName(true, BleConst.BLE_LOCAL_NAME).build()
+                BleManager.getInstance().initScanRule(bleScanRuleConfig)
+            }
+        } catch (e: Exception) {
+            Log.d("initBlueTooth", "蓝牙初始化:${e.message}")
+        }
+    }
+
+    fun scan(bleScanCallback: CustomBleScanCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().scan(bleScanCallback)
+            } else {
+                bleScanCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleScanCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun connectBySelect(bleDevice: BleDevice?, bleGattCallback: CustomBleGattCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().connect(bleDevice, bleGattCallback)
+            } else {
+                bleGattCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleGattCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun connectByMac(mac: String?, bleGattCallback: CustomBleGattCallback) {
+        if (BleManager.getInstance().isSupportBle) {
+            if (BleManager.getInstance().isBlueEnable) {
+                BleManager.getInstance().connect(mac, bleGattCallback)
+            } else {
+                bleGattCallback.onPrompt("请打开您的蓝牙后重试")
+            }
+        } else {
+            bleGattCallback.onPrompt("您的设备不支持蓝牙设备")
+        }
+    }
+
+    fun setMtu(bleDevice: BleDevice) {
+        BleManager.getInstance().setMtu(bleDevice, BleConst.MTU, object : BleMtuChangedCallback() {
+            override fun onSetMTUFailure(exception: BleException?) {
+//                indicate()
+            }
+
+            override fun onMtuChanged(mtu: Int) {
+//                indicate()
+            }
+        })
+    }
+
+    fun indicate(
+        bleDevice: BleDevice,
+        serviceUUID: String = BleConst.SERVICE_UUID,
+        indicateUUID: String = BleConst.INDICATE_UUID,
+        isStart: Boolean = true,
+        indicateCallback: CustomBleIndicateCallback?
+    ) {
+        // stopIndicate包含removeIndicateCallback
+//        BleManager.getInstance().removeIndicateCallback(bleDevice, indicateUUID)
+        BleManager.getInstance().stopIndicate(bleDevice, serviceUUID, indicateUUID)
+        if (!isStart) {
+            return
+        }
+        if (!BleManager.getInstance().isSupportBle) {
+            indicateCallback?.onPrompt("该设备不支持蓝牙BLE")
+            return
+        }
+
+        if (!BleManager.getInstance().isBlueEnable) {
+            indicateCallback?.onPrompt("蓝牙已关闭,请打开蓝牙后重试")
+            return
+        }
+        if (BleManager.getInstance().isConnected(bleDevice.mac)) {
+            BleManager.getInstance()
+                .indicate(bleDevice, serviceUUID, indicateUUID, indicateCallback)
+        } else {
+            BleManager.getInstance().connect(bleDevice.mac, object : BleGattCallback() {
+                override fun onStartConnect() {}
+                override fun onConnectFail(bleDevice: BleDevice, exception: BleException) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    indicateCallback?.onConnectPrompt("连接失败!请检查设备是否打开,并尝试重新连接 : $exception")
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice, gatt: BluetoothGatt, status: Int
+                ) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    BleManager.getInstance()
+                        .indicate(bleDevice, serviceUUID, indicateUUID, indicateCallback)
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice,
+                    gatt: BluetoothGatt,
+                    status: Int
+                ) {
+                    BleManager.getInstance().removeConnectGattCallback(device)
+                    indicateCallback?.onDisConnectPrompt("连接断开!请检查硬件状态,并尝试重新连接!")
+                }
+            })
+        }
+    }
+
+
+    fun write(
+        bleDevice: BleDevice,
+        serviceUUID: String = BleConst.SERVICE_UUID,
+        writeUUID: String = BleConst.INDICATE_UUID,
+        cmd: ByteArray?,
+        writeCallback: CustomBleWriteCallback?
+    ) {
+        logger.info("ble_write : ${cmd?.toHexStrings()}")
+        cmd ?: return
+        if (!BleManager.getInstance().isSupportBle) {
+            writeCallback?.onPrompt("该设备不支持蓝牙BLE")
+            return
+        }
+
+        if (!BleManager.getInstance().isBlueEnable) {
+            writeCallback?.onPrompt("蓝牙已关闭,请打开蓝牙后重试")
+            return
+        }
+        if (BleManager.getInstance().isConnected(bleDevice.mac)) {
+            BleManager.getInstance().write(bleDevice, serviceUUID, writeUUID, cmd, writeCallback)
+        } else {
+            BleManager.getInstance().connect(bleDevice.mac, object : BleGattCallback() {
+                override fun onStartConnect() {}
+                override fun onConnectFail(bleDevice: BleDevice, exception: BleException) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    writeCallback?.onConnectPrompt("连接失败!请检查设备是否打开,并尝试重新连接 : $exception")
+                }
+
+                override fun onConnectSuccess(
+                    bleDevice: BleDevice, gatt: BluetoothGatt, status: Int
+                ) {
+                    BleManager.getInstance().removeConnectGattCallback(bleDevice)
+                    BleManager.getInstance()
+                        .write(bleDevice, serviceUUID, writeUUID, cmd, writeCallback)
+                }
+
+                override fun onDisConnected(
+                    isActiveDisConnected: Boolean,
+                    device: BleDevice,
+                    gatt: BluetoothGatt,
+                    status: Int
+                ) {
+                    BleManager.getInstance().removeConnectGattCallback(device)
+                    writeCallback?.onDisConnectPrompt("连接断开!请检查硬件状态,并尝试重新连接!")
+                }
+            })
+        }
+    }
+}

+ 7 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleGattCallback.kt

@@ -0,0 +1,7 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.callback.BleGattCallback
+
+abstract class CustomBleGattCallback : BleGattCallback() {
+    abstract fun onPrompt(promptStr: String?)
+}

+ 9 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleIndicateCallback.kt

@@ -0,0 +1,9 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.callback.BleIndicateCallback
+
+abstract class CustomBleIndicateCallback : BleIndicateCallback() {
+    abstract fun onPrompt(promptStr: String?)
+    abstract fun onConnectPrompt(promptStr: String?)
+    abstract fun onDisConnectPrompt(promptStr: String?)
+}

+ 7 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleScanCallback.kt

@@ -0,0 +1,7 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.callback.BleScanCallback
+
+abstract class CustomBleScanCallback : BleScanCallback() {
+    abstract fun onPrompt(promptStr: String?)
+}

+ 9 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/ble/CustomBleWriteCallback.kt

@@ -0,0 +1,9 @@
+package com.grkj.ui_base.utils.ble
+
+import com.clj.fastble.callback.BleWriteCallback
+
+abstract class CustomBleWriteCallback : BleWriteCallback() {
+    open fun onPrompt(promptStr: String?) {}
+    open fun onConnectPrompt(promptStr: String?) {}
+    open fun onDisConnectPrompt(promptStr: String?) {}
+}

+ 30 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/CurrentModeEvent.kt

@@ -0,0 +1,30 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+import com.grkj.ui_base.utils.ble.BleBean
+
+/**
+ * 当前模式处理事件
+ */
+class CurrentModeEvent(
+    val bleBean: BleBean,
+    val mode: Byte
+) {
+    companion object {
+        /**
+         * 发送当前模式通知
+         */
+        @JvmStatic
+        fun sendCurrentModeEvent(
+            bleBean: BleBean,
+            mode: Byte
+        ) {
+            val currentModeEventBean = EventBean<CurrentModeEvent>(
+                EventConstants.EVENT_CURRENT_MODE,
+                CurrentModeEvent(bleBean, mode)
+            )
+            EventHelper.sendEvent(currentModeEventBean)
+        }
+    }
+}

+ 29 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceExceptionEvent.kt

@@ -0,0 +1,29 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 设备异常事件
+ */
+class DeviceExceptionEvent(
+    val type: Int,    // 0:钥匙 1:挂锁
+    val rfid: String?
+) {
+    companion object {
+        /**
+         * 发送设备一查给你通知
+         */
+        @JvmStatic
+        fun sendDeviceExceptionEvent(
+            type: Int,    // 0:钥匙 1:挂锁
+            rfid: String?
+        ) {
+            val deviceExceptionEventBean = EventBean<DeviceExceptionEvent>(
+                EventConstants.EVENT_DEVICE_EXCEPTION,
+                DeviceExceptionEvent(type, rfid)
+            )
+            EventHelper.sendEvent(deviceExceptionEventBean)
+        }
+    }
+}

+ 29 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/DeviceTakeUpdateEvent.kt

@@ -0,0 +1,29 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 设备取出事件
+ */
+class DeviceTakeUpdateEvent(
+    val deviceType: Int,    // 0:钥匙 1:挂锁
+    val nfc: String?
+) {
+    companion object {
+        /**
+         * 发送当前模式通知
+         */
+        @JvmStatic
+        fun sendCurrentModeEvent(
+            deviceType: Int,    // 0:钥匙 1:挂锁
+            nfc: String?
+        ) {
+            val deviceTakeUpdateEventBean = EventBean<DeviceTakeUpdateEvent>(
+                EventConstants.EVENT_DEVICE_TAKE,
+                DeviceTakeUpdateEvent(deviceType, nfc)
+            )
+            EventHelper.sendEvent(deviceTakeUpdateEventBean)
+        }
+    }
+}

+ 17 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/EventHelper.kt

@@ -0,0 +1,17 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import org.greenrobot.eventbus.EventBus
+
+/**
+ * 事件帮助
+ */
+object EventHelper {
+    /**
+     * 发送通知
+     */
+    @JvmStatic
+    fun sendEvent(eventBean: EventBean<*>) {
+        EventBus.getDefault().post(eventBean)
+    }
+}

+ 33 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/GetTicketStatusEvent.kt

@@ -0,0 +1,33 @@
+package com.grkj.ui_base.utils.event
+
+import com.clj.fastble.data.BleDevice
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 获取作业票状态事件
+ */
+class GetTicketStatusEvent(
+    val isSuccess: Boolean,
+    val bleDevice: BleDevice
+) {
+
+    companion object {
+        /**
+         * 发送获取作业票情况事件
+         */
+        @JvmStatic
+        fun sendGetTicketStatusEvent(
+            isSuccess: Boolean,
+            bleDevice: BleDevice
+        ) {
+            val getTicketStatusEvent = GetTicketStatusEvent(isSuccess, bleDevice)
+            val getTicketStatusEventBean =
+                EventBean<GetTicketStatusEvent>(
+                    EventConstants.EVENT_GET_TICKET_STATUS,
+                    getTicketStatusEvent
+                )
+            EventHelper.sendEvent(getTicketStatusEventBean)
+        }
+    }
+}

+ 23 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/LoadingEvent.kt

@@ -0,0 +1,23 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 加载事件
+ */
+class LoadingEvent(val msg: String? = null, val isShow: Boolean = false) {
+    companion object {
+        /**
+         * 发送加载通知
+         */
+        @JvmStatic
+        fun sendLoadingEvent(msg: String? = null, isShow: Boolean = false) {
+            val loadingEventBean = EventBean<LoadingEvent>(
+                EventConstants.EVENT_LOADING_CODE,
+                LoadingEvent(msg, isShow)
+            )
+            EventHelper.sendEvent(loadingEventBean)
+        }
+    }
+}

+ 26 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/SwitchCollectionUpdateEvent.kt

@@ -0,0 +1,26 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 开关采集更新事件
+ */
+class SwitchCollectionUpdateEvent {
+
+    companion object {
+        /**
+         * 发送获取作业票情况事件
+         */
+        @JvmStatic
+        fun sendSwitchCollectionUpdateEvent() {
+            val switchCollectionUpdateEvent = SwitchCollectionUpdateEvent()
+            val switchCollectionUpdateEventBean =
+                EventBean<SwitchCollectionUpdateEvent>(
+                    EventConstants.EVENT_SWITCH_COLLECTION_UPDATE,
+                    switchCollectionUpdateEvent
+                )
+            EventHelper.sendEvent(switchCollectionUpdateEventBean)
+        }
+    }
+}

+ 24 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/event/UpdateTicketProgressEvent.kt

@@ -0,0 +1,24 @@
+package com.grkj.ui_base.utils.event
+
+import com.grkj.shared.model.EventBean
+import com.grkj.ui_base.data.EventConstants
+
+/**
+ * 更新作业票事件
+ */
+class UpdateTicketProgressEvent(val ticketId: Long) {
+
+    companion object {
+        /**
+         * 发送更新作业票事件
+         */
+        @JvmStatic
+        fun sendUpdateTicketProgressEvent(ticketId: Long) {
+            val updateTicketProgressEvent = UpdateTicketProgressEvent(ticketId)
+            val updateTicketProgressEventEventBean = EventBean<UpdateTicketProgressEvent>(
+                EventConstants.EVENT_UPDATE_TICKET_PROGRESS, updateTicketProgressEvent
+            )
+            EventHelper.sendEvent(updateTicketProgressEventEventBean)
+        }
+    }
+}

+ 104 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/extension/ByteArray.kt

@@ -0,0 +1,104 @@
+package com.grkj.ui_base.utils.extension
+
+import android.util.Base64
+import com.grkj.ui_base.utils.CRC16
+import java.io.ByteArrayOutputStream
+import java.lang.Exception
+import kotlin.collections.indices
+import kotlin.experimental.xor
+import kotlin.ranges.coerceAtLeast
+import kotlin.ranges.coerceAtMost
+import kotlin.ranges.contains
+import kotlin.ranges.downTo
+import kotlin.ranges.until
+
+fun ByteArray.startsWith(prefix: ByteArray): Boolean {
+    require(this.size >= prefix.size) { "ByteArray is smaller than the prefix." }
+
+    for (i in prefix.indices) {
+        if (this[i] != prefix[i]) {
+            return false
+        }
+    }
+    return true
+}
+
+fun ByteArray.base64() : String {
+    return Base64.encodeToString(this, Base64.NO_WRAP)
+}
+
+private val hexEncodingTable = byteArrayOf(
+    '0'.toByte(), '1'.toByte(), '2'.toByte(), '3'.toByte(), '4'.toByte(), '5'.toByte(), '6'.toByte(), '7'.toByte(),
+    '8'.toByte(), '9'.toByte(), 'A'.toByte(), 'B'.toByte(), 'C'.toByte(), 'D'.toByte(), 'E'.toByte(), 'F'.toByte()
+)
+
+fun ByteArray.toHexStrings(space: Boolean = true) : String {
+    val out = ByteArrayOutputStream()
+    try {
+        for (i in 0 until size) {
+            val v: Int = this[i].toInt() and 0xff
+            out.write(hexEncodingTable[v ushr 4].toInt())
+            out.write(hexEncodingTable[v and 0xf].toInt())
+            if (space) {
+                out.write(' '.toInt())
+            }
+        }
+    } catch (e: java.lang.Exception) {
+        throw IllegalStateException("exception encoding Hex string: " + e.message, e)
+    }
+    val bytes = out.toByteArray()
+    return String(bytes)
+}
+
+const val RADIX_62 = 62
+
+/**
+ * 把[from, to) 部分的子数组,转换为以 0-9 a-z A-Z 共 62个字符组成的数字
+ *
+ * @param from 起始
+ * @param to 结束(不包含)
+ */
+fun ByteArray.to62Num(from: Int, to: Int) : Int {
+    var sum = 0
+    var scale = 1
+    for (i in to - 1 downTo from) {
+        sum += from62(this[i]) * scale
+        scale *= RADIX_62
+    }
+    return sum
+}
+
+/**
+ * 把[from, to) 部分的字节做异或
+ */
+fun ByteArray.xor(from: Int, to: Int) : Byte {
+    var res = this[from]
+    for (i in (from + 1) until to) {
+        res = res xor this[i]
+    }
+    return res
+}
+
+private fun from62(b: Byte): Int {
+    if (b in 0x30..0x39) {
+        return b - 0x30
+    }
+    if (b in 0x61..0x7a) {
+        return b - 0x57
+    }
+    if (b in 0x41..0x5a) {
+        return b - 0x1d
+    }
+    throw IllegalArgumentException("$b is not a num char")
+}
+
+/**
+ * 计算 [from, to) 部分的字节做 的 CRC16 校验值
+ * @return 两字节的校验值
+ */
+fun ByteArray.crc16(from: Int = 0, to: Int = size) : ByteArray {
+    val value = CRC16.crc16(this, from.coerceAtLeast(0), to.coerceAtMost(size))
+    val c1 = (0xff00 and value shr 8).toByte()
+    val c2 = (0xff and value).toByte()
+    return byteArrayOf(c1, c2)
+}

+ 52 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/extension/Context.kt

@@ -0,0 +1,52 @@
+package com.grkj.ui_base.utils.extension
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import androidx.lifecycle.Observer
+import com.grkj.ui_base.utils.NetManager
+import java.util.Locale
+
+/**
+ * 网络管理器
+ */
+fun Context.netManager() = NetManager.getInstance(applicationContext)
+
+fun Context.addNetObserver(observer: Observer<Boolean>) {
+    netManager().liveData.observeForever(observer)
+}
+
+fun Context.removeNetObserver(observer: Observer<Boolean>) {
+    netManager().liveData.removeObserver(observer)
+}
+
+@SuppressLint("MissingPermission")
+fun Context.serialNo(): String {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
+        && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
+        == PackageManager.PERMISSION_GRANTED
+    ) {
+        return Build.getSerial().uppercase(Locale.ROOT)
+    }
+    return Build.SERIAL.uppercase(Locale.ROOT)
+}
+
+/**
+ * 检查权限
+ */
+fun Context.checkPermissions(permissions: Array<String>): Boolean {
+    for (permission in permissions) {
+        if (ContextCompat.checkSelfPermission(
+                this,
+                permission
+            ) != PackageManager.PERMISSION_GRANTED
+        ) {
+            return false
+        }
+    }
+    return true
+}

+ 20 - 0
ui-base/src/main/java/com/grkj/ui_base/utils/extension/Int.kt

@@ -0,0 +1,20 @@
+package com.grkj.ui_base.utils.extension
+
+import kotlin.ranges.until
+
+//fun Int.toByteArray(capability: Int = 2): ByteArray {
+//    return ByteBuffer.allocate(capability)
+//        .order(ByteOrder.BIG_ENDIAN) // 可以根据需要改为 ByteOrder.LITTLE_ENDIAN
+//        .putShort(this.toShort()) // 只取低16位
+//        .array()
+//}
+
+fun Int.toByteArray(capability: Int = 2): ByteArray {
+//    require(capability in 1..4) { "Length must be between 1 and 4" }
+    val bytes = ByteArray(capability)
+    for (i in 0 until capability) {
+//        bytes[capability - i - 1] = ((this ushr (i * 8)) and 0xFF).toByte() // 大端模式
+        bytes[i] = ((this ushr (i * 8)) and 0xFF).toByte()  // 小端模式
+    }
+    return bytes
+}

部分文件因为文件数量过多而无法显示