1355 lines
54 KiB
C#
1355 lines
54 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Text;
|
||
using Aisu.SIM1.Face.Models;
|
||
using Aisu.SIM1.Face.Protocol;
|
||
using UnityEngine;
|
||
|
||
namespace Aisu.SIM1.Face.Core
|
||
{
|
||
/// <summary>
|
||
/// FaceModule 键盘功能测试脚本,用于不依赖 UI 面板快速验证串口与业务接口。
|
||
/// </summary>
|
||
public sealed class FaceModuleKeyboardTester : MonoBehaviour
|
||
{
|
||
[Header("Manager 引用")]
|
||
// 场景中的 FaceModuleManager,不填时自动查找或创建。
|
||
public FaceModuleManager Manager;
|
||
|
||
[Header("串口参数(A/Q/W)")]
|
||
// 是否使用 Mock 模组。
|
||
public bool UseMock;
|
||
// 串口名称;示例 COM3,按当前设备实际串口修改。
|
||
public string PortName = "COM3";
|
||
// 串口波特率。
|
||
public int BaudRate = 115200;
|
||
// 打开串口后等待 READY 的超时时间。
|
||
public int ReadyTimeoutMs = 3000;
|
||
|
||
[Header("人脸检测/验证参数(S/Z/X)")]
|
||
// 验证超时时间,单位秒。
|
||
public byte VerifyTimeout = 10;
|
||
// 最大识别次数。
|
||
public byte MaxRecognitionTimes = 30;
|
||
// 是否开启自动验证。
|
||
public bool AutoVerifyEnable = true;
|
||
|
||
[Header("注册用户参数(E/R/T/F)")]
|
||
// 注册用户名。
|
||
public string EnrollUserName = "TestUser";
|
||
// 注册用户是否为管理员。
|
||
public bool EnrollAsAdmin;
|
||
// 注册方向。
|
||
public FaceDirection EnrollDirection = FaceDirection.Middle;
|
||
// 注册超时时间,单位秒。
|
||
public byte EnrollTimeout = 20;
|
||
// 集成录入是否使用单帧模式。
|
||
public bool IntegratedSingleFrame;
|
||
// 集成录入是否查重。
|
||
public bool IntegratedDuplicateCheck = true;
|
||
|
||
[Header("用户管理参数(D/M/G/H/J)")]
|
||
// 查询 / 删除使用的用户 ID。
|
||
public int UserId = 1;
|
||
// 是否使用 GET_ALL_USERID2 查询所有用户。
|
||
public bool UseGetAllUserId2;
|
||
|
||
[Header("二维码参数(C)")]
|
||
// 扫码超时时间,单位秒。
|
||
public byte QrTimeout = 10;
|
||
|
||
[Header("图片抓拍参数(V/B/N)")]
|
||
// 大图 DPI。
|
||
public ImageDpi LargeImageDpi = ImageDpi.Dpi480x640;
|
||
|
||
[Header("照片注册参数(P/O)")]
|
||
// 照片或特征文件路径。
|
||
public string PhotoRegisterFilePath;
|
||
// 照片注册 BioType。
|
||
public BioType PhotoBioType = BioType.NormalPhoto;
|
||
// 照片注册用户名。
|
||
public string PhotoUserName = "PhotoUser";
|
||
// 照片注册是否查重。
|
||
public bool PhotoDuplicateCheck = true;
|
||
// 指定 ID 照片注册使用的用户 ID。
|
||
public int PhotoUserId = 100;
|
||
|
||
[Header("特征参数(L/K)")]
|
||
// 特征类型。
|
||
public FeatureType FeatureType = FeatureType.ColorFace;
|
||
// 写入特征文件路径。
|
||
public string FeatureFilePath;
|
||
// 写入特征使用的用户名。
|
||
public string FeatureUserName = "FeatureUser";
|
||
|
||
[Header("高级参数(1/2/3)")]
|
||
// RGB 识别阈值 0-4。
|
||
[Range(0, 4)] public byte RgbLevel = 2;
|
||
// 是否启用查重。
|
||
public bool DuplicateCheckEnable = true;
|
||
// Demo Mode 开关。
|
||
public bool DemoModeEnable;
|
||
|
||
[Header("UVC 参数(U/I/Y)")]
|
||
// USB Type。
|
||
public byte UsbType = 0x20;
|
||
// UVC 码率。
|
||
public byte UvcBitrateMbps = 8;
|
||
// MJPG 质量。
|
||
[Range(10, 100)] public byte MjpgQuality = 80;
|
||
// 图像镜像。
|
||
public bool UvcMirror;
|
||
// 180 度旋转。
|
||
public bool UvcRotate180;
|
||
// 人脸框显示。
|
||
public bool FaceLocationVisible = true;
|
||
|
||
[Header("加密参数(4/5/6)")]
|
||
// Release Key,16 bytes Hex,例如:00 01 02 ...
|
||
public string ReleaseKeyHex;
|
||
// Debug Key,16 bytes Hex,例如:00 01 02 ...
|
||
public string DebugKeyHex;
|
||
// Seed,4 bytes Hex。
|
||
public string SeedHex;
|
||
// 加密模式。
|
||
public byte EncryptionMode;
|
||
|
||
[Header("固件升级参数(7)")]
|
||
// 固件升级按键说明。
|
||
public string FirmwareKey = "7 启动固件升级";
|
||
|
||
[Header("串口指令解析(Space)")]
|
||
// 待解析的 TX 串口 Hex 指令,按空格键后只解析为中文说明,不会发送到串口。
|
||
[TextArea(2, 6)] public string TxSerialCommandHex = "EF AA 10 00 00 10";
|
||
// 待解析的 RX 串口 Hex 内容,可填模组返回的 REPLY / NOTE。
|
||
[TextArea(2, 6)] public string RxSerialCommandHex = "EF AA 01 00 01 00 00";
|
||
// 兼容旧字段,留空 TX / RX 时会解析这里的内容。
|
||
[HideInInspector] public string SerialCommandHex;
|
||
|
||
private Action m_readyHandler; // READY 事件回调。
|
||
private Action<string> m_rawLogHandler; // 原始日志事件回调。
|
||
private Action<FaceResultCode, string> m_errorHandler; // 错误事件回调。
|
||
private Action<FaceFaceStateNote> m_faceStateHandler; // 人脸状态事件回调。
|
||
|
||
[Header("按键说明")]
|
||
// A 打开串口,Q 关闭串口,W 等待 READY / GET STATUS 探测。
|
||
public string ConnectionKeys = "A 打开串口,Q 关闭串口,W 等待 READY / 主动探测";
|
||
// S 检测人脸,Z 自动验证,X 停止/RESET。
|
||
public string VerifyKeys = "S 检测人脸,Z 自动验证,X RESET";
|
||
// E 单帧注册,R 交互注册,T 集成注册,F 取消注册。
|
||
public string EnrollKeys = "E 单帧注册,R 交互注册,T 集成注册,F 取消注册";
|
||
// D 查询所有用户 ID,G 查询用户,H 删除用户,J 删除全部用户。
|
||
public string UserKeys = "D 查询所有用户 ID,M 查询所有用户信息,G 查询用户,H 删除用户,J 删除全部用户";
|
||
// C 扫码,V 普通抓拍,B 人脸抓拍,N 大图抓拍。
|
||
public string MediaKeys = "C 扫码,V 普通抓拍,B 人脸抓拍,N 大图抓拍";
|
||
// P 照片注册,O 指定 ID 照片注册,L 读特征,K 写特征。
|
||
public string AdvancedKeys = "P 照片注册,O 指定 ID 照片注册,L 读特征,K 写特征";
|
||
// U 读 UVC,I 设置 UVC,Y 人脸框,数字 1/2/3 分别设置 RGB/查重/Demo。
|
||
public string SettingKeys = "U 读 UVC,I 设置 UVC,Y 人脸框,1 RGB,2 查重,3 Demo";
|
||
// 数字 4/5/6/7 分别设置 Release Key、Debug Key、Init Encryption、固件升级。
|
||
public string SystemKeys = "4 Release Key,5 Debug Key,6 初始化加密,7 固件升级,Space 解析串口指令";
|
||
|
||
#region Unity API
|
||
|
||
/// <summary>
|
||
/// 初始化测试脚本。
|
||
/// </summary>
|
||
private void Awake()
|
||
{
|
||
EnsureManager();
|
||
BindLogs();
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
UnbindLogs();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 每帧检测键盘输入并触发测试功能。
|
||
/// </summary>
|
||
private void Update()
|
||
{
|
||
if (Input.GetKeyDown(KeyCode.A)) OpenSerial();
|
||
if (Input.GetKeyDown(KeyCode.Q)) CloseSerial();
|
||
if (Input.GetKeyDown(KeyCode.W)) WaitReadyOrProbe();
|
||
|
||
if (Input.GetKeyDown(KeyCode.S)) VerifyFace();
|
||
if (Input.GetKeyDown(KeyCode.Z)) AutoVerify();
|
||
if (Input.GetKeyDown(KeyCode.X)) ResetModule();
|
||
|
||
if (Input.GetKeyDown(KeyCode.E)) EnrollSingle();
|
||
if (Input.GetKeyDown(KeyCode.R)) Enroll();
|
||
if (Input.GetKeyDown(KeyCode.T)) EnrollIntegrated();
|
||
if (Input.GetKeyDown(KeyCode.F)) FaceReset();
|
||
|
||
if (Input.GetKeyDown(KeyCode.D)) GetAllUserIds();
|
||
if (Input.GetKeyDown(KeyCode.M)) GetAllUserInfos();
|
||
if (Input.GetKeyDown(KeyCode.G)) GetUserInfo();
|
||
if (Input.GetKeyDown(KeyCode.H)) DeleteUser();
|
||
if (Input.GetKeyDown(KeyCode.J)) DeleteAllUsers();
|
||
|
||
if (Input.GetKeyDown(KeyCode.C)) ScanQrCode();
|
||
if (Input.GetKeyDown(KeyCode.V)) SnapImage();
|
||
if (Input.GetKeyDown(KeyCode.B)) SnapFaceImage();
|
||
if (Input.GetKeyDown(KeyCode.N)) SnapLargeImage();
|
||
|
||
if (Input.GetKeyDown(KeyCode.P)) EnrollWithPhoto();
|
||
if (Input.GetKeyDown(KeyCode.O)) EnrollWithPhotoAndId();
|
||
if (Input.GetKeyDown(KeyCode.L)) ReadFeature();
|
||
if (Input.GetKeyDown(KeyCode.K)) WriteFeature();
|
||
|
||
if (Input.GetKeyDown(KeyCode.U)) ReadUvc();
|
||
if (Input.GetKeyDown(KeyCode.I)) SetUvc();
|
||
if (Input.GetKeyDown(KeyCode.Y)) SetFaceLocationVisible();
|
||
if (Input.GetKeyDown(KeyCode.Alpha1)) SetRgbLevel();
|
||
if (Input.GetKeyDown(KeyCode.Alpha2)) SetDuplicateCheck();
|
||
if (Input.GetKeyDown(KeyCode.Alpha3)) SetDemoMode();
|
||
if (Input.GetKeyDown(KeyCode.Alpha4)) SetReleaseKey();
|
||
if (Input.GetKeyDown(KeyCode.Alpha5)) SetDebugKey();
|
||
if (Input.GetKeyDown(KeyCode.Alpha6)) InitEncryption();
|
||
if (Input.GetKeyDown(KeyCode.Alpha7)) UpgradeFirmware();
|
||
if (Input.GetKeyDown(KeyCode.Space)) ParseSerialCommand();
|
||
}
|
||
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 确保 Manager 存在。
|
||
/// </summary>
|
||
private void EnsureManager()
|
||
{
|
||
if (Manager != null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Manager = FindObjectOfType<FaceModuleManager>();
|
||
if (Manager == null)
|
||
{
|
||
Manager = new GameObject("FaceModuleManager").AddComponent<FaceModuleManager>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 绑定日志事件。
|
||
/// </summary>
|
||
private void BindLogs()
|
||
{
|
||
UnbindLogs();
|
||
m_readyHandler = () => Log("收到 READY,模组已就绪");
|
||
m_rawLogHandler = message => Log("原始数据:" + message);
|
||
m_errorHandler = (code, message) => LogError("错误码:" + code + ",原因:" + message);
|
||
m_faceStateHandler = state => Log("人脸状态:" + FaceProtocolEncoding.FaceStateMessage(state.State));
|
||
Manager.OnReady += m_readyHandler;
|
||
Manager.OnRawLog += m_rawLogHandler;
|
||
Manager.OnError += m_errorHandler;
|
||
Manager.OnFaceStateUpdated += m_faceStateHandler;
|
||
}
|
||
|
||
private void UnbindLogs()
|
||
{
|
||
if (Manager == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
if (m_readyHandler != null)
|
||
{
|
||
Manager.OnReady -= m_readyHandler;
|
||
}
|
||
|
||
if (m_rawLogHandler != null)
|
||
{
|
||
Manager.OnRawLog -= m_rawLogHandler;
|
||
}
|
||
|
||
if (m_errorHandler != null)
|
||
{
|
||
Manager.OnError -= m_errorHandler;
|
||
}
|
||
|
||
if (m_faceStateHandler != null)
|
||
{
|
||
Manager.OnFaceStateUpdated -= m_faceStateHandler;
|
||
}
|
||
|
||
m_readyHandler = null;
|
||
m_rawLogHandler = null;
|
||
m_errorHandler = null;
|
||
m_faceStateHandler = null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开串口。
|
||
/// </summary>
|
||
public async void OpenSerial()
|
||
{
|
||
try
|
||
{
|
||
Manager.Configure(UseMock, PortName, BaudRate);
|
||
Manager.Open();
|
||
Log("串口已打开:" + PortName + ",波特率:" + BaudRate);
|
||
await Manager.WaitReadyAsync(ReadyTimeoutMs);
|
||
Log("已收到 READY。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log("未收到 READY,开始使用 GET STATUS 主动探测。原因:" + ex.Message);
|
||
await ProbeReadyAfterOpen();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭串口。
|
||
/// </summary>
|
||
public void CloseSerial()
|
||
{
|
||
Manager.Close();
|
||
Log("串口已关闭。");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 等待 READY 或主动探测。
|
||
/// </summary>
|
||
public async void WaitReadyOrProbe()
|
||
{
|
||
try
|
||
{
|
||
await Manager.WaitReadyAsync(ReadyTimeoutMs);
|
||
Log("已收到 READY。");
|
||
}
|
||
catch
|
||
{
|
||
await ProbeReadyAfterOpen();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 使用 GET STATUS 主动探测 READY。
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task ProbeReadyAfterOpen()
|
||
{
|
||
try
|
||
{
|
||
var status = await Manager.ProbeReadyByStatusAsync();
|
||
Log("主动探测成功,当前模组状态:" + status);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogError("主动探测失败:" + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测人脸 / 普通验证。
|
||
/// </summary>
|
||
public async void VerifyFace()
|
||
{
|
||
try
|
||
{
|
||
var result = await Manager.Verify.VerifyAsync(VerifyTimeout, MaxRecognitionTimes);
|
||
Log("人脸验证完成:成功=" + result.Success + ",用户ID=" + result.UserId + ",姓名=" + result.UserName + ",管理员=" + result.IsAdmin);
|
||
}
|
||
catch (Exception ex) { LogError("人脸验证失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 自动验证。
|
||
/// </summary>
|
||
public async void AutoVerify()
|
||
{
|
||
try
|
||
{
|
||
if (AutoVerifyEnable)
|
||
{
|
||
await Manager.StartAutoVerifyAsync(VerifyTimeout);
|
||
Log("自动验证已开启,识别结果通过事件回调。");
|
||
return;
|
||
}
|
||
|
||
await Manager.StopAutoVerifyAsync(VerifyTimeout);
|
||
Log("自动验证已关闭。");
|
||
}
|
||
catch (Exception ex) { LogError("自动验证失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// RESET。
|
||
/// </summary>
|
||
public async void ResetModule()
|
||
{
|
||
try { await Manager.Basic.ResetAsync(); Log("RESET 成功。"); }
|
||
catch (Exception ex) { LogError("RESET 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// FACE RESET。
|
||
/// </summary>
|
||
public async void FaceReset()
|
||
{
|
||
try { await Manager.Basic.FaceResetAsync(); Log("FACE RESET 成功。"); }
|
||
catch (Exception ex) { LogError("FACE RESET 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单帧注册。
|
||
/// </summary>
|
||
public async void EnrollSingle()
|
||
{
|
||
try
|
||
{
|
||
var result = await Manager.Enrollment.EnrollSingleAsync(EnrollAsAdmin, EnrollUserName, EnrollTimeout);
|
||
Log("单帧注册完成:成功=" + result.Success + ",用户ID=" + result.UserId + ",方向=" + result.Direction);
|
||
await ConfirmEnrollResult(result);
|
||
}
|
||
catch (Exception ex) { LogError("单帧注册失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 交互注册。
|
||
/// </summary>
|
||
public async void Enroll()
|
||
{
|
||
try
|
||
{
|
||
var result = await Manager.Enrollment.EnrollAsync(EnrollAsAdmin, EnrollUserName, EnrollDirection, EnrollTimeout);
|
||
Log("交互注册完成:成功=" + result.Success + ",用户ID=" + result.UserId + ",方向=" + result.Direction);
|
||
await ConfirmEnrollResult(result);
|
||
}
|
||
catch (Exception ex) { LogError("交互注册失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 集成注册。
|
||
/// </summary>
|
||
public async void EnrollIntegrated()
|
||
{
|
||
try
|
||
{
|
||
var result = await Manager.Enrollment.EnrollIntegratedAsync(EnrollAsAdmin, EnrollUserName, EnrollDirection, EnrollTimeout, IntegratedSingleFrame, IntegratedDuplicateCheck);
|
||
Log("集成注册完成:成功=" + result.Success + ",用户ID=" + result.UserId + ",方向=" + result.Direction);
|
||
await ConfirmEnrollResult(result);
|
||
}
|
||
catch (Exception ex) { LogError("集成注册失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册成功后查询用户信息,确认姓名和用户 ID 已经写入模组。
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task ConfirmEnrollResult(FaceEnrollResult result)
|
||
{
|
||
if (!result.Success || result.UserId <= 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
var info = await Manager.Users.GetUserInfoAsync(result.UserId);
|
||
Log("注册确认成功:ID=" + info.UserId + ",姓名=" + info.UserName + ",管理员=" + info.IsAdmin);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogError("注册已返回成功,但查询用户确认失败:" + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询所有用户 ID。
|
||
/// </summary>
|
||
public async void GetAllUserIds()
|
||
{
|
||
try
|
||
{
|
||
var ids = UseGetAllUserId2 ? await Manager.Users.GetAllUserIds2Async() : await Manager.Users.GetAllUserIdsAsync();
|
||
Log("用户 ID 列表:" + string.Join(",", ids));
|
||
}
|
||
catch (Exception ex) { LogError("查询用户 ID 列表失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询所有用户信息。
|
||
/// </summary>
|
||
public async void GetAllUserInfos()
|
||
{
|
||
try
|
||
{
|
||
var infos = UseGetAllUserId2 ? await Manager.Users.GetAllUserInfos2Async() : await Manager.Users.GetAllUserInfosAsync();
|
||
var builder = new StringBuilder();
|
||
builder.AppendLine("用户信息列表,共 " + infos.Count + " 个:");
|
||
for (var i = 0; i < infos.Count; i++)
|
||
{
|
||
builder.AppendLine("ID=" + infos[i].UserId + ",姓名=" + infos[i].UserName + ",管理员=" + infos[i].IsAdmin);
|
||
}
|
||
|
||
Log(builder.ToString().TrimEnd());
|
||
}
|
||
catch (Exception ex) { LogError("查询所有用户信息失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询用户信息。
|
||
/// </summary>
|
||
public async void GetUserInfo()
|
||
{
|
||
try
|
||
{
|
||
var info = await Manager.Users.GetUserInfoAsync(UserId);
|
||
Log("用户信息:ID=" + info.UserId + ",姓名=" + info.UserName + ",管理员=" + info.IsAdmin);
|
||
}
|
||
catch (Exception ex) { LogError("查询用户信息失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除用户。
|
||
/// </summary>
|
||
public async void DeleteUser()
|
||
{
|
||
try { await Manager.Users.DeleteUserAsync(UserId); Log("删除用户成功,用户ID:" + UserId); }
|
||
catch (Exception ex) { LogError("删除用户失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除全部用户。
|
||
/// </summary>
|
||
public async void DeleteAllUsers()
|
||
{
|
||
try { await Manager.Users.DeleteAllAsync(); Log("删除全部用户成功。"); }
|
||
catch (Exception ex) { LogError("删除全部用户失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扫描二维码。
|
||
/// </summary>
|
||
public async void ScanQrCode()
|
||
{
|
||
try { Log("二维码内容:" + await Manager.QrCode.ScanQrCodeAsync(QrTimeout)); }
|
||
catch (Exception ex) { LogError("扫描二维码失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 抓拍普通图片。
|
||
/// </summary>
|
||
public async void SnapImage()
|
||
{
|
||
try { LogImageResult("普通图片", await Manager.Images.SnapUploadImageAsync()); }
|
||
catch (Exception ex) { LogError("抓拍普通图片失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 抓拍人脸图片。
|
||
/// </summary>
|
||
public async void SnapFaceImage()
|
||
{
|
||
try { LogImageResult("人脸图片", await Manager.Images.SnapUploadFaceImageAsync()); }
|
||
catch (Exception ex) { LogError("抓拍人脸图片失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 抓拍大图。
|
||
/// </summary>
|
||
public async void SnapLargeImage()
|
||
{
|
||
try { LogImageResult("大图", await Manager.Images.SnapUploadLargeImageAsync(LargeImageDpi)); }
|
||
catch (Exception ex) { LogError("抓拍大图失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 照片注册。
|
||
/// </summary>
|
||
public async void EnrollWithPhoto()
|
||
{
|
||
try
|
||
{
|
||
var bytes = System.IO.File.ReadAllBytes(PhotoRegisterFilePath);
|
||
var result = await Manager.Images.EnrollWithPhotoAsync(bytes, PhotoBioType, PhotoUserName, PhotoDuplicateCheck, LogProgress);
|
||
Log("照片注册完成:成功=" + result.Success + ",用户ID=" + result.UserId);
|
||
}
|
||
catch (Exception ex) { LogError("照片注册失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定 ID 照片注册。
|
||
/// </summary>
|
||
public async void EnrollWithPhotoAndId()
|
||
{
|
||
try
|
||
{
|
||
var bytes = System.IO.File.ReadAllBytes(PhotoRegisterFilePath);
|
||
var result = await Manager.Images.EnrollWithPhotoAndIdAsync(PhotoUserId, bytes, PhotoBioType, PhotoUserName, PhotoDuplicateCheck, LogProgress);
|
||
Log("指定 ID 照片注册完成:成功=" + result.Success + ",用户ID=" + result.UserId);
|
||
}
|
||
catch (Exception ex) { LogError("指定 ID 照片注册失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取特征。
|
||
/// </summary>
|
||
public async void ReadFeature()
|
||
{
|
||
try
|
||
{
|
||
var bytes = await Manager.Features.ReadFeatureAsync(UserId, FeatureType);
|
||
Log("读取特征完成,长度:" + bytes.Length + " bytes");
|
||
}
|
||
catch (Exception ex) { LogError("读取特征失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 写入特征。
|
||
/// </summary>
|
||
public async void WriteFeature()
|
||
{
|
||
try
|
||
{
|
||
var bytes = System.IO.File.ReadAllBytes(FeatureFilePath);
|
||
var result = await Manager.Features.WriteFeatureAsync(bytes, FeatureType, FeatureUserName, LogProgress);
|
||
Log("写入特征完成:成功=" + result.Success + ",用户ID=" + result.UserId);
|
||
}
|
||
catch (Exception ex) { LogError("写入特征失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 读取 UVC 参数。
|
||
/// </summary>
|
||
public async void ReadUvc()
|
||
{
|
||
try
|
||
{
|
||
var p = await Manager.Uvc.ReadUsbUvcParametersAsync();
|
||
Log("UVC 参数:USB=0x" + p.UsbType.ToString("X2") + ",码率=" + p.BitrateMbps + ",质量=" + p.MjpgQuality + ",镜像=" + p.Mirror + ",旋转180=" + p.Rotate180);
|
||
}
|
||
catch (Exception ex) { LogError("读取 UVC 参数失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 UVC 参数。
|
||
/// </summary>
|
||
public async void SetUvc()
|
||
{
|
||
try
|
||
{
|
||
await Manager.Uvc.SetUsbUvcParametersAsync(new UsbUvcParameters
|
||
{
|
||
UsbType = UsbType,
|
||
BitrateMbps = UvcBitrateMbps,
|
||
MjpgQuality = MjpgQuality,
|
||
Mirror = UvcMirror,
|
||
Rotate180 = UvcRotate180
|
||
});
|
||
Log("设置 UVC 参数成功。");
|
||
}
|
||
catch (Exception ex) { LogError("设置 UVC 参数失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置人脸框显示。
|
||
/// </summary>
|
||
public async void SetFaceLocationVisible()
|
||
{
|
||
try { await Manager.Uvc.SetFaceLocationDisplayAsync(FaceLocationVisible); Log("设置人脸框显示成功,当前值:" + FaceLocationVisible); }
|
||
catch (Exception ex) { LogError("设置人脸框显示失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 RGB_LEVEL。
|
||
/// </summary>
|
||
public async void SetRgbLevel()
|
||
{
|
||
try { await Manager.Uvc.SetRgbLevelAsync(RgbLevel); Log("设置 RGB_LEVEL 成功,当前值:" + RgbLevel); }
|
||
catch (Exception ex) { LogError("设置 RGB_LEVEL 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置查重。
|
||
/// </summary>
|
||
public async void SetDuplicateCheck()
|
||
{
|
||
try { await Manager.Uvc.SetDuplicateCheckAsync(DuplicateCheckEnable); Log("设置查重成功,启用查重:" + DuplicateCheckEnable); }
|
||
catch (Exception ex) { LogError("设置查重失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Demo Mode。
|
||
/// </summary>
|
||
public async void SetDemoMode()
|
||
{
|
||
try { await Manager.Basic.SetDemoModeAsync(DemoModeEnable); Log("设置 Demo Mode 成功,当前值:" + DemoModeEnable); }
|
||
catch (Exception ex) { LogError("设置 Demo Mode 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Release Key。
|
||
/// </summary>
|
||
public async void SetReleaseKey()
|
||
{
|
||
try { await Manager.Encryption.SetReleaseEncryptionKeyAsync(ParseHex(ReleaseKeyHex, 16)); Log("设置 Release Key 成功。"); }
|
||
catch (Exception ex) { LogError("设置 Release Key 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置 Debug Key。
|
||
/// </summary>
|
||
public async void SetDebugKey()
|
||
{
|
||
try { await Manager.Encryption.SetDebugEncryptionKeyAsync(ParseHex(DebugKeyHex, 16)); Log("设置 Debug Key 成功。"); }
|
||
catch (Exception ex) { LogError("设置 Debug Key 失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化加密。
|
||
/// </summary>
|
||
public async void InitEncryption()
|
||
{
|
||
try { await Manager.Encryption.InitEncryptionAsync(ParseHex(SeedHex, 4), EncryptionMode); Log("初始化加密成功。"); }
|
||
catch (Exception ex) { LogError("初始化加密失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动固件升级。
|
||
/// </summary>
|
||
public async void UpgradeFirmware()
|
||
{
|
||
try { await Manager.Basic.UpgradeFirmwareAsync(); Log("固件升级命令已发送。"); }
|
||
catch (Exception ex) { LogError("固件升级命令发送失败:" + ex.Message); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 Inspector 输入的串口 Hex 指令解析为中文说明。
|
||
/// </summary>
|
||
public void ParseSerialCommand()
|
||
{
|
||
try
|
||
{
|
||
var hasTx = !string.IsNullOrWhiteSpace(TxSerialCommandHex);
|
||
var hasRx = !string.IsNullOrWhiteSpace(RxSerialCommandHex);
|
||
if (!hasTx && !hasRx && !string.IsNullOrWhiteSpace(SerialCommandHex))
|
||
{
|
||
Log("兼容旧字段解析:\n" + DescribeSerialPackets(ParseHexFlexible(SerialCommandHex)));
|
||
return;
|
||
}
|
||
|
||
var builder = new StringBuilder();
|
||
if (hasTx)
|
||
{
|
||
builder.AppendLine("TX 内容解析:");
|
||
builder.AppendLine(DescribeSerialPackets(ParseHexFlexible(TxSerialCommandHex)));
|
||
}
|
||
|
||
if (hasRx)
|
||
{
|
||
if (builder.Length > 0)
|
||
{
|
||
builder.AppendLine();
|
||
}
|
||
|
||
builder.AppendLine("RX 内容解析:");
|
||
builder.AppendLine(DescribeSerialPackets(ParseHexFlexible(RxSerialCommandHex)));
|
||
}
|
||
|
||
Log(builder.Length > 0 ? builder.ToString().TrimEnd() : "请先填写 TX 或 RX 串口 Hex 内容。");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogError("串口指令解析失败:" + ex.Message);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析一段串口 bytes 中的所有完整协议包。
|
||
/// </summary>
|
||
private static string DescribeSerialPackets(byte[] bytes)
|
||
{
|
||
if (bytes == null || bytes.Length == 0)
|
||
{
|
||
return "串口指令为空。";
|
||
}
|
||
|
||
var builder = new StringBuilder();
|
||
var offset = 0;
|
||
var index = 1;
|
||
while (offset < bytes.Length)
|
||
{
|
||
if (offset + FaceModuleConstants.HeaderLength + FaceModuleConstants.ChecksumLength > bytes.Length)
|
||
{
|
||
builder.AppendLine("第 " + index + " 包:剩余字节不足一个完整包头。");
|
||
break;
|
||
}
|
||
|
||
if (bytes[offset] != FaceModuleConstants.SyncWordHigh || bytes[offset + 1] != FaceModuleConstants.SyncWordLow)
|
||
{
|
||
builder.AppendLine("第 " + index + " 包:同步头错误,应为 EF AA,当前位置为 " + ToHex(bytes, offset, Math.Min(2, bytes.Length - offset)) + "。");
|
||
break;
|
||
}
|
||
|
||
var msgId = bytes[offset + 2];
|
||
var size = (ushort)((bytes[offset + 3] << 8) | bytes[offset + 4]);
|
||
var packetLength = FaceModuleConstants.HeaderLength + size + FaceModuleConstants.ChecksumLength;
|
||
if (offset + packetLength > bytes.Length)
|
||
{
|
||
builder.AppendLine("第 " + index + " 包:声明 DATA 长度为 " + size + ",但输入字节不完整。");
|
||
break;
|
||
}
|
||
|
||
var data = new byte[size];
|
||
if (size > 0)
|
||
{
|
||
Buffer.BlockCopy(bytes, offset + FaceModuleConstants.HeaderLength, data, 0, size);
|
||
}
|
||
|
||
var checksum = bytes[offset + packetLength - 1];
|
||
var expected = FacePacketBuilder.CalculateChecksum(msgId, size, data);
|
||
builder.AppendLine("第 " + index + " 包:");
|
||
builder.AppendLine("类型:" + DescribeMessageType(msgId));
|
||
builder.AppendLine("MID/MsgID:0x" + msgId.ToString("X2"));
|
||
builder.AppendLine("DATA 长度:" + size);
|
||
builder.AppendLine("DATA:" + (data.Length > 0 ? ToHex(data, 0, data.Length) : "无"));
|
||
builder.AppendLine("校验:" + (checksum == expected ? "正确" : "错误,应为 0x" + expected.ToString("X2")));
|
||
builder.AppendLine(DescribePacketData(msgId, data));
|
||
|
||
offset += packetLength;
|
||
index++;
|
||
}
|
||
|
||
return builder.ToString().TrimEnd();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析协议包类型。
|
||
/// </summary>
|
||
private static string DescribeMessageType(byte msgId)
|
||
{
|
||
if (msgId == (byte)FaceMessageType.Reply)
|
||
{
|
||
return "模组回复 REPLY";
|
||
}
|
||
|
||
if (msgId == (byte)FaceMessageType.Note)
|
||
{
|
||
return "模组主动通知 NOTE";
|
||
}
|
||
|
||
return "主控发送命令:" + DescribeCommand((FaceCommandId)msgId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析协议包 DATA。
|
||
/// </summary>
|
||
private static string DescribePacketData(byte msgId, byte[] data)
|
||
{
|
||
if (msgId == (byte)FaceMessageType.Reply)
|
||
{
|
||
return DescribeReplyData(data);
|
||
}
|
||
|
||
if (msgId == (byte)FaceMessageType.Note)
|
||
{
|
||
return DescribeNoteData(data);
|
||
}
|
||
|
||
return DescribeCommandData((FaceCommandId)msgId, data);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析 REPLY 数据。
|
||
/// </summary>
|
||
private static string DescribeReplyData(byte[] data)
|
||
{
|
||
if (data == null || data.Length < 2)
|
||
{
|
||
return "回复解析:DATA 长度不足,至少需要 MID + RESULT。";
|
||
}
|
||
|
||
var command = (FaceCommandId)data[0];
|
||
var result = (FaceResultCode)data[1];
|
||
var payload = Slice(data, 2, data.Length - 2);
|
||
var builder = new StringBuilder();
|
||
builder.AppendLine("回复命令:" + DescribeCommand(command));
|
||
builder.AppendLine("结果码:0x" + ((byte)result).ToString("X2") + "," + FaceProtocolEncoding.ResultMessage(result));
|
||
builder.Append("回复内容:" + DescribeReplyPayload(command, payload));
|
||
return builder.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析 NOTE 数据。
|
||
/// </summary>
|
||
private static string DescribeNoteData(byte[] data)
|
||
{
|
||
if (data == null || data.Length < 1)
|
||
{
|
||
return "通知解析:DATA 长度不足,至少需要 NID。";
|
||
}
|
||
|
||
var noteId = (FaceNoteId)data[0];
|
||
var payload = Slice(data, 1, data.Length - 1);
|
||
switch (noteId)
|
||
{
|
||
case FaceNoteId.Ready:
|
||
return "通知内容:READY,模组已就绪。";
|
||
case FaceNoteId.FaceState:
|
||
return "通知内容:人脸状态," + DescribeFaceStatePayload(payload);
|
||
case FaceNoteId.QrData:
|
||
return "通知内容:二维码数据," + Encoding.UTF8.GetString(payload);
|
||
default:
|
||
return "通知内容:" + noteId + ",原始 DATA=" + ToHex(payload, 0, payload.Length);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析主控命令 DATA。
|
||
/// </summary>
|
||
private static string DescribeCommandData(FaceCommandId command, byte[] data)
|
||
{
|
||
switch (command)
|
||
{
|
||
case FaceCommandId.Reset:
|
||
case FaceCommandId.GetStatus:
|
||
case FaceCommandId.FaceReset:
|
||
case FaceCommandId.GetVersion:
|
||
case FaceCommandId.GetSn:
|
||
case FaceCommandId.ReadUsbUvcParameters:
|
||
case FaceCommandId.UpgradeFirmware:
|
||
return "命令内容:" + DescribeCommand(command) + ",无参数。";
|
||
case FaceCommandId.Verify:
|
||
return data != null && data.Length == 2 ? DescribeAutoVerifyData(data) : DescribeVerifyData(data);
|
||
case FaceCommandId.Enroll:
|
||
case FaceCommandId.EnrollSingle:
|
||
return DescribeEnrollData(command, data, false);
|
||
case FaceCommandId.EnrollIntegrated:
|
||
return DescribeEnrollData(command, data, true);
|
||
case FaceCommandId.DeleteUser:
|
||
case FaceCommandId.GetUserInfo:
|
||
return "命令内容:" + DescribeCommand(command) + ",用户ID=" + ReadUInt16Safe(data, 0);
|
||
case FaceCommandId.DeleteAll:
|
||
return "命令内容:删除全部用户。";
|
||
case FaceCommandId.GetAllUserId:
|
||
case FaceCommandId.GetAllUserId2:
|
||
return "命令内容:" + DescribeCommand(command) + ",页索引=" + ReadByteSafe(data, 0);
|
||
case FaceCommandId.ScanQrCode:
|
||
return "命令内容:扫描二维码,超时=" + ReadByteSafe(data, 1) + " 秒。";
|
||
case FaceCommandId.SnapUploadLargeImage:
|
||
return "命令内容:抓拍大图,DPI=" + ReadByteSafe(data, 0) + "。";
|
||
case FaceCommandId.SetUsbUvcParameters:
|
||
return DescribeSetUvcData(data);
|
||
case FaceCommandId.SetFaceLocationDisplay:
|
||
return "命令内容:设置人脸框显示=" + (ReadByteSafe(data, 0) != 0) + "。";
|
||
case FaceCommandId.RgbLevel:
|
||
return "命令内容:设置 RGB_LEVEL=" + ReadByteSafe(data, 0) + "。";
|
||
case FaceCommandId.DuplicateCheck:
|
||
return data == null || data.Length == 0 ? "命令内容:查询查重状态。" : "命令内容:设置查重=" + (ReadByteSafe(data, 0) == 0) + "。";
|
||
case FaceCommandId.DemoMode:
|
||
return "命令内容:设置 Demo Mode=" + (ReadByteSafe(data, 0) != 0) + "。";
|
||
case FaceCommandId.ReadFeature:
|
||
return "命令内容:读取特征,用户ID=" + ReadUInt16Safe(data, 2) + ",特征类型=" + ReadByteSafe(data, 4) + "。";
|
||
case FaceCommandId.WriteFeature:
|
||
return DescribeWriteFeatureInitData(data);
|
||
case FaceCommandId.EnrollWithPhoto:
|
||
case FaceCommandId.EnrollWithPhotoAndId:
|
||
return DescribePhotoEnrollInitData(command, data);
|
||
default:
|
||
return "命令内容:" + DescribeCommand(command) + ",原始 DATA=" + (data == null || data.Length == 0 ? "无" : ToHex(data, 0, data.Length));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析 VERIFY 命令参数。
|
||
/// </summary>
|
||
private static string DescribeVerifyData(byte[] data)
|
||
{
|
||
return "命令内容:人脸验证,立即检测=" + ReadByteSafe(data, 0) + ",超时=" + ReadByteSafe(data, 1) + " 秒,最大识别次数=" + ReadByteSafe(data, 2) + "。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析 AUTO_VERIFY 命令参数。
|
||
/// </summary>
|
||
private static string DescribeAutoVerifyData(byte[] data)
|
||
{
|
||
return "命令内容:自动验证,启用=" + (ReadByteSafe(data, 0) != 0) + ",超时=" + ReadByteSafe(data, 1) + " 秒。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析录入命令参数。
|
||
/// </summary>
|
||
private static string DescribeEnrollData(FaceCommandId command, byte[] data, bool integrated)
|
||
{
|
||
var name = data != null && data.Length > 1 ? FaceProtocolEncoding.DecodeUserName(Slice(data, 1, Math.Min(FaceModuleConstants.UserNameSize, data.Length - 1))) : string.Empty;
|
||
var directionOffset = 1 + FaceModuleConstants.UserNameSize;
|
||
var builder = new StringBuilder();
|
||
builder.Append("命令内容:" + DescribeCommand(command));
|
||
builder.Append(",管理员=" + (ReadByteSafe(data, 0) != 0));
|
||
builder.Append(",用户名=" + name);
|
||
builder.Append(",方向=" + (FaceDirection)ReadByteSafe(data, directionOffset));
|
||
builder.Append(",超时=" + ReadByteSafe(data, directionOffset + 1) + " 秒");
|
||
if (integrated)
|
||
{
|
||
builder.Append(",单帧=" + (ReadByteSafe(data, directionOffset + 2) != 0));
|
||
builder.Append(",查重=" + (ReadByteSafe(data, directionOffset + 3) == 0));
|
||
}
|
||
|
||
builder.Append("。");
|
||
return builder.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析设置 UVC 参数。
|
||
/// </summary>
|
||
private static string DescribeSetUvcData(byte[] data)
|
||
{
|
||
var attributes = ReadByteSafe(data, 3);
|
||
return "命令内容:设置 UVC,USB=0x" + ReadByteSafe(data, 0).ToString("X2") + ",码率=" + ReadByteSafe(data, 1) + "Mbps,质量=" + ReadByteSafe(data, 2) + ",镜像=" + ((attributes & 0x01) != 0) + ",旋转180=" + ((attributes & 0x02) != 0) + "。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析照片注册初始化参数。
|
||
/// </summary>
|
||
private static string DescribePhotoEnrollInitData(FaceCommandId command, byte[] data)
|
||
{
|
||
var hasId = command == FaceCommandId.EnrollWithPhotoAndId;
|
||
var fileSizeOffset = hasId ? 4 : 2;
|
||
var nameLengthOffset = fileSizeOffset + 5;
|
||
var builder = new StringBuilder();
|
||
builder.Append("命令内容:" + DescribeCommand(command));
|
||
if (hasId)
|
||
{
|
||
builder.Append(",指定用户ID=" + ReadUInt16Safe(data, 2));
|
||
}
|
||
|
||
builder.Append(",文件长度=" + ReadUInt32Safe(data, fileSizeOffset));
|
||
builder.Append(",BioType=" + ReadByteSafe(data, fileSizeOffset + 4));
|
||
builder.Append(",用户名=" + ReadVariableNameSafe(data, nameLengthOffset));
|
||
var duplicateOffset = nameLengthOffset + 1 + ReadByteSafe(data, nameLengthOffset);
|
||
builder.Append(",查重=" + (ReadByteSafe(data, duplicateOffset) == 0) + "。");
|
||
return builder.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析写特征初始化参数。
|
||
/// </summary>
|
||
private static string DescribeWriteFeatureInitData(byte[] data)
|
||
{
|
||
return "命令内容:写入特征,特征类型=" + ReadByteSafe(data, 2) + ",用户名=" + ReadVariableNameSafe(data, 3) + "。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析 REPLY 业务数据。
|
||
/// </summary>
|
||
private static string DescribeReplyPayload(FaceCommandId command, byte[] payload)
|
||
{
|
||
switch (command)
|
||
{
|
||
case FaceCommandId.GetStatus:
|
||
return "模组状态=" + (FaceModuleStatus)ReadByteSafe(payload, 0) + "。";
|
||
case FaceCommandId.Verify:
|
||
case FaceCommandId.GetUserInfo:
|
||
return "用户ID=" + ReadUInt16Safe(payload, 0) + ",用户名=" + FaceProtocolEncoding.DecodeUserName(Slice(payload, 2, Math.Min(FaceModuleConstants.UserNameSize, Math.Max(0, payload.Length - 2)))) + ",管理员=" + (ReadByteSafe(payload, 34) != 0) + "。";
|
||
case FaceCommandId.Enroll:
|
||
case FaceCommandId.EnrollSingle:
|
||
case FaceCommandId.EnrollIntegrated:
|
||
case FaceCommandId.EnrollWithPhoto:
|
||
case FaceCommandId.EnrollWithPhotoAndId:
|
||
case FaceCommandId.WriteFeature:
|
||
return "用户ID=" + ReadUInt16Safe(payload, 0) + ",方向/附加值=" + ReadByteSafe(payload, 2) + "。";
|
||
case FaceCommandId.GetAllUserId:
|
||
return DescribeUserIdList(payload, false);
|
||
case FaceCommandId.GetAllUserId2:
|
||
return DescribeUserIdList(payload, true);
|
||
case FaceCommandId.ReadUsbUvcParameters:
|
||
return DescribeSetUvcData(payload);
|
||
default:
|
||
return payload == null || payload.Length == 0 ? "无。" : ToHex(payload, 0, payload.Length);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析用户 ID 列表。
|
||
/// </summary>
|
||
private static string DescribeUserIdList(byte[] payload, bool twoByteCount)
|
||
{
|
||
var count = twoByteCount ? ReadUInt16Safe(payload, 0) : ReadByteSafe(payload, 0);
|
||
var offset = twoByteCount ? 2 : 1;
|
||
var ids = new List<string>();
|
||
for (var i = 0; i < count && offset + 1 < payload.Length; i++, offset += 2)
|
||
{
|
||
ids.Add(ReadUInt16Safe(payload, offset).ToString());
|
||
}
|
||
|
||
return "用户数量=" + count + ",用户ID列表=" + string.Join(",", ids) + "。";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析人脸状态通知。
|
||
/// </summary>
|
||
private static string DescribeFaceStatePayload(byte[] payload)
|
||
{
|
||
if (payload == null || payload.Length < 2)
|
||
{
|
||
return "状态数据不足。";
|
||
}
|
||
|
||
var state = (FaceStateCode)ReadUInt16Safe(payload, 0);
|
||
return FaceProtocolEncoding.FaceStateMessage(state);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 返回命令中文名称。
|
||
/// </summary>
|
||
private static string DescribeCommand(FaceCommandId command)
|
||
{
|
||
switch (command)
|
||
{
|
||
case FaceCommandId.Reset: return "复位模组";
|
||
case FaceCommandId.GetStatus: return "获取状态";
|
||
case FaceCommandId.Verify: return "人脸验证 / 自动验证";
|
||
case FaceCommandId.Enroll: return "交互式人脸录入";
|
||
case FaceCommandId.EnrollSingle: return "单帧人脸录入";
|
||
case FaceCommandId.DeleteUser: return "删除用户";
|
||
case FaceCommandId.DeleteAll: return "删除全部用户";
|
||
case FaceCommandId.GetUserInfo: return "查询用户信息";
|
||
case FaceCommandId.FaceReset: return "取消当前人脸流程";
|
||
case FaceCommandId.GetAllUserId: return "查询所有用户 ID";
|
||
case FaceCommandId.GetAllUserId2: return "查询所有用户 ID 扩展";
|
||
case FaceCommandId.EnrollIntegrated: return "集成式人脸录入";
|
||
case FaceCommandId.GetVersion: return "获取版本";
|
||
case FaceCommandId.InitEncryption: return "初始化加密";
|
||
case FaceCommandId.SetReleaseEncKey: return "设置 Release Key";
|
||
case FaceCommandId.SetDebugEncKey: return "设置 Debug Key";
|
||
case FaceCommandId.ScanQrCode: return "扫描二维码";
|
||
case FaceCommandId.SnapUploadImage: return "抓拍普通图片";
|
||
case FaceCommandId.SnapUploadFaceImage: return "抓拍人脸图片";
|
||
case FaceCommandId.EnrollSnapFaceImage: return "抓拍人脸后注册";
|
||
case FaceCommandId.SnapUploadLargeImage: return "抓拍大图";
|
||
case FaceCommandId.GetSn: return "获取设备 SN";
|
||
case FaceCommandId.ReadUsbUvcParameters: return "读取 UVC 参数";
|
||
case FaceCommandId.SetUsbUvcParameters: return "设置 UVC 参数";
|
||
case FaceCommandId.SetFaceLocationDisplay: return "设置人脸框显示";
|
||
case FaceCommandId.RgbLevel: return "设置 RGB_LEVEL";
|
||
case FaceCommandId.EnrollWithPhotoAndId: return "指定 ID 照片注册";
|
||
case FaceCommandId.UpgradeFirmware: return "固件升级";
|
||
case FaceCommandId.EnrollWithPhoto: return "照片注册";
|
||
case FaceCommandId.ReadFeature: return "读取特征";
|
||
case FaceCommandId.WriteFeature: return "写入特征";
|
||
case FaceCommandId.DuplicateCheck: return "查重设置";
|
||
case FaceCommandId.DemoMode: return "Demo Mode";
|
||
default: return "未知命令 0x" + ((byte)command).ToString("X2");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 输出图片结果。
|
||
/// </summary>
|
||
private void LogImageResult(string name, FaceImageResult result)
|
||
{
|
||
Log(name + "抓拍完成:字节数=" + result.Bytes.Length + ",用户ID=" + result.UserId + ",贴图是否生成=" + (result.Texture != null));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 输出进度。
|
||
/// </summary>
|
||
private void LogProgress(int sent, int total)
|
||
{
|
||
Log("发送进度:" + sent + "/" + total);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析固定长度 Hex。
|
||
/// </summary>
|
||
private static byte[] ParseHex(string text, int expectedLength)
|
||
{
|
||
var parts = text.Split(new[] { ' ', '-', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length != expectedLength)
|
||
{
|
||
throw new ArgumentException("Hex 长度必须为 " + expectedLength + " bytes");
|
||
}
|
||
|
||
var bytes = new byte[parts.Length];
|
||
for (var i = 0; i < parts.Length; i++)
|
||
{
|
||
bytes[i] = Convert.ToByte(parts[i], 16);
|
||
}
|
||
|
||
return bytes;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析任意长度 Hex 字符串。
|
||
/// </summary>
|
||
private static byte[] ParseHexFlexible(string text)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
{
|
||
return Array.Empty<byte>();
|
||
}
|
||
|
||
var normalized = text.Replace("0x", string.Empty).Replace("0X", string.Empty).Replace("TX", " ").Replace("tx", " ").Replace("Rx", " ").Replace("RX", " ").Replace("rx", " ");
|
||
var parts = normalized.Split(new[] { ' ', '-', ',', ':', ':', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||
if (parts.Length == 1 && parts[0].Length > 2 && IsHexText(parts[0]))
|
||
{
|
||
var compact = parts[0];
|
||
if (compact.Length % 2 != 0)
|
||
{
|
||
throw new ArgumentException("连续 Hex 字符数量必须为偶数。");
|
||
}
|
||
|
||
var compactBytes = new byte[compact.Length / 2];
|
||
for (var i = 0; i < compactBytes.Length; i++)
|
||
{
|
||
compactBytes[i] = Convert.ToByte(compact.Substring(i * 2, 2), 16);
|
||
}
|
||
|
||
return compactBytes;
|
||
}
|
||
|
||
var result = new List<byte>();
|
||
for (var i = 0; i < parts.Length; i++)
|
||
{
|
||
if (!IsHexByteText(parts[i]))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
result.Add(Convert.ToByte(parts[i], 16));
|
||
}
|
||
|
||
if (result.Count == 0)
|
||
{
|
||
throw new ArgumentException("没有找到有效 Hex 字节。");
|
||
}
|
||
|
||
return result.ToArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断字符串是否全部为 Hex 字符。
|
||
/// </summary>
|
||
private static bool IsHexText(string text)
|
||
{
|
||
if (string.IsNullOrEmpty(text))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
for (var i = 0; i < text.Length; i++)
|
||
{
|
||
if (!IsHexChar(text[i]))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断字符串是否为一个 Hex byte。
|
||
/// </summary>
|
||
private static bool IsHexByteText(string text)
|
||
{
|
||
return text != null && text.Length == 2 && IsHexText(text);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断字符是否为 Hex 字符。
|
||
/// </summary>
|
||
private static bool IsHexChar(char value)
|
||
{
|
||
return value >= '0' && value <= '9' || value >= 'a' && value <= 'f' || value >= 'A' && value <= 'F';
|
||
}
|
||
|
||
/// <summary>
|
||
/// 截取 byte 数组。
|
||
/// </summary>
|
||
private static byte[] Slice(byte[] bytes, int offset, int length)
|
||
{
|
||
if (bytes == null || offset < 0 || length <= 0 || offset >= bytes.Length)
|
||
{
|
||
return Array.Empty<byte>();
|
||
}
|
||
|
||
length = Math.Min(length, bytes.Length - offset);
|
||
var result = new byte[length];
|
||
Buffer.BlockCopy(bytes, offset, result, 0, length);
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 安全读取 1 byte。
|
||
/// </summary>
|
||
private static byte ReadByteSafe(byte[] bytes, int offset)
|
||
{
|
||
return bytes != null && offset >= 0 && offset < bytes.Length ? bytes[offset] : (byte)0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 安全读取 UInt16。
|
||
/// </summary>
|
||
private static int ReadUInt16Safe(byte[] bytes, int offset)
|
||
{
|
||
return bytes != null && offset >= 0 && offset + 1 < bytes.Length ? (bytes[offset] << 8) | bytes[offset + 1] : 0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 安全读取 UInt32。
|
||
/// </summary>
|
||
private static uint ReadUInt32Safe(byte[] bytes, int offset)
|
||
{
|
||
if (bytes == null || offset < 0 || offset + 3 >= bytes.Length)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
return (uint)((bytes[offset] << 24) | (bytes[offset + 1] << 16) | (bytes[offset + 2] << 8) | bytes[offset + 3]);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 安全读取变长 UTF-8 名称。
|
||
/// </summary>
|
||
private static string ReadVariableNameSafe(byte[] bytes, int lengthOffset)
|
||
{
|
||
var length = ReadByteSafe(bytes, lengthOffset);
|
||
return FaceProtocolEncoding.DecodeUserName(Slice(bytes, lengthOffset + 1, length));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 转换 Hex 字符串。
|
||
/// </summary>
|
||
private static string ToHex(byte[] bytes, int offset, int length)
|
||
{
|
||
if (bytes == null || length <= 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
var builder = new StringBuilder();
|
||
var end = Math.Min(bytes.Length, offset + length);
|
||
for (var i = offset; i < end; i++)
|
||
{
|
||
if (builder.Length > 0)
|
||
{
|
||
builder.Append(' ');
|
||
}
|
||
|
||
builder.Append(bytes[i].ToString("X2"));
|
||
}
|
||
|
||
return builder.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 输出普通日志。
|
||
/// </summary>
|
||
private void Log(string message)
|
||
{
|
||
Debug.Log("[FaceModule键盘测试] " + message);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 输出错误日志。
|
||
/// </summary>
|
||
private void LogError(string message)
|
||
{
|
||
Debug.LogError("[FaceModule键盘测试] " + message);
|
||
}
|
||
}
|
||
}
|