1355 lines
54 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}