Kaynağa Gözat

refactor(人脸/指纹):
- `ArcSoftUtil`, `BiometricVerifier`: 将虹软人脸和指纹比对工具类从 `shared` 模块移至 `data/hardware` 下,使其职责更清晰地归属于硬件层。
- `shared/build.gradle.kts`: 将 `sourceafis` 指纹比对库的依赖从 `implementation` 改为 `api`,以便上层模块可以直接使用。
- 各 UI 模块 (`iscs_lock`, `iscs_mc`): 更新对 `ArcSoftUtil` 的引用路径。

feat(人脸):
- `ArcSoftUtil`:
- 新增后端切换机制,引入 `FaceBackend` (ARC/HLK),并提供 `enableHlkBackend` 和 `disableHlkBackend` 方法,允许在不改变外部 API 的前提下,将人脸识别的底层实现从虹软 SDK 切换到 HLK-223 模组。
- `verifyFaceArcSoft` 方法在HLK模式下,由于模组协议不支持图片对图片的比对,会安全降级(Fallback)到使用本地的虹软引擎进行计算,保证了接口行为的稳定。
- 新增 `extractFeatureFromBase64` 工具方法,用于从Base64图片中提取虹软的人脸特征。
- `iscs_lock/ISCSApplication`: 在应用启动时,增加对HLK-223模组的初始化,并将其设置为 `ArcSoftUtil` 的后端。

feat(硬件):
- 新增 `HLK-223` 人脸模组驱动:
- `Hlk223Config`: 提供HLK-223模组的Modbus串口配置。
- `Hlk223Frames`: 封装了HLK模组的帧协议打包和解析逻辑。
- `Hlk223Client`: 提供了对HLK模组常用命令的高层封装,如复位、获取设备信息、删除用户、获取用户信息、交互式录入、单次录入、1:N验证等。
- `Hlk223PhotoEnroll`: 实现了通过分包下发JPG图片或人脸特征来进行用户注册的完整流程。
- `FingerprintCaptureService`:
- 新增 `getCharOnce` 方法,用于从指纹模块读取已生成的指纹特征模板。
- 优化了指纹图像上传 `upImageOnce` 的分包接收逻辑。

refactor(指纹):
- `BiometricVerifier`:
- 重构 `verifyFingerprint` 方法,增加 `verifyFingerprintWithDpi` 重载,以支持对不同 DPI 候选集(如500, 508)进行并行模板构建和组合匹配,找出最佳匹配分数,提高了对不同来源指纹图像的识别准确率。
- `FingerprintCaptureService`: 优化了图像采集流程,在成功获取指纹图像后,将其保存到 `/sdcard/iscs/logs/3.txt` 以便于调试。

chore(日志):
- `CanReadyPlugin`: 移除在轮询时打印CAN节点二进制状态的调试日志。

周文健 3 hafta önce
ebeveyn
işleme
c358bb631f
27 değiştirilmiş dosya ile 471 ekleme ve 113 silme
  1. 1 1
      data/src/main/java/com/grkj/data/domain/logic/impl/UserLogic.kt
  2. 102 0
      data/src/main/java/com/grkj/data/hardware/BiometricVerifier.kt
  3. 0 12
      data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt
  4. 63 5
      data/src/main/java/com/grkj/data/hardware/face/ArcSoftUtil.kt
  5. 127 2
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt
  6. 31 0
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Config.kt
  7. 2 2
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Frames.kt
  8. 20 18
      data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt
  9. 96 6
      data/src/main/java/com/grkj/data/hardware/fingerprint/FingerprintCaptureService.kt
  10. 9 1
      iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt
  11. 3 3
      iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt
  12. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/login/viewmodel/LoginViewModel.kt
  13. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt
  14. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt
  15. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt
  16. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt
  17. 1 1
      iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/user_info/UserInfoViewModel.kt
  18. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt
  19. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt
  20. 2 2
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt
  21. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/viewmodel/LoginViewModel.kt
  22. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/dialog/data_manage/RegisterFaceDialog.kt
  23. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFaceFragment.kt
  24. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/UserInfoFragment.kt
  25. 1 1
      iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/user_info/UserInfoViewModel.kt
  26. 1 1
      shared/build.gradle.kts
  27. 0 46
      shared/src/main/java/com/grkj/shared/utils/BiometricVerifier.kt

+ 1 - 1
data/src/main/java/com/grkj/data/domain/logic/impl/UserLogic.kt

@@ -22,7 +22,7 @@ import com.grkj.data.repository.SysMenuRepository
 import com.grkj.data.repository.UserRepository
 import com.grkj.data.repository.WorkstationRepository
 import com.grkj.shared.utils.BCryptUtils
-import com.grkj.shared.utils.BiometricVerifier
+import com.grkj.data.hardware.BiometricVerifier
 import com.grkj.shared.utils.i18n.I18nManager
 import com.sik.sikcore.data.BeanUtils
 import com.sik.sikcore.extension.exists

+ 102 - 0
data/src/main/java/com/grkj/data/hardware/BiometricVerifier.kt

