Переглянути джерело

chore(依赖):
- 升级通信库`sik-comm`版本从`1.0.19`至`1.0.20`。
- `shared`: 新增`hardware-sdk-release.aar`依赖。

refactor(指纹):
- `FingerprintCaptureService`:
- 重构了`upImageOnce`和`getCharOnce`中的分包接收逻辑,提取并统一使用可复用的`buildReadUntilLastPolicy`策略来处理`PID_DATA`和`PID_LAST`帧,简化了代码并提高了健壮性。
- 优化了`listen`方法中处理`DATA/LAST`帧的逻辑,增加了超时和间隙配置,并修正了状态清理逻辑,确保在连续采集时状态正确。
- `FingerprintUtil`: 将`FingerprintCaptureService`的注册和监听操作移至IO线程池执行,避免在主线程进行耗时操作。

refactor(启动):
- `SplashActivity`:
- `iscs_lock`: 移除已废弃的`GlobalManager`和`CronJobManager`的直接调用。
- `iscs_mc`: 将所有初始化逻辑包装在`window.decorView.post`中,确保在视图完全附加到窗口后再执行,提高了启动流程的稳定性。

refactor(代码):
- `Hlk223Frames`: 修正了`CommMessage`的构造函数调用,确保返回正确的对象实例。

chore(代码):
- 删除了`iscs_lock`模块中已不再使用的`GlobalManager.kt`文件。
- 注释掉了`iscs_lock/AndroidManifest.xml`中对`TaskService`的声明。
- 注释掉了不再使用的`CheckKeyInfoTask.kt`文件及其相关`CronJob`逻辑。

周文健 4 годин тому
батько
коміт
d4ef1a9170

+ 1 - 1
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt

@@ -129,7 +129,7 @@ object Hlk223 {
             gapMs?.let     { put("silenceGapMs", it) }
             put("rawFrame", frame)
         }
