573 lines
18 KiB
C#
573 lines
18 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO.Ports;
|
||
using System.Threading.Tasks;
|
||
using Aisu.SIM1.Face.Core;
|
||
using Aisu.SIM1.Face.Models;
|
||
using Aisu.SIM1.Face.Protocol;
|
||
using UnityEngine;
|
||
using UnityEngine.UI;
|
||
|
||
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
|
||
{
|
||
/// <summary>
|
||
/// 人脸检测 Demo 主流程控制器。
|
||
/// </summary>
|
||
public sealed class FaceDetectionDemoController : MonoBehaviour
|
||
{
|
||
[SerializeField] private Dropdown m_cameraDropdown; // 摄像头下拉框。
|
||
[SerializeField] private Dropdown m_serialDropdown; // 串口下拉框。
|
||
[SerializeField] private InputField m_serialInput; // 串口输入兜底。
|
||
[SerializeField] private InputField m_baudRateInput; // 波特率输入。
|
||
[SerializeField] private InputField m_userNameInput; // 用户名输入。
|
||
[SerializeField] private Toggle m_adminToggle; // 管理员开关。
|
||
[SerializeField] private Text m_statusText; // 状态文本。
|
||
[SerializeField] private Text m_resultText; // 结果文本。
|
||
|
||
private FaceModuleManager m_manager; // SDK Manager。
|
||
private FaceDetectionDemoCamera m_camera; // 摄像头预览。
|
||
private FaceDetectionDemoLogger m_logger; // 日志。
|
||
private FaceDetectionDemoUserList m_userList; // 用户列表。
|
||
|
||
/// <summary>
|
||
/// 绑定 MVP UI。
|
||
/// </summary>
|
||
public void Bind(
|
||
FaceModuleManager manager,
|
||
FaceDetectionDemoCamera camera,
|
||
FaceDetectionDemoLogger logger,
|
||
FaceDetectionDemoUserList userList,
|
||
Dropdown cameraDropdown,
|
||
Dropdown serialDropdown,
|
||
InputField serialInput,
|
||
InputField baudRateInput,
|
||
InputField userNameInput,
|
||
Toggle adminToggle,
|
||
Text statusText,
|
||
Text resultText)
|
||
{
|
||
m_manager = manager;
|
||
m_camera = camera;
|
||
m_logger = logger;
|
||
m_userList = userList;
|
||
m_cameraDropdown = cameraDropdown;
|
||
m_serialDropdown = serialDropdown;
|
||
m_serialInput = serialInput;
|
||
m_baudRateInput = baudRateInput;
|
||
m_userNameInput = userNameInput;
|
||
m_adminToggle = adminToggle;
|
||
m_statusText = statusText;
|
||
m_resultText = resultText;
|
||
|
||
if (m_manager != null)
|
||
{
|
||
m_manager.OnError += OnManagerError;
|
||
m_manager.OnReady += OnManagerReady;
|
||
m_manager.OnFaceStateUpdated += OnFaceStateUpdated;
|
||
m_manager.OnAutoVerifyResult += OnAutoVerifyResult;
|
||
}
|
||
|
||
if (m_serialDropdown != null)
|
||
{
|
||
m_serialDropdown.onValueChanged.AddListener(OnSerialDropdownChanged);
|
||
}
|
||
|
||
RefreshCameras();
|
||
RefreshSerialPorts();
|
||
RefreshStatus();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新摄像头列表。
|
||
/// </summary>
|
||
public void RefreshCameras()
|
||
{
|
||
if (m_camera == null)
|
||
{
|
||
SetDropdownOptions(m_cameraDropdown, Array.Empty<string>(), "未发现摄像头");
|
||
LogInfo("摄像头数量: 0");
|
||
return;
|
||
}
|
||
|
||
LogInfo("正在刷新摄像头");
|
||
m_camera.GetDeviceNamesWhenAuthorized(names =>
|
||
{
|
||
SetDropdownOptions(m_cameraDropdown, names, "未发现摄像头");
|
||
LogInfo("摄像头数量: " + names.Length);
|
||
if (names.Length == 0)
|
||
{
|
||
LogInfo("Android 未枚举到摄像头时,请确认 USB 摄像头是否被系统 Camera API 识别;部分 UVC 设备需要原生插件读取");
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 打开摄像头。
|
||
/// </summary>
|
||
public void OpenCamera()
|
||
{
|
||
if (m_cameraDropdown == null || m_cameraDropdown.options == null || m_cameraDropdown.options.Count == 0)
|
||
{
|
||
LogError("未发现可打开的摄像头");
|
||
return;
|
||
}
|
||
|
||
var deviceName = SelectedDropdownText(m_cameraDropdown);
|
||
if (deviceName == "未发现摄像头")
|
||
{
|
||
LogError("未发现可打开的摄像头");
|
||
return;
|
||
}
|
||
|
||
m_camera?.Open(m_cameraDropdown.value);
|
||
RefreshStatus();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 销毁测试页面时释放连接和事件。
|
||
/// </summary>
|
||
private void OnDestroy()
|
||
{
|
||
if (m_manager != null)
|
||
{
|
||
m_manager.OnError -= OnManagerError;
|
||
m_manager.OnReady -= OnManagerReady;
|
||
m_manager.OnFaceStateUpdated -= OnFaceStateUpdated;
|
||
m_manager.OnAutoVerifyResult -= OnAutoVerifyResult;
|
||
m_manager.Close();
|
||
}
|
||
|
||
if (m_serialDropdown != null)
|
||
{
|
||
m_serialDropdown.onValueChanged.RemoveListener(OnSerialDropdownChanged);
|
||
}
|
||
|
||
m_camera?.Close();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭摄像头。
|
||
/// </summary>
|
||
public void CloseCamera()
|
||
{
|
||
m_camera?.Close();
|
||
LogInfo("摄像头已关闭");
|
||
RefreshStatus();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新串口列表。
|
||
/// </summary>
|
||
public void RefreshSerialPorts()
|
||
{
|
||
string[] ports;
|
||
try
|
||
{
|
||
ports = SerialPort.GetPortNames();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
ports = Array.Empty<string>();
|
||
LogError("串口枚举失败: " + ex.Message);
|
||
}
|
||
|
||
SetDropdownOptions(m_serialDropdown, ports, "手动输入串口");
|
||
if (ports.Length > 0 && m_serialInput != null)
|
||
{
|
||
m_serialInput.text = ports[0];
|
||
}
|
||
|
||
LogInfo("串口数量: " + ports.Length);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 连接模组。
|
||
/// </summary>
|
||
public void ConnectModule()
|
||
{
|
||
RunAsync(async () =>
|
||
{
|
||
if (m_manager == null)
|
||
{
|
||
LogError("FaceModuleManager 不存在");
|
||
return;
|
||
}
|
||
|
||
var portName = ReadPortName();
|
||
if (string.IsNullOrWhiteSpace(portName))
|
||
{
|
||
LogError("未选择串口");
|
||
return;
|
||
}
|
||
|
||
var baudRate = ReadInt(m_baudRateInput, 115200);
|
||
m_manager.Configure(false, portName, baudRate);
|
||
m_manager.Config.RawLogEnabled = false;
|
||
LogInfo("正在连接模组: " + portName + " @ " + baudRate);
|
||
m_manager.Open();
|
||
await WaitReadyAndProbeAsync();
|
||
LogInfo("模组连接成功,状态: " + m_manager.CurrentStatus);
|
||
RefreshStatus();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开模组。
|
||
/// </summary>
|
||
public void DisconnectModule()
|
||
{
|
||
RunAsync(async () =>
|
||
{
|
||
if (m_manager != null)
|
||
{
|
||
await m_manager.CloseAsync();
|
||
}
|
||
|
||
LogInfo("模组连接已关闭");
|
||
RefreshStatus();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 录入人脸。
|
||
/// </summary>
|
||
public void EnrollFace()
|
||
{
|
||
RunAsync(async () =>
|
||
{
|
||
if (!RequireReady())
|
||
{
|
||
return;
|
||
}
|
||
|
||
var userName = string.IsNullOrWhiteSpace(m_userNameInput?.text) ? "User" : m_userNameInput.text.Trim();
|
||
await PrepareFaceOperationAsync("录入人脸");
|
||
LogInfo("开始录入人脸: " + userName);
|
||
var result = await EnrollSingleWithFallbackAsync(m_adminToggle != null && m_adminToggle.isOn, userName, 30);
|
||
if (result.Success)
|
||
{
|
||
SetResult("录入成功,UserID=" + result.UserId);
|
||
LogInfo("录入成功,UserID=" + result.UserId);
|
||
await RefreshUsersInternal();
|
||
}
|
||
else
|
||
{
|
||
var message = FaceProtocolEncoding.ResultMessage(result.ResultCode);
|
||
SetResult("录入失败: " + message);
|
||
LogError("录入失败: " + message);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测人脸。
|
||
/// </summary>
|
||
public void VerifyFace()
|
||
{
|
||
RunAsync(async () =>
|
||
{
|
||
if (!RequireReady())
|
||
{
|
||
return;
|
||
}
|
||
|
||
await PrepareFaceOperationAsync("检测人脸");
|
||
LogInfo("开始检测人脸");
|
||
var result = await m_manager.Verify.VerifyAsync(30, 60);
|
||
if (result.Success)
|
||
{
|
||
var message = $"检测成功,UserID={result.UserId},姓名={result.UserName},管理员={result.IsAdmin}";
|
||
SetResult(message);
|
||
LogInfo(message);
|
||
}
|
||
else
|
||
{
|
||
var message = FaceProtocolEncoding.ResultMessage(result.ResultCode);
|
||
SetResult("检测失败: " + message);
|
||
LogError("检测失败: " + message);
|
||
}
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新用户列表。
|
||
/// </summary>
|
||
public void RefreshUsers()
|
||
{
|
||
RunAsync(RefreshUsersInternal);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除指定用户。
|
||
/// </summary>
|
||
public void DeleteUser(int userId)
|
||
{
|
||
RunAsync(async () =>
|
||
{
|
||
if (!RequireReady())
|
||
{
|
||
return;
|
||
}
|
||
|
||
await m_manager.Users.DeleteUserAsync(userId);
|
||
LogInfo("已删除用户: " + userId);
|
||
await RefreshUsersInternal();
|
||
});
|
||
}
|
||
|
||
private async Task RefreshUsersInternal()
|
||
{
|
||
if (!RequireReady())
|
||
{
|
||
return;
|
||
}
|
||
|
||
await EnsureModuleStandbyAsync("刷新用户列表");
|
||
var users = await GetAllUsersWithFallbackAsync();
|
||
m_userList?.SetUsers(users);
|
||
LogInfo("用户列表已刷新,数量: " + users.Count);
|
||
}
|
||
|
||
private bool RequireReady()
|
||
{
|
||
if (m_manager != null && m_manager.IsOpen && m_manager.IsReady)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
LogError("模组未连接或未 READY");
|
||
RefreshStatus();
|
||
return false;
|
||
}
|
||
|
||
private void RefreshStatus()
|
||
{
|
||
if (m_statusText == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var cameraStatus = m_camera != null && m_camera.IsOpen ? "摄像头已打开" : "摄像头未打开";
|
||
var moduleStatus = m_manager == null
|
||
? "模组不存在"
|
||
: (m_manager.IsReady ? "模组 READY" : (m_manager.IsOpen ? "模组已连接,未 READY" : "模组未连接"));
|
||
m_statusText.text = cameraStatus + " | " + moduleStatus;
|
||
}
|
||
|
||
private void OnFaceStateUpdated(FaceFaceStateNote state)
|
||
{
|
||
SetResult("人脸状态: " + FaceProtocolEncoding.FaceStateMessage(state.State));
|
||
}
|
||
|
||
private void OnAutoVerifyResult(FaceAutoVerifyResult result)
|
||
{
|
||
if (result.Success)
|
||
{
|
||
SetResult("自动检测成功,UserID=" + result.VerifyResult.UserId + ",姓名=" + result.VerifyResult.UserName);
|
||
return;
|
||
}
|
||
|
||
SetResult("自动检测结果: " + FaceProtocolEncoding.ResultMessage(result.ResultCode));
|
||
}
|
||
|
||
private void OnManagerError(FaceResultCode _, string message)
|
||
{
|
||
LogError("SDK 错误: " + message);
|
||
}
|
||
|
||
private void OnManagerReady()
|
||
{
|
||
LogInfo("模组 READY");
|
||
}
|
||
|
||
private async Task WaitReadyAndProbeAsync()
|
||
{
|
||
try
|
||
{
|
||
await m_manager.WaitReadyAsync(1500);
|
||
}
|
||
catch (TimeoutException)
|
||
{
|
||
LogInfo("未收到 READY,改用主动查询确认模组状态");
|
||
}
|
||
|
||
LogInfo("正在查询模组状态");
|
||
await m_manager.ProbeReadyByStatusAsync();
|
||
}
|
||
|
||
private async Task PrepareFaceOperationAsync(string operationName)
|
||
{
|
||
await EnsureModuleStandbyAsync(operationName);
|
||
await ResetFaceFlowIfPossibleAsync(operationName);
|
||
}
|
||
|
||
private async Task EnsureModuleStandbyAsync(string operationName)
|
||
{
|
||
if (m_manager.IsAutoVerifyEnabled)
|
||
{
|
||
await m_manager.EnsureExclusiveOperationReadyAsync(operationName);
|
||
return;
|
||
}
|
||
|
||
var status = await m_manager.Basic.GetStatusAsync();
|
||
if (status == FaceModuleStatus.Busy)
|
||
{
|
||
LogInfo(operationName + " 前模组忙碌,正在取消当前人脸流程");
|
||
await ResetFaceFlowIfPossibleAsync(operationName);
|
||
await Task.Delay(300);
|
||
status = await m_manager.Basic.GetStatusAsync();
|
||
}
|
||
|
||
if (status == FaceModuleStatus.Busy)
|
||
{
|
||
throw new InvalidOperationException(operationName + " 前模组仍处于忙碌状态");
|
||
}
|
||
|
||
if (status == FaceModuleStatus.Error || status == FaceModuleStatus.Invalid)
|
||
{
|
||
throw new InvalidOperationException(operationName + " 前模组状态异常: " + status);
|
||
}
|
||
}
|
||
|
||
private async Task ResetFaceFlowIfPossibleAsync(string operationName)
|
||
{
|
||
try
|
||
{
|
||
await m_manager.StopFaceOperationAsync();
|
||
LogInfo(operationName + " 前已取消残留人脸流程");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogInfo(operationName + " 前取消残留流程未完成: " + ex.Message);
|
||
}
|
||
}
|
||
|
||
private async Task<FaceEnrollResult> EnrollSingleWithFallbackAsync(bool admin, string userName, byte timeout)
|
||
{
|
||
try
|
||
{
|
||
return await m_manager.Enrollment.EnrollSingleAsync(admin, userName, timeout);
|
||
}
|
||
catch (TimeoutException)
|
||
{
|
||
LogInfo("单帧录入超时,改用集成单帧录入重试");
|
||
await ResetFaceFlowIfPossibleAsync("录入重试");
|
||
return await m_manager.Enrollment.EnrollIntegratedAsync(admin, userName, FaceDirection.Middle, timeout, true, true);
|
||
}
|
||
}
|
||
|
||
private async Task<List<FaceUserInfo>> GetAllUsersWithFallbackAsync()
|
||
{
|
||
try
|
||
{
|
||
return await m_manager.Users.GetAllUserInfos2Async();
|
||
}
|
||
catch (TimeoutException)
|
||
{
|
||
LogInfo("扩展用户列表超时,改用普通用户列表查询");
|
||
return await m_manager.Users.GetAllUserInfosAsync();
|
||
}
|
||
}
|
||
|
||
private void OnSerialDropdownChanged(int _)
|
||
{
|
||
var selected = SelectedDropdownText(m_serialDropdown);
|
||
if (!string.IsNullOrWhiteSpace(selected) && selected != "手动输入串口" && m_serialInput != null)
|
||
{
|
||
m_serialInput.text = selected;
|
||
}
|
||
}
|
||
|
||
private string ReadPortName()
|
||
{
|
||
var selected = SelectedDropdownText(m_serialDropdown);
|
||
if (!string.IsNullOrWhiteSpace(selected) && selected != "手动输入串口")
|
||
{
|
||
return selected;
|
||
}
|
||
|
||
return m_serialInput != null ? m_serialInput.text.Trim() : string.Empty;
|
||
}
|
||
|
||
private static int ReadInt(InputField input, int fallback)
|
||
{
|
||
return input != null && int.TryParse(input.text, out var value) ? value : fallback;
|
||
}
|
||
|
||
private static string SelectedDropdownText(Dropdown dropdown)
|
||
{
|
||
if (dropdown == null || dropdown.options == null || dropdown.options.Count == 0)
|
||
{
|
||
return string.Empty;
|
||
}
|
||
|
||
return dropdown.options[Mathf.Clamp(dropdown.value, 0, dropdown.options.Count - 1)].text;
|
||
}
|
||
|
||
private static void SetDropdownOptions(Dropdown dropdown, IReadOnlyList<string> values, string emptyText)
|
||
{
|
||
if (dropdown == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
dropdown.ClearOptions();
|
||
var options = new List<string>();
|
||
if (values != null)
|
||
{
|
||
for (var i = 0; i < values.Count; i++)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(values[i]))
|
||
{
|
||
options.Add(values[i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (options.Count == 0)
|
||
{
|
||
options.Add(emptyText);
|
||
}
|
||
|
||
dropdown.AddOptions(options);
|
||
dropdown.value = 0;
|
||
dropdown.RefreshShownValue();
|
||
}
|
||
|
||
private async void RunAsync(Func<Task> action)
|
||
{
|
||
try
|
||
{
|
||
await action();
|
||
RefreshStatus();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogError(ex.Message);
|
||
SetResult("错误: " + ex.Message);
|
||
RefreshStatus();
|
||
}
|
||
}
|
||
|
||
private void SetResult(string message)
|
||
{
|
||
if (m_resultText != null)
|
||
{
|
||
m_resultText.text = message;
|
||
}
|
||
}
|
||
|
||
private void LogInfo(string message)
|
||
{
|
||
m_logger?.Info(message);
|
||
}
|
||
|
||
private void LogError(string message)
|
||
{
|
||
m_logger?.Error(message);
|
||
}
|
||
}
|
||
}
|
||
|
||
|