si-m1-face-package/Samples~/Face Detection Demo/Scripts/FaceDetectionDemoController.cs

566 lines
18 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.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()
{
m_manager?.Close();
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.Basic.FaceResetAsync();
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);
}
}
}