-        return CommMessage(
+        return  CommMessage(
             command = "HLK_MID_${mid.toString(16).uppercase().padStart(2, '0')}",
             payload = frame,
             metadata = meta

+ 82 - 74
data/src/main/java/com/grkj/data/hardware/fingerprint/FingerprintCaptureService.kt

@@ -1,6 +1,7 @@
 package com.grkj.data.hardware.fingerprint
 
 import android.util.Base64
+import com.grkj.data.hardware.face.hlk.LoggerPlugin
 import com.grkj.shared.utils.extension.toHexStrings
 import com.machinezoo.sourceafis.FingerprintTemplate
 import com.sik.comm.codec.PassThroughCodec
@@ -48,26 +49,33 @@ object FingerprintCaptureService {
 
     private val chunks = ArrayList<ByteArray>(256)
     private var isLast = false
-    private val policy = object : ChainPolicy {
+    private val policy get() = object : ChainPolicy {
         override suspend fun afterSendStep(
             stepIndex: Int,
             sent: CommMessage,
             io: LinkIO
         ): ChainStepResult {
-            isLast = false
-            chunks.clear()
+            if (stepIndex == 0 || isLast) {
+                isLast = false
+                chunks.clear()
+            }
             val deadlineNs = System.nanoTime() + 15_000_000_000L
             while (System.nanoTime() < deadlineNs) {
                 val rsp = try {
-                    io.readRaw(timeoutMs = 600, expectedSize = null, silenceGapMs = 8)
+                    io.readRaw(timeoutMs = 1000, expectedSize = null, silenceGapMs = 40)
                 } catch (e: Exception) {
-                    if (isLast) return ChainStepResult(emptyList(), false)
+                    if (chunks.isNotEmpty()) {
+                        return ChainStepResult(emptyList(), false)
+                    }
+                    // 如果已经处理过 LAST(理论上不会到这儿),就结束
+                    if (isLast) return ChainStepResult(emptyList(), true)
                     delay(10)
                     continue
                 }
                 log.debug("readRaw(): got {}B", rsp.payload.size)
                 val frames = framer.feed(rsp.payload)
                 for (f in frames) {
+                    log.debug("指令内容:${f.toHexStrings()}")
                     when (f.getOrNull(6)) {
                         FpmPackets.PID_ACK -> { /* 可读确认码;不强制 */
                         }
@@ -79,10 +87,16 @@ object FingerprintCaptureService {
                         FpmPackets.PID_LAST -> {
                             chunks += FpmPackets.parseDataLike(f)
                             isLast = true
-                            return ChainStepResult(emptyList(), false)
+                            val result = ChainStepResult(emptyList(), true)
+                            // 状态保持到下一轮由 stepIndex==0 再清理
+                            return result
                         }
                     }
                 }
+                // 如果本轮读到了若干 DATA,但还没等到 LAST,可以先上交,提升管道吞吐
+                if (chunks.isNotEmpty()) {
+                    return ChainStepResult(emptyList(), false)
+                }
             }
             if (!isLast) error("UP_IMAGE not finished (no LAST) within deadline")
             return ChainStepResult(emptyList(), false)
@@ -356,94 +370,88 @@ object FingerprintCaptureService {
 
     /** 发 UP_IMAGE,一直读到 PID=0x08(LAST)为止 */
     private suspend fun upImageOnce(proto: ModbusProtocol): ByteArray {
-        framer.feed(ByteArray(0)) // 确保内部状态干净(可选)
+        framer.feed(ByteArray(0))
         val start = FpmPackets.cmd(address, FpmIns.UP_IMAGE)
-        val plan = TxPlan(
-            listOf(
-                FpmPackets.toMessage(
-                    "FPM_UP_IMAGE",
-                    start,
-                    timeoutMs = 15000,
-                    gapMs = 60
-                )
-            )
-        )
+        val plan = TxPlan(listOf(FpmPackets.toMessage("FPM_UP_IMAGE", start, timeoutMs = 15000, gapMs = 60)))
 
         val chunks = ArrayList<ByteArray>(256)
-        var isLast = false
-        val policy = object : ChainPolicy {
-            override suspend fun afterSendStep(
-                stepIndex: Int,
-                sent: CommMessage,
-                io: LinkIO
-            ): ChainStepResult {
-                val deadlineNs = System.nanoTime() + 15_000_000_000L
-                while (System.nanoTime() < deadlineNs) {
-                    val rsp = try {
-                        io.readRaw(timeoutMs = 600, expectedSize = null, silenceGapMs = 8)
-                    } catch (e: Exception) {
-                        if (isLast) return ChainStepResult(emptyList(), false)
-                        delay(10)
-                        continue
-                    }
-                    log.debug("readRaw(): got {}B", rsp.payload.size)
-                    val frames = framer.feed(rsp.payload)
-                    for (f in frames) {
-                        when (f.getOrNull(6)) {
-                            FpmPackets.PID_ACK -> { /* 可读确认码;不强制 */
-                            }
+        val policy = buildReadUntilLastPolicy(framer, chunks, readTimeoutMs = 600, silenceGapMs = 8, hardDeadlineMs = 15000)
 
-                            FpmPackets.PID_DATA -> {
-                                chunks += FpmPackets.parseDataLike(f)
-                            }
+        proto.sendChain(deviceId, plan, policy)
 
-                            FpmPackets.PID_LAST -> {
-                                chunks += FpmPackets.parseDataLike(f)
-                                isLast = true
-                                return ChainStepResult(emptyList(), false)
-                            }
+        val total = chunks.sumOf { it.size }
+        return ByteArray(total).also { out ->
+            var pos = 0; for (c in chunks) { System.arraycopy(c, 0, out, pos, c.size); pos += c.size }
+        }
+    }
+
+
+    // 统一的读取策略:直到收到 PID_LAST,一次性把 chunks 交上去
+    private fun buildReadUntilLastPolicy(
+        framer: FpmFramer,
+        chunksOut: MutableList<ByteArray>,
+        lastPid: Byte = FpmPackets.PID_LAST,
+        dataPid: Byte = FpmPackets.PID_DATA,
+        ackPid: Byte = FpmPackets.PID_ACK,
+        readTimeoutMs: Int = 600,        // 单次 read
+        silenceGapMs: Int = 8,
+        hardDeadlineMs: Long = 15_000    // 整体死线
+    ): ChainPolicy = object : ChainPolicy {
+        override suspend fun afterSendStep(stepIndex: Int, sent: CommMessage, io: LinkIO): ChainStepResult {
+            chunksOut.clear()
+            val deadlineNs = System.nanoTime() + hardDeadlineMs * 1_000_000
+            var gotLast = false
+
+            while (System.nanoTime() < deadlineNs) {
+                val rsp = try {
+                    io.readRaw(timeoutMs = readTimeoutMs, expectedSize = null, silenceGapMs = silenceGapMs)
+                } catch (_: Throwable) {
+                    // 读超时,继续等,交给整体死线兜底
+                    continue
+                }
+                val frames = framer.feed(rsp.payload)
+                for (f in frames) {
+                    when (f.getOrNull(6)) {
+                        ackPid -> { /* ignore or log */ }
+                        dataPid -> chunksOut += FpmPackets.parseDataLike(f)
+                        lastPid -> {
+                            chunksOut += FpmPackets.parseDataLike(f)
+                            gotLast = true
                         }
                     }
                 }
-                if (!isLast) error("UP_IMAGE not finished (no LAST) within deadline")
-                return ChainStepResult(emptyList(), false)
+                if (gotLast) {
+                    // 一次性交付,停止链路
+                    return ChainStepResult(
+                        received = listOf(CommMessage("CHAIN_AGG", ByteArray(0), emptyMap())),
+                        continueNext = false,
+                        interFrameDelayMs = 0
+                    )
+                }
             }
+            error("UP_IMAGE/GET_CHAR not finished (no LAST) within $hardDeadlineMs ms")
         }
-        proto.sendChain(deviceId, plan, policy)
-
-        val total = chunks.sumOf { it.size }
-        val out = ByteArray(total)
-        var pos = 0
-        for (c in chunks) {
-            System.arraycopy(c, 0, out, pos, c.size); pos += c.size
-        }
-        return out
     }
 
+
     /** 发 GET_CHAR,一直读到 PID=0x08(LAST)为止 */
     private suspend fun getCharOnce(proto: ModbusProtocol): ByteArray {
-        framer.feed(ByteArray(0)) // 确保内部状态干净(可选)
+        framer.feed(ByteArray(0))
         val start = FpmPackets.cmd(address, FpmIns.GET_CHAR, byteArrayOf(FpmIns.CHAR_BUFFER_ID))
-        val plan = TxPlan(
-            listOf(
-                FpmPackets.toMessage(
-                    "FPM_GET_CHAR",
-                    start,
-                    timeoutMs = 15000,
-                    gapMs = 60
-                )
-            )
-        )
+        val plan = TxPlan(listOf(FpmPackets.toMessage("FPM_GET_CHAR", start, timeoutMs = 15000, gapMs = 60)))
+
+        val chunks = ArrayList<ByteArray>(256)
+        val policy = buildReadUntilLastPolicy(framer, chunks, readTimeoutMs = 1000, silenceGapMs = 40, hardDeadlineMs = 15000)
+
         proto.sendChain(deviceId, plan, policy)
+
         val total = chunks.sumOf { it.size }
-        val out = ByteArray(total)
-        var pos = 0
-        for (c in chunks) {
-            System.arraycopy(c, 0, out, pos, c.size); pos += c.size
+        return ByteArray(total).also { out ->
+            var pos = 0; for (c in chunks) { System.arraycopy(c, 0, out, pos, c.size); pos += c.size }
         }
-        return out
     }
 
+
     // === 解码自适应 ===
 
     private enum class NibbleOrder { HI_LO, LO_HI }

+ 1 - 1
gradle/libs.versions.toml

@@ -24,7 +24,7 @@ ksp = "2.1.10-1.0.31"
 nav_version = "2.9.0"
 kotlin_serialization_json = "1.7.3"
 fastble = "1.4.2"
-sikcomm = "1.0.19"
+sikcomm = "1.0.20"
 
 [libraries]
 androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }

+ 0 - 4
iscs_lock/src/main/AndroidManifest.xml

@@ -76,10 +76,6 @@
                 <action android:name="android.media.AUDIO_BECOMEING_NOISY" />
             </intent-filter>
         </receiver>
-<!--        <service-->
-<!--            android:name="com.sik.cronjob.services.TaskService"-->
-<!--            android:exported="false"-->
-<!--            android:process=":CronJobService" />-->
 
         <meta-data
             android:name="design_width_in_dp"

+ 0 - 15
iscs_lock/src/main/java/com/grkj/iscs/common/GlobalManager.kt

@@ -1,15 +0,0 @@
-package com.grkj.iscs.common
-
-import com.sik.cronjob.managers.CronJobManager
-import com.sik.sikcore.SIKCore
-
-/**
- * 全局管理器
- */
-object GlobalManager {
-
-    /**
-     * 任务管理器
-     */
-    val cronJobManager: CronJobManager by lazy { CronJobManager.getInstance(SIKCore.getApplication()) }
-}

+ 14 - 5
iscs_lock/src/main/java/com/grkj/iscs/features/splash/activity/SplashActivity.kt

@@ -6,14 +6,13 @@ import androidx.activity.viewModels
 import androidx.lifecycle.ProcessLifecycleOwner
 import androidx.lifecycle.lifecycleScope
 import com.grkj.data.data.MMKVConstants
+import com.grkj.data.enums.HardwareMode
 import com.grkj.data.local.database.BackupScheduler
 import com.grkj.data.local.database.DbReadyGate
 import com.grkj.data.local.database.ISCSDatabase
 import com.grkj.data.local.database.PresetData
-import com.grkj.data.enums.HardwareMode
 import com.grkj.data.utils.event.StartListenerEvent
 import com.grkj.iscs.R
-import com.grkj.iscs.common.GlobalManager
 import com.grkj.iscs.databinding.ActivitySplashBinding
 import com.grkj.iscs.features.init.activity.InitActivity
 import com.grkj.iscs.features.login.activity.LoginActivity
@@ -31,7 +30,10 @@ import com.sik.sikandroid.permission.PermissionUtils
 import com.sik.sikcore.extension.getMMKVData
 import com.sik.sikcore.extension.saveMMKVData
 import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.*
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlinx.coroutines.withTimeoutOrNull
 import java.util.Locale
 
 @AndroidEntryPoint
@@ -60,7 +62,12 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
     private fun routeAndWarmup() {
         // 先决定去哪里 → 立刻跳转并结束自己,释放主线程压力
         val isAppInit = MMKVConstants.APP_INIT.getMMKVData(false)
-        startActivity(Intent(this, if (isAppInit) LoginActivity::class.java else InitActivity::class.java))
+        startActivity(
+            Intent(
+                this,
+                if (isAppInit) LoginActivity::class.java else InitActivity::class.java
+            )
+        )
         finish()
 
         // 后台继续:单并发 IO,避免并行把 GC/JIT 压爆
@@ -118,7 +125,9 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
             val targetRegion = "US"
             val entries = LanguageRegistry.entriesFromSources(targetRegion)
             LanguageStore.setExplicit(
-                Locale.forLanguageTag(entries.find { it.region == targetRegion }?.tag ?: entries[0].tag)
+                Locale.forLanguageTag(
+                    entries.find { it.region == targetRegion }?.tag ?: entries[0].tag
+                )
             )
         }
         val dialogXTextInfo = TextInfo()

+ 34 - 31
iscs_mc/src/main/java/com/grkj/iscs_mc/features/splash/activity/SplashActivity.kt

@@ -47,40 +47,43 @@ class SplashActivity : BaseActivity<ActivitySplashBinding>() {
 
     override fun initView() {
         // 应用启动时按已存配置安排下一次(默认=每天 00:00)
-        GlobalManager.cronJobManager.bindService()
-        initConfig()
-        PermissionUtils.checkAndRequestPermissions(Constants.needPermission) {
-            logger.info("授权结果:${it}")
-            if (!it) {
-                PermissionUtils.requestAllFilesAccessPermission {
-                    logger.info("授权结果:${it}")
-                    if (it) {
-                        initDatabase()
+        window.decorView.post {
+            GlobalManager.cronJobManager.bindService()
+            initConfig()
+            PermissionUtils.checkAndRequestPermissions(Constants.needPermission) {
+                logger.info("授权结果:${it}")
+                if (!it) {
+                    PermissionUtils.requestAllFilesAccessPermission {
+                        logger.info("授权结果:${it}")
+                        if (it) {
+                            initDatabase()
+                        }
                     }
+                } else {
+                    initDatabase()
                 }
-            } else {
-                initDatabase()
             }
-        }
-        lifecycleScope.launch(Dispatchers.IO) {
-            BackupScheduler.applySaved(this@SplashActivity)
-            DbReadyGate.await()
-            MMKVConstants.KEY_HARDWARE_MODE.saveMMKVData(HardwareMode.CAN.name)
-            val hardwareMode = MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.CAN.name)
-            if (hardwareMode.isNotEmpty()) {
-                StartListenerEvent.sendStartListenerEvent()
-                HardwareMode.getCurrentHardwareMode()
-                    .connectAndAddListener()
-            }
-            val targetRegion = "CN" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
-            val entries = LanguageRegistry.entriesFromSources(targetRegion)
-            PresetData.targetRegion =
-                entries.find { it.isSelected }?.region ?: targetRegion
-            withContext(Dispatchers.Main) {
-                viewModel.checkPresetData().observe(this@SplashActivity) {
-                    viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
-                        startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
-                        finish()
+            lifecycleScope.launch(Dispatchers.IO) {
+                BackupScheduler.applySaved(this@SplashActivity)
+                DbReadyGate.await()
+                MMKVConstants.KEY_HARDWARE_MODE.saveMMKVData(HardwareMode.CAN.name)
+                val hardwareMode =
+                    MMKVConstants.KEY_HARDWARE_MODE.getMMKVData(HardwareMode.CAN.name)
+                if (hardwareMode.isNotEmpty()) {
+                    StartListenerEvent.sendStartListenerEvent()
+                    HardwareMode.getCurrentHardwareMode()
+                        .connectAndAddListener()
+                }
+                val targetRegion = "CN" // 自己决定用什么;也可以是 "CN" / 配置项 / 服务器下发
+                val entries = LanguageRegistry.entriesFromSources(targetRegion)
+                PresetData.targetRegion =
+                    entries.find { it.isSelected }?.region ?: targetRegion
+                withContext(Dispatchers.Main) {
+                    viewModel.checkPresetData().observe(this@SplashActivity) {
+                        viewModel.checkSysMenuAndRole().observe(this@SplashActivity) {
+                            startActivity(Intent(this@SplashActivity, LoginActivity::class.java))
+                            finish()
+                        }
                     }
                 }
             }

BIN
shared/libs/hardware-sdk-release.aar


+ 151 - 151
ui-base/src/main/java/com/grkj/ui_base/service/CheckKeyInfoTask.kt

@@ -1,151 +1,151 @@
-package com.grkj.ui_base.service
-
-import android.annotation.SuppressLint
-import com.grkj.ui_base.business.HardwareBusinessManager
-import com.grkj.data.hardware.ble.BleSendDispatcher
-import com.sik.cronjob.annotations.CronJob
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import org.slf4j.Logger
-import org.slf4j.LoggerFactory
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
-
-/**
- * 检查钥匙信息任务
- *
- * - 支持在检查过程中“挂起等待 isInLogin=true 后继续”
- * - 防重入:同一时刻只会有一个检查任务在跑
- * - 仍保留 CronJob 定时入口;也支持在进入登录页时延迟启动一次
- */
-class CheckKeyInfoTask {
-
-    private val logger: Logger = LoggerFactory.getLogger(this::class.java)
-    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
-    private val runMutex = Mutex()
-
-    /** 闸门:true = 允许跑(即 isInLogin == false) */
-    private val runGate = MutableStateFlow(false)
-
-    private var startCheckKeyInfoJob: Job? = null
-
-    /** 参数可按需调整 */
-    private companion object {
-        const val STABLE_MS = 1200L          // 退出登录需稳定的时间窗
-        const val DELAY_AFTER_EXIT_MS = 60_000L // 退出登录后再延迟启动(低优先级)
-        const val CHECK_TIMEOUT_MS = 10_000L // 单次 BLE 信息获取的超时时间
-    }
-
-    /**
-     * 登录态:false = 允许跑;true = 暂停(必须在非登录下运行)
-     */
-    var isInLogin: Boolean = false
-        set(value) {
-            field = value
-            runGate.value = !value // 反向:非登录 → 允许跑
-
-            // 触发策略:只有“退出登录”(value=false)才计划一次低优先级检查
-            startCheckKeyInfoJob?.cancel()
-            if (!value) {
-                // 抗抖 + 延迟(降低优先级,不跟业务抢)
-                startCheckKeyInfoJob = scope.launch {
-                    awaitNotInLogin(stableMs = STABLE_MS) // 先稳定
-                    delay(DELAY_AFTER_EXIT_MS)
-                    safeCheckKeyInfo()
-                }
-            } else {
-                startCheckKeyInfoJob = null
-            }
-        }
-
-    @SuppressLint("MissingPermission")
-    @CronJob(intervalMillis = 30 * 60_000L, initialDelay = 0, runOnMainThread = false)
-    fun checkKeyInfo() {
-        scope.launch { safeCheckKeyInfo() }
-    }
-
-    @SuppressLint("MissingPermission")
-    private suspend fun safeCheckKeyInfo() {
-        // 不在锁里等闸门,避免大锁长期被占
-        awaitNotInLogin()
-        runMutex.withLock {
-            logger.info("开始检查钥匙信息(非登录态)")
-            for (mac in HardwareBusinessManager.getExistsKeyMac()) {
-                mac?.let { handleSingleMac(it) } ?: continue
-            }
-            logger.info("检查钥匙信息结束")
-        }
-    }
-
-    private suspend fun handleSingleMac(mac: String) {
-        // 任意时刻闸门关了就暂停
-        awaitNotInLogin()
-        waitUntilCanConnect() // 若无可用连接位,轻量等待;闸门关了会提前返回
-
-        if (!runGate.value) {
-            logger.info("检测到进入登录页,暂停检查;mac=$mac")
-            awaitNotInLogin()
-        }
-
-        awaitBleCheck(mac) // 取信息 + 强制断开(见下)
-    }
-
-    /** 统一策略:完成或取消都 scheduleDisconnect,确保单连接芯片不被占坑 */
-    @SuppressLint("MissingPermission")
-    private suspend fun awaitBleCheck(mac: String) {
-        withTimeout(CHECK_TIMEOUT_MS) {
-            suspendCancellableCoroutine { cont ->
-                // 监听闸门:一旦进入登录页(runGate=false),立即取消
-                val watcher = scope.launch {
-                    runGate
-                        .first { allowed -> !allowed } // 变为不允许时
-                    if (cont.isActive) cont.cancel(CancellationException("Gate closed"))
-                }
-
-                cont.invokeOnCancellation {
-                    try { BleSendDispatcher.scheduleDisconnect(mac) } catch (_: Throwable) {}
-                    watcher.cancel()
-                }
-
-                BleSendDispatcher.submit(mac) { ok ->
-                    // 拿到信息后,无条件断开(不依赖实时 isInLogin,避免竞态)
-                    try { BleSendDispatcher.scheduleDisconnect(mac) } catch (_: Throwable) {}
-                    if (cont.isActive) cont.resume(Unit) {}
-                    watcher.cancel()
-                }
-            }
-        }
-    }
-
-    /** 等待“非登录态”且稳定 STABLE_MS,真正挂起,不 busy-wait */
-    private suspend fun awaitNotInLogin(stableMs: Long = STABLE_MS) {
-        if (runGate.value) return
-        logger.info("当前在登录页,挂起等待退出登录…")
-        runGate
-            .debounce(stableMs)          // 抗抖:需稳定一段时间
-            .first { it }                // 等到允许跑
-        logger.info("已退出登录且稳定 ${stableMs}ms,继续检查")
-    }
-
-    /** 轻量的连接位等待:闸门关闭或超时就让路;指数退避,避免忙等 */
-    private suspend fun waitUntilCanConnect(maxWaitMillis: Long = 5_000L) {
-        val start = System.currentTimeMillis()
-        var delayMs = 100L
-        while (!BleSendDispatcher.canConnect()) {
-            if (!runGate.value) return // 闸门关了,优先让路
-            if (System.currentTimeMillis() - start > maxWaitMillis) break
-            delay(delayMs)
-            delayMs = (delayMs * 2).coerceAtMost(800L)
-        }
-    }
-
-    /** 建议加:在宿主销毁时调用,避免悬挂 */
-    fun close() {
-        startCheckKeyInfoJob?.cancel()
-        scope.cancel()
-    }
-}
+//package com.grkj.ui_base.service
+//
+//import android.annotation.SuppressLint
+//import com.grkj.ui_base.business.HardwareBusinessManager
+//import com.grkj.data.hardware.ble.BleSendDispatcher
+//import com.sik.cronjob.annotations.CronJob
+//import kotlinx.coroutines.*
+//import kotlinx.coroutines.flow.MutableStateFlow
+//import kotlinx.coroutines.flow.first
+//import kotlinx.coroutines.sync.Mutex
+//import kotlinx.coroutines.sync.withLock
+//import org.slf4j.Logger
+//import org.slf4j.LoggerFactory
+//import kotlinx.coroutines.flow.debounce
+//import kotlinx.coroutines.flow.distinctUntilChanged
+//import kotlinx.coroutines.flow.map
+//
+///**
+// * 检查钥匙信息任务
+// *
+// * - 支持在检查过程中“挂起等待 isInLogin=true 后继续”
+// * - 防重入:同一时刻只会有一个检查任务在跑
+// * - 仍保留 CronJob 定时入口;也支持在进入登录页时延迟启动一次
+// */
+//class CheckKeyInfoTask {
+//
+//    private val logger: Logger = LoggerFactory.getLogger(this::class.java)
+//    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
+//    private val runMutex = Mutex()
+//
+//    /** 闸门:true = 允许跑(即 isInLogin == false) */
+//    private val runGate = MutableStateFlow(false)
+//
+//    private var startCheckKeyInfoJob: Job? = null
+//
+//    /** 参数可按需调整 */
+//    private companion object {
+//        const val STABLE_MS = 1200L          // 退出登录需稳定的时间窗
+//        const val DELAY_AFTER_EXIT_MS = 60_000L // 退出登录后再延迟启动(低优先级)
+//        const val CHECK_TIMEOUT_MS = 10_000L // 单次 BLE 信息获取的超时时间
+//    }
+//
+//    /**
+//     * 登录态:false = 允许跑;true = 暂停(必须在非登录下运行)
+//     */
+//    var isInLogin: Boolean = false
+//        set(value) {
+//            field = value
+//            runGate.value = !value // 反向:非登录 → 允许跑
+//
+//            // 触发策略:只有“退出登录”(value=false)才计划一次低优先级检查
+//            startCheckKeyInfoJob?.cancel()
+//            if (!value) {
+//                // 抗抖 + 延迟(降低优先级,不跟业务抢)
+//                startCheckKeyInfoJob = scope.launch {
+//                    awaitNotInLogin(stableMs = STABLE_MS) // 先稳定
+//                    delay(DELAY_AFTER_EXIT_MS)
+//                    safeCheckKeyInfo()
+//                }
+//            } else {
+//                startCheckKeyInfoJob = null
+//            }
+//        }
+//
+//    @SuppressLint("MissingPermission")
+//    @CronJob(intervalMillis = 30 * 60_000L, initialDelay = 0, runOnMainThread = false)
+//    fun checkKeyInfo() {
+//        scope.launch { safeCheckKeyInfo() }
+//    }
+//
+//    @SuppressLint("MissingPermission")
+//    private suspend fun safeCheckKeyInfo() {
+//        // 不在锁里等闸门,避免大锁长期被占
+//        awaitNotInLogin()
+//        runMutex.withLock {
+//            logger.info("开始检查钥匙信息(非登录态)")
+//            for (mac in HardwareBusinessManager.getExistsKeyMac()) {
+//                mac?.let { handleSingleMac(it) } ?: continue
+//            }
+//            logger.info("检查钥匙信息结束")
+//        }
+//    }
+//
+//    private suspend fun handleSingleMac(mac: String) {
+//        // 任意时刻闸门关了就暂停
+//        awaitNotInLogin()
+//        waitUntilCanConnect() // 若无可用连接位,轻量等待;闸门关了会提前返回
+//
+//        if (!runGate.value) {
+//            logger.info("检测到进入登录页,暂停检查;mac=$mac")
+//            awaitNotInLogin()
+//        }
+//
+//        awaitBleCheck(mac) // 取信息 + 强制断开(见下)
+//    }
+//
+//    /** 统一策略:完成或取消都 scheduleDisconnect,确保单连接芯片不被占坑 */
+//    @SuppressLint("MissingPermission")
+//    private suspend fun awaitBleCheck(mac: String) {
+//        withTimeout(CHECK_TIMEOUT_MS) {
+//            suspendCancellableCoroutine { cont ->
+//                // 监听闸门:一旦进入登录页(runGate=false),立即取消
+//                val watcher = scope.launch {
+//                    runGate
+//                        .first { allowed -> !allowed } // 变为不允许时
+//                    if (cont.isActive) cont.cancel(CancellationException("Gate closed"))
+//                }
+//
+//                cont.invokeOnCancellation {
+//                    try { BleSendDispatcher.scheduleDisconnect(mac) } catch (_: Throwable) {}
+//                    watcher.cancel()
+//                }
+//
+//                BleSendDispatcher.submit(mac) { ok ->
+//                    // 拿到信息后,无条件断开(不依赖实时 isInLogin,避免竞态)
+//                    try { BleSendDispatcher.scheduleDisconnect(mac) } catch (_: Throwable) {}
+//                    if (cont.isActive) cont.resume(Unit) {}
+//                    watcher.cancel()
+//                }
+//            }
+//        }
+//    }
+//
+//    /** 等待“非登录态”且稳定 STABLE_MS,真正挂起,不 busy-wait */
+//    private suspend fun awaitNotInLogin(stableMs: Long = STABLE_MS) {
+//        if (runGate.value) return
+//        logger.info("当前在登录页,挂起等待退出登录…")
+//        runGate
+//            .debounce(stableMs)          // 抗抖:需稳定一段时间
+//            .first { it }                // 等到允许跑
+//        logger.info("已退出登录且稳定 ${stableMs}ms,继续检查")
+//    }
+//
+//    /** 轻量的连接位等待:闸门关闭或超时就让路;指数退避,避免忙等 */
+//    private suspend fun waitUntilCanConnect(maxWaitMillis: Long = 5_000L) {
+//        val start = System.currentTimeMillis()
+//        var delayMs = 100L
+//        while (!BleSendDispatcher.canConnect()) {
+//            if (!runGate.value) return // 闸门关了,优先让路
+//            if (System.currentTimeMillis() - start > maxWaitMillis) break
+//            delay(delayMs)
+//            delayMs = (delayMs * 2).coerceAtMost(800L)
+//        }
+//    }
+//
+//    /** 建议加:在宿主销毁时调用,避免悬挂 */
+//    fun close() {
+//        startCheckKeyInfoJob?.cancel()
+//        scope.cancel()
+//    }
+//}

+ 13 - 10
ui-base/src/main/java/com/grkj/ui_base/utils/fingerprint/FingerprintUtil.kt

@@ -85,16 +85,19 @@ object FingerprintUtil {
         if (!enumSensor()) {
             logger.info("Device not found!,尝试更换模组")
             try {
-                FingerprintCaptureService.register("/dev/ttyS0").also { fingerprintCaptureService = it }
-                    .listen({
-                        onScanListener?.onStart()
-                    }, {
-                        ImageConvertUtils.base64ToBitmap(it)?.let { bmp ->
-                            onScanListener?.onScan(bmp)
-                        }
-                    }, {
-                        logger.info("指纹异常:${it}")
-                    })
+                ThreadUtils.runOnIO {
+                    FingerprintCaptureService.register("/dev/ttyS0")
+                        .also { fingerprintCaptureService = it }
+                        .listen({
+                            onScanListener?.onStart()
+                        }, {
+                            ImageConvertUtils.base64ToBitmap(it)?.let { bmp ->
+                                onScanListener?.onScan(bmp)
+                            }
+                        }, {
+                            logger.info("指纹异常:${it}")
+                        })
+                }
             } catch (e: Exception) {
                 logger.info("指纹模组不支持")
             }