@@ -0,0 +1,102 @@
+package com.grkj.data.hardware
+
+import android.util.Base64
+import com.grkj.data.hardware.face.ArcSoftUtil
+import com.machinezoo.sourceafis.FingerprintMatcher
+import com.machinezoo.sourceafis.FingerprintTemplate
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.withContext
+
+// 可选:带 DPI 细节的结果
+data class FingerprintMatchResult(
+    val score: Double,
+    val dpiA: Int,
+    val dpiB: Int
+)
+
+object BiometricVerifier {
+
+    // —— 指纹比对 ——
+    /**
+     * 原函数:多 DPI 自适应匹配,返回最佳分数。
+     * 内置 DPI 候选:300~600,步长 50。你也可以把它们提到外部配置。
+     */
+    suspend fun verifyFingerprint(b64a: String, b64b: String): Double {
+        val dpiCandidates = listOf(500, 508)
+        return verifyFingerprintWithDpi(b64a, b64b, dpiCandidates, dpiCandidates).score
+    }
+
+    /**
+     * 重载:返回最佳分数 + 最佳 DPI 组合,支持 A/B 分别传不同候选集。
+     */
+    suspend fun verifyFingerprintWithDpi(
+        b64a: String,
+        b64b: String,
+        dpiCandidatesA: List<Int> = listOf(500, 508),
+        dpiCandidatesB: List<Int> = listOf(500, 508)
+    ): FingerprintMatchResult = withContext(Dispatchers.Default) {
+        // 1) Base64 -> bytes(PNG/JPG 原始图)
+        val imgA = try {
+            Base64.decode(b64a, Base64.DEFAULT)
+        } catch (_: Throwable) {
+            ByteArray(0)
+        }
+        val imgB = try {
+            Base64.decode(b64b, Base64.DEFAULT)
+        } catch (_: Throwable) {
+            ByteArray(0)
+        }
+        if (imgA.isEmpty() || imgB.isEmpty()) return@withContext FingerprintMatchResult(0.0, -1, -1)
+
+        // 2) 预构建不同 DPI 的模板(并行)
+        val templatesA = buildTemplatesForDpi(imgA, dpiCandidatesA)
+        val templatesB = buildTemplatesForDpi(imgB, dpiCandidatesB)
+        if (templatesA.isEmpty() || templatesB.isEmpty()) {
+            return@withContext FingerprintMatchResult(0.0, -1, -1)
+        }
+
+        // 3) 全组合打分,取最大(注意:模板构建最耗时,打分相对便宜)
+        var best = FingerprintMatchResult(0.0, -1, -1)
+        for ((dpiA, tplA) in templatesA) {
+            val matcher = FingerprintMatcher(tplA) // 复用 matcher 提升吞吐
+            for ((dpiB, tplB) in templatesB) {
+                val score = matcher.match(tplB)
+                if (score > best.score) best = FingerprintMatchResult(score, dpiA, dpiB)
+            }
+        }
+        best
+    }
+
+    /** 并行生成一组 DPI 的模板;异常自动跳过。 */
+    private suspend fun buildTemplatesForDpi(
+        imageBytes: ByteArray,
+        dpiList: List<Int>
+    ): Map<Int, FingerprintTemplate> = coroutineScope {
+        dpiList.map { dpi ->
+            async {
+                try {
+                    dpi to FingerprintTemplate()
+                        .dpi(dpi.toDouble())
+                        .create(imageBytes)
+                } catch (_: Throwable) {
+                    null // 某些 DPI 下可能失败,跳过
+                }
+            }
+        }.mapNotNull { it.await() }.toMap()
+    }
+
+    /**
+     * 比对两张 Base64 人脸,返回是否同人
+     * @param threshold 阈值,一般设置 0.7f 左右
+     */
+    fun verifyFaceArcSoft(
+        b64a: String,
+        b64b: String,
+        threshold: Float = 0.7f
+    ): Boolean {
+        // compareResult.score 在 [0,1],越大越像
+        return ArcSoftUtil.verifyFaceArcSoft(b64a, b64b, threshold)
+    }
+}

+ 0 - 12
data/src/main/java/com/grkj/data/hardware/can/CanReadyPlugin.kt

@@ -100,12 +100,6 @@ class CanReadyPlugin : CommPlugin {
                                     rd.payload
                                 )
                             }
-                            logger.debug(
-                                "硬件状态:{},{},{}",
-                                rd.index,
-                                rd.payload.toBinaryString(),
-                                getDeviceByDeviceType(CanHelper.getDeviceTypeByNodeId(nodeId))
-                            )
                         }
                     }
                     safeRead(cmds.readControlReg())?.let { rd ->
@@ -116,12 +110,6 @@ class CanReadyPlugin : CommPlugin {
                                 rd.payload
                             )
                         }
