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