-                        logger.debug(
-                            "硬件状态:{},{},{}",
-                            rd.index,
-                            rd.payload.toBinaryString(),
-                            getDeviceByDeviceType(CanHelper.getDeviceTypeByNodeId(nodeId))
-                        )
                     }
 
                 } catch (t: Throwable) {

+ 63 - 5
shared/src/main/java/com/grkj/shared/utils/ArcSoftUtil.kt → data/src/main/java/com/grkj/data/hardware/face/ArcSoftUtil.kt

@@ -1,5 +1,6 @@
-package com.grkj.shared.utils
+package com.grkj.data.hardware.face
 
+// 新增:协程(你文件里用到了 CoroutineScope/Dispatchers/SupervisorJob)
 import android.Manifest
 import android.content.Context
 import android.graphics.Bitmap
@@ -21,12 +22,13 @@ import com.arcsoft.face.LivenessInfo
 import com.arcsoft.face.enums.DetectFaceOrientPriority
 import com.arcsoft.face.enums.DetectMode
 import com.arcsoft.face.enums.ExtractType
+import com.grkj.data.hardware.face.hlk.Hlk223Client
 import com.grkj.shared.config.Constants
+import com.grkj.shared.utils.DisplayUtils
 import com.grkj.shared.utils.extension.isInCenterArea
 import com.grkj.shared.utils.face.arcsoft.CameraHelper
 import com.grkj.shared.utils.face.arcsoft.CameraListener
 import com.grkj.shared.widget.FaceOverlayView
-import com.sik.sikcore.SIKCore
 import com.sik.sikimage.ImageConvertUtils
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -39,11 +41,35 @@ import java.io.File
 /**
  * ArcSoft 人脸:老工具类(兼容新实现)
  * - 保留所有旧方法签名
- * - 新增:在线/离线激活 + 本地配置文件 / 未检测到人脸节流提示 / stop() 释放引擎
+ * - 新增:后端切换(HLK 模组)但不改外部 API;HLK 不支持“图对图”比对时降级到本地算分
  */
 object ArcSoftUtil {
     private val logger: Logger = LoggerFactory.getLogger(ArcSoftUtil::class.java)
 
+    // == 新增:后端切换(默认 ArcSoft) ==
+    private enum class FaceBackend { ARC, HLK }
+
+    @Volatile
+    private var backend: FaceBackend = FaceBackend.ARC
+    @Volatile
+    private var hlkClient: Hlk223Client? = null
+
+    /** 开启 HLK 后端(不改对外接口,仅改变内部实现策略) */
+    @JvmStatic
+    fun enableHlkBackend(client: Hlk223Client) {
+        hlkClient = client
+        backend = FaceBackend.HLK
+        logger.info("ArcSoftUtil backend -> HLK")
+    }
+
+    /** 关闭 HLK 后端,回到 ArcSoft */
+    @JvmStatic
+    fun disableHlkBackend() {
+        backend = FaceBackend.ARC
+        hlkClient = null
+        logger.info("ArcSoftUtil backend -> ARC")
+    }
+
     // ---- 兼容新实现:本地配置 ----
     private const val CONFIG_DIR = "arcsoft_config"
     private const val CONFIG_FILE = "ArcSoftConfig.json"
@@ -400,8 +426,10 @@ object ArcSoftUtil {
     fun stop() {
         cameraHelper?.release()
         cameraHelper = null
-        // 新:对齐“新的”实现,释放引擎
-//        unInitEngine()
+        // 与新策略对齐:退出时回落到本地 ARC,避免悬挂的 HLK 引用
+//        disableHlkBackend()
+        // 如需同时释放引擎,可解开下行:
+        // unInitEngine()
     }
 
     // ----------------- 下方原有注册/比对逻辑保持不变 -----------------
@@ -452,9 +480,19 @@ object ArcSoftUtil {
     /**
      * 比对两张 Base64 人脸,返回是否同人
      * @param threshold 阈值,一般设置 0.7f 左右
+     *
+     * 说明:
+     * - HLK 模式下协议不支持“图对图”直接比对,这里**降级为本地 ArcSoft 算分**以保证行为稳定、签名不变。
+     * - 若后续要用 HLK 做“设备端验证”,请在业务层走摄像头流并调用 HLKClient.verify()。
      */
     fun verifyFaceArcSoft(b64a: String, b64b: String, threshold: Float = 0.7f): Boolean {
         if (b64a.isEmpty() || b64b.isEmpty()) return false
+
+        // 当选择 HLK 后端:协议上无“图片RPC比对”,安全降级为本地算分
+        if (backend == FaceBackend.HLK) {
+            logger.info("verifyFaceArcSoft: HLK backend active -> fallback to local ARC scoring")
+        }
+
         val bmpA = decodeBase64ToBitmap(b64a)
         val bmpB = decodeBase64ToBitmap(b64b)
 
@@ -494,6 +532,26 @@ object ArcSoftUtil {
         return compareResult.score >= threshold
     }
 
+    // —— 可复用的小工具:提取 2048B 特征(后续若要下发到 HLK,可直接拿这个结果) ——
+    private fun extractFeatureFromBase64(b64: String): ByteArray? {
+        return try {
+            val bmp = decodeBase64ToBitmap(b64)
+            val img = bitmapToBgr24(bmp)
+            val faces = mutableListOf<FaceInfo>()
+            val fe = faceEngine ?: return null
+            val code = fe.detectFaces(img, bmp.width, bmp.height, FaceEngine.CP_PAF_BGR24, faces)
+            if (code != ErrorInfo.MOK || faces.isEmpty()) return null
+            val feature = FaceFeature()
+            val ex = fe.extractFaceFeature(
+                img, bmp.width, bmp.height, FaceEngine.CP_PAF_BGR24,
+                faces[0], ExtractType.REGISTER, 0, feature
+            )
+            if (ex != ErrorInfo.MOK) null else feature.featureData
+        } catch (_: Throwable) {
+            null
+        }
+    }
+
     private fun decodeBase64ToBitmap(b64: String): Bitmap {
         val bytes = Base64.decode(b64, Base64.DEFAULT)
         return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)

+ 127 - 2
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Client.kt

@@ -2,15 +2,17 @@ package com.grkj.data.hardware.face.hlk
 
 import com.sik.comm.core.model.CommMessage
 import com.sik.comm.impl_modbus.ModbusProtocol
+import kotlin.math.min
 
 /**
- * HLK-FM223 常用命令的“人话”封装:单帧往返。
+ * HLK-FM223 常用命令的“人话”封装:单帧往返 + 少量多帧场景入口
  * 传输层复用 ModbusProtocol + PassThroughCodec(直通)。
  */
 class Hlk223Client(
     private val protocol: ModbusProtocol,
     private val deviceId: String
 ) {
+
     /** 基础收发:req -> rsp(payload 为完整 HLK 帧),再本地解析 */
     private suspend fun exchange(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = 3000): Pair<Int, ByteArray> {
         val req: CommMessage = Hlk223.msg(mid, data, timeoutMs)
@@ -18,6 +20,8 @@ class Hlk223Client(
         return Hlk223.parse(rsp.payload)
     }
 
+    // --- 基础信息 ---
+
     suspend fun reset() {
         val (mid, data) = exchange(Hlk223.MID.RESET)
         require(mid == Hlk223.MID.REPLY && data.size >= 2)
@@ -43,5 +47,126 @@ class Hlk223Client(
         return data.copyOfRange(2, data.size).toString(Charsets.US_ASCII).trim('\u0000')
     }
 
-    // TODO: 可继续补充 VERIFY/ENROLL/DEL/GET_USER_INFO... 的高层封装
+    // --- 用户管理 ---
+
+    suspend fun delUser(userId: Int) {
+        val data = byteArrayOf(((userId ushr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
+        val (mid, rsp) = exchange(Hlk223.MID.DEL_USER, data)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "DEL_USER failed" }
+    }
+
+    suspend fun delAll() {
+        val (mid, rsp) = exchange(Hlk223.MID.DEL_ALL)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "DEL_ALL failed" }
+    }
+
+    data class UserInfo(val userId: Int, val isAdmin: Boolean, val name: String)
+
+    suspend fun getUserInfo(userId: Int): UserInfo {
+        val data = byteArrayOf(((userId ushr 8) and 0xFF).toByte(), (userId and 0xFF).toByte())
+        val (mid, rsp) = exchange(Hlk223.MID.GET_USER_INFO, data)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "GET_USER_INFO failed" }
+        // 具体字段偏移视固件版本略有差异,下方对齐常见布局:flags(1) + name(N)...
+        val flags = if (rsp.size > 2) (rsp[2].toInt() and 0xFF) else 0
+        val isAdmin = (flags and 0x01) == 1
+        val name = if (rsp.size > 3) rsp.copyOfRange(3, rsp.size).toString(Charsets.UTF_8).trim('\u0000') else ""
+        return UserInfo(userId, isAdmin, name)
+    }
+
+    suspend fun getAllUserId(max: Int = 256): List<Int> {
+        val (mid, rsp) = exchange(Hlk223.MID.GET_ALL_USERID)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "GET_ALL_USERID failed" }
+        // 紧随其后的为一串 userId(2B,BE);不同版本有 NOTE/分页,这里做最小实现:一次吃完
+        val list = mutableListOf<Int>()
+        var i = 2
+        while (i + 1 < rsp.size && list.size < max) {
+            val id = ((rsp[i].toInt() and 0xFF) shl 8) or (rsp[i + 1].toInt() and 0xFF)
+            list += id
+            i += 2
+        }
+        return list
+    }
+
+    // --- 录入/验证 ---
+
+    /** 交互式录入(五次抬头转头之类),按需监听 NOTE;这里给最小版:触发并等待 REPLY 结束码 */
+    suspend fun enrollInteractive(nameUtf8: String, admin: Boolean = false, timeoutMs: Int = 30_000): Int {
+        val nameBytes = nameUtf8.toByteArray(Charsets.UTF_8)
+        val flag = if (admin) 0x01 else 0x00
+        val data = byteArrayOf(flag.toByte()) + nameBytes
+        val (mid, rsp) = exchange(Hlk223.MID.ENROLL, data, timeoutMs)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "ENROLL failed, code=${rsp[1].toInt() and 0xFF}" }
+        // 常见布局:尾部2字节 userId(BE)
+        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
+        return uid
+    }
+
+    /** 单帧录入(只需一张正脸) */
+    suspend fun enrollSingle(nameUtf8: String, admin: Boolean = false, timeoutMs: Int = 10_000): Int {
+        val nameBytes = nameUtf8.toByteArray(Charsets.UTF_8)
+        val flag = if (admin) 0x01 else 0x00
+        val data = byteArrayOf(flag.toByte()) + nameBytes
+        val (mid, rsp) = exchange(Hlk223.MID.ENROLL_SINGLE, data, timeoutMs)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        require((rsp[1].toInt() and 0xFF) == 0) { "ENROLL_SINGLE failed, code=${rsp[1].toInt() and 0xFF}" }
+        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
+        return uid
+    }
+
+    /** 触发设备端验证(摄像头采集→库内比对),返回是否命中与命中ID(未命中为0) */
+    data class VerifyResult(val success: Boolean, val userId: Int)
+
+    suspend fun verify(timeoutMs: Int = 10_000): VerifyResult {
+        val (mid, rsp) = exchange(Hlk223.MID.VERIFY, byteArrayOf(), timeoutMs)
+        require(mid == Hlk223.MID.REPLY && rsp.size >= 2)
+        val ok = (rsp[1].toInt() and 0xFF) == 0
+        val uid = if (rsp.size >= 4) (((rsp[rsp.size - 2].toInt() and 0xFF) shl 8) or (rsp.last().toInt() and 0xFF)) else 0
+        return VerifyResult(ok && uid > 0, uid)
+    }
+
+    // --- 照片/特征下发注册 ---
+
+    /** 计算 CRC32(标准,多项式 0xEDB88320,初始化 0xFFFFFFFF,输出与 HLK 文档一致,写入大端) */
+    private fun crc32(bytes: ByteArray): Int {
+        var crc = -1 // 0xFFFFFFFF
+        for (b in bytes) {
+            var c = (crc xor (b.toInt() and 0xFF)) and 0xFFFFFFFF.toInt()
+            repeat(8) {
+                val mask = -(c and 1)
+                c = (c ushr 1) xor (0xEDB88320.toInt() and mask)
+            }
+            crc = c
+        }
+        return crc
+    }
+
+    /**
+     * 通过照片/特征(BioType=0/1/2)执行 ENROLL_WITH_PHOTO 流。
+     * - BioType=2:payload 必须是 2052B(前 2048 特征 + 后 4B CRC32 大端);如果传入 2048B,会自动拼 CRC。
+     * - 返回 userId(>0)
+     */
+    suspend fun enrollWithPhotoOrFeature(payload: ByteArray, bioType: Int): Int {
+        val tx = Hlk223PhotoEnroll(protocol, deviceId)
+        val data = if (bioType == 2 && payload.size == 2048) {
+            val crc = crc32(payload)
+            payload + byteArrayOf(
+                ((crc ushr 24) and 0xFF).toByte(),
+                ((crc ushr 16) and 0xFF).toByte(),
+                ((crc ushr 8) and 0xFF).toByte(),
+                (crc and 0xFF).toByte()
+            )
+        } else payload
+        return tx.enroll(data, bioType = bioType, crc32 = if (bioType == 2) 0 /*占位,首包仍写真实CRC*/ else crc32(data))
+    }
+
+    /** 语法糖:直接下发 2048B 特征(自动拼 2052B + BioType=2) */
+    suspend fun enrollFeature(feature2048: ByteArray): Int {
+        require(feature2048.size == 2048) { "feature size must be 2048 bytes" }
+        return enrollWithPhotoOrFeature(feature2048, bioType = 2)
+    }
 }

+ 31 - 0
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223Config.kt

@@ -0,0 +1,31 @@
+package com.grkj.data.hardware.face.hlk
+
+import com.sik.comm.codec.PassThroughCodec
+import com.sik.comm.impl_modbus.ModbusConfig
+import com.sik.comm.impl_modbus.ModbusProtocol
+
+/**
+ * 人脸配置
+ */
+object Hlk223Config {
+    /**
+     * 获取协议
+     */
+    fun getProtocol(): ModbusProtocol {
+        val modbusConfig = ModbusConfig(
+            deviceId = "HLK-223",
+            portName = "/dev/ttyS2",
+            baudRate = 115200,
+            dataBits = 8,
+            stopBits = 1,
+            parity = 'N',
+            defaultUnitId = null,
+            codec = PassThroughCodec(),
+            requestTimeoutMs = 4000,
+            responseGapMs = 30
+        )
+        val protocol = ModbusProtocol()
+        protocol.registerConfig(modbusConfig)
+        return protocol
+    }
+}

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

@@ -65,9 +65,9 @@ object Hlk223 {
     fun msg(mid: Int, data: ByteArray = byteArrayOf(), timeoutMs: Int? = null, gapMs: Int? = null): CommMessage {
         val frame = build(mid, data)
         val meta = buildMap<String, Any> {
-            timeoutMs?.let { put("timeoutMs", it) }   // ModbusProtocol 会读取并覆写本次等待
+            timeoutMs?.let { put("timeoutMs", it) }
             gapMs?.let     { put("silenceGapMs", it) }
-            put("rawFrame", frame)                    // 方便日志/调试
+            put("rawFrame", frame)
         }
         return CommMessage(
             command = "HLK_MID_${mid.toString(16).uppercase().padStart(2, '0')}",

+ 20 - 18
data/src/main/java/com/grkj/data/hardware/face/hlk/Hlk223PhotoEnroll.kt

@@ -9,9 +9,10 @@ import com.sik.comm.impl_modbus.ModbusProtocol
 import kotlin.math.min
 
 /**
- * HLK 照片下发注册(分包,步步等待 REPLY)。
- * - 首包: Seq=0, data = len(4,BE) + bioType(1) + crc32(4,BE)
- * - 后续: Seq=1..N, 每包 <= 246B
+ * HLK 照片/特征下发注册(分包,步步等待 REPLY)。
+ * - 首包(Seq=0): 预留(1) + len(4,BE) + bioType(1: 0=JPG,1=加密,2=特征) + crc32(4,BE)
+ * - 后续(Seq=1..N): 每包 <= 246B(文档 MTU)
+ * - 末包 REPLY 一般会带 userId(>0)【固件版本字段偏移可能略有不同】。
  */
 class Hlk223PhotoEnroll(
     private val protocol: ModbusProtocol,
@@ -28,6 +29,7 @@ class Hlk223PhotoEnroll(
         return out
     }
 
+    private fun be16(v: Int) = byteArrayOf(((v ushr 8) and 0xFF).toByte(), (v and 0xFF).toByte())
     private fun be32(v: Int) = byteArrayOf(
         ((v ushr 24) and 0xFF).toByte(),
         ((v ushr 16) and 0xFF).toByte(),
@@ -35,20 +37,20 @@ class Hlk223PhotoEnroll(
         (v and 0xFF).toByte()
     )
 
-    private fun firstData(photoLen: Int, bioType: Int, crc32: Int): ByteArray {
-        // 文档对首字节是否保留位各版本略有差异,如需对齐可在此调整
-        return byteArrayOf(0x00) + be32(photoLen) + byteArrayOf(bioType.toByte()) + be32(crc32)
+    private fun firstData(totalLen: Int, bioType: Int, crc32: Int): ByteArray {
+        // 不同版本文档对首字节有“保留位”描述,这里置 0 兼容(与你已有实现保持一致)
+        return byteArrayOf(0x00) + be32(totalLen) + byteArrayOf(bioType.toByte()) + be32(crc32)
     }
 
-    private fun buildPlan(photo: ByteArray, bioType: Int, crc32: Int): TxPlan {
+    private fun buildPlan(payload: ByteArray, bioType: Int, crc32: Int): TxPlan {
         val frames = mutableListOf<CommMessage>()
         // Seq=0
-        frames += Hlk223.msg(Hlk223.MID.ENROLL_WITH_PHOTO, firstData(photo.size, bioType, crc32))
+        frames += Hlk223.msg(Hlk223.MID.ENROLL_WITH_PHOTO, firstData(payload.size, bioType, crc32))
         // Seq=1..N
-        val parts = chunk(photo)
-        parts.forEachIndexed { i, part ->
-            val seq = i + 1
-            val seqBytes = byteArrayOf(((seq ushr 8) and 0xFF).toByte(), (seq and 0xFF).toByte())
+        val parts = chunk(payload)
+        parts.forEachIndexed { idx, part ->
+            val seq = idx + 1
+            val seqBytes = be16(seq)
             frames += Hlk223.msg(Hlk223.MID.ENROLL_WITH_PHOTO, seqBytes + part)
         }
         return TxPlan(frames = frames)
@@ -56,6 +58,7 @@ class Hlk223PhotoEnroll(
 
     private fun policy(): ChainPolicy = object : ChainPolicy {
         override suspend fun afterSendStep(stepIndex: Int, sent: CommMessage, io: LinkIO): ChainStepResult {
+            // 每发一包就等一次 REPLY(如需处理 NOTE,可在此多读一次 NOTE)
             val ack = io.readRaw(timeoutMs = 3000, expectedSize = null, silenceGapMs = 30)
             val (mid, data) = Hlk223.parse(ack.payload)
             require(mid == Hlk223.MID.REPLY && data.size >= 2) { "Expect REPLY" }
@@ -63,18 +66,17 @@ class Hlk223PhotoEnroll(
             val result = data[1].toInt() and 0xFF
             require(reqMid == Hlk223.MID.ENROLL_WITH_PHOTO) { "Ack MID mismatch" }
             require(result == 0) { "Step=$stepIndex failed, result=$result" }
-            // 如需处理 NOTE,可在这里循环 readRaw 两次:先 NOTE 再 REPLY。
             return ChainStepResult(received = listOf(ack), continueNext = true, interFrameDelayMs = 10)
         }
     }
 
-    /** 执行注册,返回 userId(末包 REPLY 携带) */
-    suspend fun enroll(photoJpg: ByteArray, bioType: Int = 1, crc32: Int): Int {
-        val rsps = protocol.sendChain(deviceId, buildPlan(photoJpg, bioType, crc32), policy())
+    /** 执行注册,返回 userId(末包 REPLY 通常携带) */
+    suspend fun enroll(payload: ByteArray, bioType: Int = 1, crc32: Int): Int {
+        val rsps = protocol.sendChain(deviceId, buildPlan(payload, bioType, crc32), policy())
         val last = rsps.lastOrNull() ?: error("No reply")
         val (_, data) = Hlk223.parse(last.payload)
-        // 常见格式:REPLY data 尾部 2 字节为 userId(BE),若文档版本不同请对齐偏移
-        val uid = if (data.size >= 2) {
+        // 常见格式:REPLY data 尾部 2 字节为 userId(BE),若固件不同需对齐偏移
+        val uid = if (data.size >= 4) {
             val hi = data[data.size - 2].toInt() and 0xFF
             val lo = data[data.size - 1].toInt() and 0xFF
             (hi shl 8) or lo

+ 96 - 6
data/src/main/java/com/grkj/data/hardware/fingerprint/FingerprintCaptureService.kt

@@ -2,18 +2,27 @@ package com.grkj.data.hardware.fingerprint
 
 import android.util.Base64
 import com.grkj.shared.utils.extension.toHexStrings
-import org.slf4j.LoggerFactory
+import com.machinezoo.sourceafis.FingerprintTemplate
 import com.sik.comm.codec.PassThroughCodec
-import com.sik.comm.impl_modbus.ModbusConfig
-import com.sik.comm.impl_modbus.ModbusProtocol
+import com.sik.comm.core.model.ChainStepResult
 import com.sik.comm.core.model.CommMessage
 import com.sik.comm.core.model.TxPlan
-import com.sik.comm.core.model.ChainStepResult
 import com.sik.comm.core.policy.ChainPolicy
 import com.sik.comm.core.protocol.LinkIO
-import kotlinx.coroutines.*
+import com.sik.comm.impl_modbus.ModbusConfig
+import com.sik.comm.impl_modbus.ModbusProtocol
+import com.sik.sikcore.extension.toJson
+import com.sik.sikcore.shell.ShellUtils
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancelChildren
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import org.slf4j.LoggerFactory
 import java.util.concurrent.atomic.AtomicBoolean
-import kotlin.math.abs
 
 object FingerprintCaptureService {
     private val log = LoggerFactory.getLogger("FPM.Capture")
@@ -36,6 +45,50 @@ object FingerprintCaptureService {
     private const val REG_BAUD_RATE: Int = 0x04    // 4: 波特率控制 N (波特率 = N*9600, 1..12)
     private const val REG_PKT_SIZE: Int = 0x06    // 6: 包大小 0:32 / 1:64 / 2:128 / 3:256
 
+
+    private val chunks = ArrayList<ByteArray>(256)
+    private var isLast = false
+    private val policy = object : ChainPolicy {
+        override suspend fun afterSendStep(
+            stepIndex: Int,
+            sent: CommMessage,
+            io: LinkIO
+        ): ChainStepResult {
+            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)
+                } 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 -> { /* 可读确认码;不强制 */
+                        }
+
+                        FpmPackets.PID_DATA -> {
+                            chunks += FpmPackets.parseDataLike(f)
+                        }
+
+                        FpmPackets.PID_LAST -> {
+                            chunks += FpmPackets.parseDataLike(f)
+                            isLast = true
+                            return ChainStepResult(emptyList(), false)
+                        }
+                    }
+                }
+            }
+            if (!isLast) error("UP_IMAGE not finished (no LAST) within deadline")
+            return ChainStepResult(emptyList(), false)
+        }
+    }
+
     // 写“1字节内容”的挂起版本(PS_WriteReg: [INS][reg(1)][value(1)])
     private suspend fun writeReg8Suspend(
         proto: ModbusProtocol,
@@ -259,6 +312,7 @@ object FingerprintCaptureService {
                             // —— 自适应解码:半字节顺序 + 分辨率 —— //
                             val (gray8, w, h) = decodeAuto(raw4b)
                             val b64 = gray8ToPngBase64(gray8, w, h)
+                            ShellUtils.execCmd("echo ${b64} > /sdcard/iscs/logs/3.txt")
                             try {
                                 withContext(Dispatchers.Main) { onImage(b64) }
                             } catch (_: Throwable) {
@@ -291,6 +345,15 @@ object FingerprintCaptureService {
         return ack
     }
 
+    private suspend fun genChar(proto: ModbusProtocol): Int {
+        val frame = FpmPackets.cmd(address, FpmIns.GEN_CHAR, byteArrayOf(FpmIns.CHAR_BUFFER_ID))
+        val msg: CommMessage =
+            FpmPackets.toMessage("FPM_GEN_CHAR", frame, timeoutMs = 3000, gapMs = 30)
+        val rsp: CommMessage = proto.send(deviceId, msg)
+        val (ack, _) = FpmPackets.parseAck(rsp.payload, address)
+        return ack
+    }
+
     /** 发 UP_IMAGE,一直读到 PID=0x08(LAST)为止 */
     private suspend fun upImageOnce(proto: ModbusProtocol): ByteArray {
         framer.feed(ByteArray(0)) // 确保内部状态干净(可选)
@@ -357,6 +420,30 @@ object FingerprintCaptureService {
         return out
     }
 
+    /** 发 GET_CHAR,一直读到 PID=0x08(LAST)为止 */
+    private suspend fun getCharOnce(proto: ModbusProtocol): ByteArray {
+        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
+                )
+            )
+        )
+        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
+    }
+
     // === 解码自适应 ===
 
     private enum class NibbleOrder { HI_LO, LO_HI }
@@ -479,4 +566,7 @@ object FingerprintCaptureService {
 object FpmIns {
     const val GET_IMAGE: Byte = 0x01
     const val UP_IMAGE: Byte = 0x0A
+    const val GEN_CHAR: Byte = 0x02
+    const val CHAR_BUFFER_ID: Byte = 0x03
+    const val GET_CHAR: Byte = 0x08
 }

+ 9 - 1
iscs_lock/src/main/java/com/grkj/iscs/ISCSApplication.kt

@@ -20,7 +20,9 @@ import com.grkj.data.hardware.ble.BleUtil
 import com.grkj.data.hardware.modbus.ModBusController
 import com.grkj.iscs.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.hlk.Hlk223Client
+import com.grkj.data.hardware.face.hlk.Hlk223Config
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.shared.utils.i18n.LanguageCatalog
 import com.grkj.shared.utils.i18n.LanguageStore
@@ -32,6 +34,7 @@ import com.kongzue.dialogx.DialogX
 import com.scwang.smart.refresh.footer.ClassicsFooter
 import com.scwang.smart.refresh.header.ClassicsHeader
 import com.scwang.smart.refresh.layout.SmartRefreshLayout
+import com.sik.comm.impl_modbus.ModbusProtocol
 import com.sik.sikcore.SIKCore
 import com.sik.sikcore.crash.GlobalCrashCatch
 import com.sik.sikcore.log.LogUtils
@@ -92,6 +95,11 @@ class ISCSApplication : Application() {
         //todo 模拟器不支持 测试用,直接创建管理员账号
         ArcSoftUtil.checkActiveStatus(SIKCore.getApplication())
         ArcSoftUtil.initEngine(SIKCore.getApplication())
+
+        // ② 建一个 HLK 客户端(串口或你现有的 Modbus 封装)
+        val hlk = Hlk223Client(Hlk223Config.getProtocol(), "HLK-223")
+        // ③ 想切到 HLK 路线(但保持对外 API 不变)
+        ArcSoftUtil.enableHlkBackend(hlk)
         AutoSizeConfig.getInstance().isCustomFragment = false
         StateConfig.emptyLayout = com.grkj.ui_base.R.layout.layout_empty
         ThreadUtils.runOnIO {

+ 3 - 3
iscs_lock/src/main/java/com/grkj/iscs/features/login/dialog/LoginDialog.kt

@@ -7,8 +7,8 @@ import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogLoginBinding
 import com.grkj.iscs.features.login.viewmodel.LoginViewModel
-import com.grkj.shared.utils.ArcSoftUtil
-import com.grkj.shared.utils.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.data.utils.event.LoadingEvent
@@ -180,7 +180,7 @@ class LoginDialog(
 
 
     private fun startFace() {
-        ArcSoftUtil.inDetecting = false
+        inDetecting = false
         ActivityTracker.getCurrentActivity()?.let { context ->
             ArcSoftUtil.checkCamera(
                 context.windowManager,

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/login/viewmodel/LoginViewModel.kt

@@ -8,7 +8,7 @@ import com.grkj.data.domain.logic.IRoleLogic
 import com.grkj.data.domain.logic.IUserLogic
 import com.grkj.data.domain.logic.IWorkflowLogic
 import com.grkj.shared.config.AESConfig
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.sik.sikcore.extension.file
 import com.sik.sikcore.extension.toJson

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/CheckFaceDialog.kt

@@ -7,7 +7,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogCheckFaceBinding
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -9,7 +9,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.DialogRegisterFaceBinding
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.CustomDialog

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/SetFaceFragment.kt

@@ -11,7 +11,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentSetFaceBinding
 import com.grkj.iscs.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.utils.CommonUtils

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/fragment/user_info/UserInfoFragment.kt

@@ -12,7 +12,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs.R
 import com.grkj.iscs.databinding.FragmentUserInfoBinding
 import com.grkj.iscs.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog

+ 1 - 1
iscs_lock/src/main/java/com/grkj/iscs/features/main/viewmodel/user_info/UserInfoViewModel.kt

@@ -9,7 +9,7 @@ import com.grkj.data.domain.vo.FingerprintDataVo
 import com.grkj.data.domain.vo.SysBiometricDataVo
 import com.grkj.data.domain.logic.IHardwareLogic
 import com.grkj.data.domain.logic.IUserLogic
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.base.BaseViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/ISCSMCApplication.kt

@@ -24,7 +24,7 @@ import com.grkj.data.local.database.DbReadyGate
 import com.grkj.data.local.database.ISCSDatabase
 import com.grkj.iscs_mc.features.splash.activity.SplashActivity
 import com.grkj.shared.model.EventBean
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.i18n.I18nManager
 import com.grkj.shared.utils.i18n.LanguageCatalog
 import com.grkj.shared.utils.i18n.LanguageStore

+ 2 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/dialog/LoginDialog.kt

@@ -7,8 +7,8 @@ import com.grkj.data.enums.LoginResultEnum
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.DialogLoginBinding
 import com.grkj.iscs_mc.features.login.viewmodel.LoginViewModel
-import com.grkj.shared.utils.ArcSoftUtil
-import com.grkj.shared.utils.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
 import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.data.utils.event.LoadingEvent

+ 2 - 2
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/fragment/LoginFragment.kt

@@ -38,8 +38,8 @@ import com.grkj.ui_base.skin.loadSkinIcon
 import com.grkj.ui_base.utils.CommonUtils
 import com.grkj.ui_base.utils.changeBgTint
 import com.grkj.data.utils.event.LoadingEvent
-import com.grkj.shared.utils.ArcSoftUtil
-import com.grkj.shared.utils.ArcSoftUtil.inDetecting
+import com.grkj.data.hardware.face.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil.inDetecting
 import com.grkj.ui_base.utils.event.RFIDCardReadEvent
 import com.grkj.ui_base.utils.extension.getAppVersionName
 import com.grkj.ui_base.utils.extension.serialNo

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/login/viewmodel/LoginViewModel.kt

@@ -6,7 +6,7 @@ import com.grkj.data.domain.logic.IUserLogic
 import com.grkj.data.enums.LoginResultEnum
 import com.grkj.data.hardware.can.CanCommands
 import com.grkj.data.hardware.can.CanHelper
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.ui_base.base.BaseViewModel
 import com.sik.sikcore.extension.file
 import dagger.hilt.android.lifecycle.HiltViewModel

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/dialog/data_manage/RegisterFaceDialog.kt

@@ -9,7 +9,7 @@ import com.grkj.data.data.MainDomainData
 import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.DialogRegisterFaceBinding
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.utils.CommonUtils
 import com.kongzue.dialogx.dialogs.CustomDialog

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/SetFaceFragment.kt

@@ -11,7 +11,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentSetFaceBinding
 import com.grkj.iscs_mc.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.utils.CommonUtils

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/fragment/user_info/UserInfoFragment.kt

@@ -12,7 +12,7 @@ import com.grkj.data.utils.FileStorageUtils
 import com.grkj.iscs_mc.R
 import com.grkj.iscs_mc.databinding.FragmentUserInfoBinding
 import com.grkj.iscs_mc.features.main.viewmodel.user_info.UserInfoViewModel
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.CancellableTimer
 import com.grkj.ui_base.base.BaseFragment
 import com.grkj.ui_base.dialog.TipDialog

+ 1 - 1
iscs_mc/src/main/java/com/grkj/iscs_mc/features/main/viewmodel/user_info/UserInfoViewModel.kt

@@ -9,7 +9,7 @@ import com.grkj.data.domain.vo.FingerprintDataVo
 import com.grkj.data.domain.vo.SysBiometricDataVo
 import com.grkj.data.local.dos.IsJobCard
 import com.grkj.data.local.dos.SysUserCharacteristicDo
-import com.grkj.shared.utils.ArcSoftUtil
+import com.grkj.data.hardware.face.ArcSoftUtil
 import com.grkj.shared.utils.BCryptUtils
 import com.grkj.ui_base.base.BaseViewModel
 import dagger.hilt.android.lifecycle.HiltViewModel

+ 1 - 1
shared/build.gradle.kts

@@ -59,7 +59,7 @@ dependencies {
     api(libs.sik.extension.android)
     api(libs.sik.comm)
     api("org.mindrot:jbcrypt:0.4")
-    implementation("com.machinezoo.sourceafis:sourceafis:3.15.0")
+    api("com.machinezoo.sourceafis:sourceafis:3.15.0")
     implementation("com.google.dagger:hilt-android:2.56.2")
     ksp("com.google.dagger:hilt-android-compiler:2.56.2")
     api("com.github.SilverIceKey:SIKCronJob:1.0.5")

+ 0 - 46
shared/src/main/java/com/grkj/shared/utils/BiometricVerifier.kt

@@ -1,46 +0,0 @@
-package com.grkj.shared.utils
-
-import android.util.Base64
-import com.machinezoo.sourceafis.FingerprintMatcher
-import com.machinezoo.sourceafis.FingerprintTemplate
-import com.zkteco.zkfinger.FingerprintService
-
-object BiometricVerifier {
-
-    // —— 指纹比对 ——
-    /**
-     * @param b64a 指纹1 的 Base64 图像(灰度或彩色均可)
-     * @param b64b 指纹2 的 Base64 图像
-     * @return 两者是否认为同一人(score >= threshold)
-     */
-    suspend fun verifyFingerprint(b64a: String, b64b: String): Double {
-        // 1. 解码成 Bitmap
-        val bmpA = Base64.decode(b64a, Base64.DEFAULT)
-        val bmpB = Base64.decode(b64b, Base64.DEFAULT)
-
-        // 2. 构建指纹模板
-        val templateA = FingerprintTemplate()
-            .dpi(500.0) // 根据图像 DPI 调整
-            .create(bmpA)
-        val templateB = FingerprintTemplate()
-            .dpi(500.0)
-            .create(bmpB)
-
-        // 3. 比对
-        val score = FingerprintMatcher(templateA).match(templateB)
-        return score
-    }
-
-    /**
-     * 比对两张 Base64 人脸,返回是否同人
-     * @param threshold 阈值,一般设置 0.7f 左右
-     */
-    fun verifyFaceArcSoft(
-        b64a: String,
-        b64b: String,
-        threshold: Float = 0.7f
-    ): Boolean {
-        // compareResult.score 在 [0,1],越大越像
-        return ArcSoftUtil.verifyFaceArcSoft(b64a, b64b, threshold)
-    }
-}