Compare commits

...

2 Commits

Author SHA1 Message Date
su
affb537801 Publish obfuscated DLL package with full samples 2026-08-19 12:06:13 +08:00
su
72858e3ada Update DLL-only package samples 2026-08-19 11:56:47 +08:00
54 changed files with 6142 additions and 5 deletions

2
.gitignore vendored
View File

@ -11,6 +11,8 @@ UserSettings/
*.asmdef
*.pdb
*.mdb
!Samples~/**/*.cs
!Samples~/**/*.asmdef
# OS/editor noise
.DS_Store

Binary file not shown.

Binary file not shown.

View File

@ -11,7 +11,7 @@ PluginImporter:
enabled: 1
settings: {}
Editor:
enabled: 0
enabled: 1
settings:
DefaultValueInitialized: true
WindowsStoreApps:

8
Samples~.meta Normal file
View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 98eb2c19172144b47b7bc037fc6f5923
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3f0e42a6f3e84ef2b9f0d2a01f5d7c31
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "Aisu.SIM1.Face.Samples.FaceDetectionDemo",
"rootNamespace": "Aisu.SIM1.Face.Samples.FaceDetectionDemo",
"references": [
"Aisu.SIM1.Face.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 44fd32577c8a40df9df37c2ef1e3d003
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,57 @@
# SI-M1 Face Detection Demo
该目录是 `SI-M1 Face Demo` Sample 内的人脸检测最小测试场景,用于验证 USB 摄像头、串口连接、录入、识别和用户数据存储。
## 场景
通过 Package Manager 导入 `SI-M1 Face Demo`,或使用 `AISU/SI-M1 Face SDK/Generate Demo Scene` 后,场景会位于:
```text
Assets/Samples/Face Detection Demo/Scenes/FaceDetectionDemo.unity
```
打开该场景后运行,`FaceDetectionDemoBootstrap` 会自动生成单页 UGUI。
## MVP 功能
- 使用 `WebCamTexture` 枚举摄像头。
- 选择摄像头并显示实时预览。
- 枚举串口,枚举失败时可手动输入串口名。
- 连接 / 断开 SI-M1 模组。
- 输入用户名并录入人脸。
- 检测人脸并显示识别结果。
- 刷新人脸数据列表。
- 删除指定用户。
- 下方日志窗口记录所有操作,可上下拖动查看。
## 授权文件
真实串口运行需要授权文件:
```text
Assets/StreamingAssets/si-m1_face.dat
```
Mock 模式不需要授权,但本 MVP 默认用于真实设备测试,不提供 Mock 开关。
## Android 注意事项
- 摄像头读取使用 Unity `WebCamTexture`。
- Android 外接 USB 摄像头是否能被 `WebCamTexture.devices` 枚举,取决于设备系统和摄像头驱动。
- 如果 APK 中无法枚举或打开 USB 摄像头,需要接 Android UVC 原生插件。
- 串口枚举使用 `SerialPort.GetPortNames()`;Android 设备上如果枚举不到串口,可在输入框手动填写实际串口路径。
## 测试顺序
1. 点击 `刷新摄像头`。
2. 从下拉框选择 USB 摄像头。
3. 点击 `打开摄像头`,确认预览画面正常。
4. 点击 `刷新串口`,选择或手动输入串口。
5. 点击 `连接模组`,等待 READY。
6. 输入用户名,点击 `录入人脸`。
7. 点击 `检测人脸`。
8. 点击 `刷新用户列表`,确认用户数据已存储。
9. 点击用户行的 `删除`,确认可删除用户。

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 28af189cc75c46d3857843802ab27446
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3b3b5204665d4d94be3e40f59ce773c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,162 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
buildHeightMesh: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &100000
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 100001}
- component: {fileID: 100002}
m_Layer: 0
m_Name: FaceDetectionDemoBootstrap
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!4 &100001
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100000}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!114 &100002
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 100000}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5a5af54f3c89457a8e08e47e0b46c1c1, type: 3}
m_Name:
m_EditorClassIdentifier:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: d5d10f2a28b74fd98aacbe5c80561894
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7800e597b5f45d7a7da08b3f936a1f2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,382 @@
using Aisu.SIM1.Face.Core;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
{
/// <summary>
/// 人脸检测 Demo 场景 UI 动态构建器。
/// </summary>
public sealed class FaceDetectionDemoBootstrap : MonoBehaviour
{
private Font m_font; // UI 字体。
private void Awake()
{
Build();
}
/// <summary>
/// 构建 MVP 测试页面。
/// </summary>
public void Build()
{
m_font = Resources.GetBuiltinResource<Font>("Arial.ttf");
EnsureEventSystem();
var manager = FindObjectOfType<FaceModuleManager>() ?? new GameObject("FaceModuleManager").AddComponent<FaceModuleManager>();
var camera = gameObject.AddComponent<FaceDetectionDemoCamera>();
var logger = gameObject.AddComponent<FaceDetectionDemoLogger>();
var userList = gameObject.AddComponent<FaceDetectionDemoUserList>();
var controller = gameObject.AddComponent<FaceDetectionDemoController>();
var canvasGo = Create("FaceDetectionDemoCanvas", transform);
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.AddComponent<GraphicRaycaster>();
var scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1280, 720);
var root = Create("Root", canvasGo.transform);
Stretch(root.GetComponent<RectTransform>());
var rootLayout = root.AddComponent<VerticalLayoutGroup>();
rootLayout.padding = new RectOffset(16, 16, 16, 16);
rootLayout.spacing = 10;
rootLayout.childControlWidth = true;
rootLayout.childForceExpandWidth = true;
var title = Text(root.transform, "SI-M1 Face Detection Demo", 28, TextAnchor.MiddleLeft);
title.color = new Color(0.05f, 0.07f, 0.1f);
var statusText = Text(root.transform, "摄像头未打开 | 模组未连接", 20, TextAnchor.MiddleLeft);
statusText.color = new Color(0.12f, 0.18f, 0.24f);
var body = Create("Body", root.transform);
body.AddComponent<LayoutElement>().flexibleHeight = 1f;
var bodyLayout = body.AddComponent<HorizontalLayoutGroup>();
bodyLayout.spacing = 12;
bodyLayout.childControlHeight = true;
bodyLayout.childForceExpandHeight = true;
var left = Panel(body.transform, "LeftPanel", new Color(0.95f, 0.97f, 1f));
left.AddComponent<LayoutElement>().flexibleWidth = 1f;
var leftLayout = left.AddComponent<VerticalLayoutGroup>();
leftLayout.padding = new RectOffset(10, 10, 10, 10);
leftLayout.spacing = 8;
leftLayout.childControlWidth = true;
leftLayout.childForceExpandWidth = true;
var right = Panel(body.transform, "RightPanel", new Color(0.98f, 0.98f, 0.96f));
right.AddComponent<LayoutElement>().preferredWidth = 430;
var rightLayout = right.AddComponent<VerticalLayoutGroup>();
rightLayout.padding = new RectOffset(10, 10, 10, 10);
rightLayout.spacing = 8;
rightLayout.childControlWidth = true;
rightLayout.childForceExpandWidth = true;
var cameraDropdown = Dropdown(left.transform, "未发现摄像头");
var cameraRow = Row(left.transform);
var refreshCameraButton = Button(cameraRow.transform, "刷新摄像头");
var openCameraButton = Button(cameraRow.transform, "打开摄像头");
var closeCameraButton = Button(cameraRow.transform, "关闭摄像头");
var previewFrame = Panel(left.transform, "CameraPreview", Color.black);
previewFrame.AddComponent<LayoutElement>().flexibleHeight = 1f;
var preview = Create("Preview", previewFrame.transform).AddComponent<RawImage>();
preview.color = Color.white;
var previewRect = preview.GetComponent<RectTransform>();
previewRect.anchorMin = Vector2.zero;
previewRect.anchorMax = Vector2.one;
previewRect.offsetMin = Vector2.zero;
previewRect.offsetMax = Vector2.zero;
var aspect = preview.gameObject.AddComponent<AspectRatioFitter>();
aspect.aspectMode = AspectRatioFitter.AspectMode.FitInParent;
Text(right.transform, "串口 / 模组", 22, TextAnchor.MiddleLeft);
var serialDropdown = Dropdown(right.transform, "手动输入串口");
var serialInput = Input(right.transform, "串口名,例如 COM3 或 /dev/ttyUSB0");
var baudInput = Input(right.transform, "115200");
var serialRow = Row(right.transform);
var refreshSerialButton = Button(serialRow.transform, "刷新串口");
var connectButton = Button(serialRow.transform, "连接模组");
var disconnectButton = Button(serialRow.transform, "断开模组");
Text(right.transform, "人脸操作", 22, TextAnchor.MiddleLeft);
var userNameInput = Input(right.transform, "User");
var adminToggle = Toggle(right.transform, "管理员", false);
var faceRow = Row(right.transform);
var enrollButton = Button(faceRow.transform, "录入人脸");
var verifyButton = Button(faceRow.transform, "检测人脸");
var resultText = Text(right.transform, "结果: -", 20, TextAnchor.UpperLeft);
resultText.gameObject.AddComponent<LayoutElement>().preferredHeight = 58;
Text(right.transform, "人脸数据", 22, TextAnchor.MiddleLeft);
var userButtonRow = Row(right.transform);
var refreshUsersButton = Button(userButtonRow.transform, "刷新用户列表");
var userScroll = ScrollView(right.transform, out var userContent);
userScroll.GetComponent<LayoutElement>().preferredHeight = 170;
Text(root.transform, "操作日志", 22, TextAnchor.MiddleLeft);
var logRow = Row(root.transform);
var clearLogButton = Button(logRow.transform, "清空日志");
var logScroll = ScrollView(root.transform, out var logContent);
logScroll.GetComponent<LayoutElement>().preferredHeight = 150;
var logText = Text(logContent, string.Empty, 18, TextAnchor.UpperLeft);
logText.font = m_font;
logText.color = Color.white;
logText.horizontalOverflow = HorizontalWrapMode.Wrap;
logText.verticalOverflow = VerticalWrapMode.Overflow;
logText.gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
logContent.gameObject.GetComponent<Image>().color = new Color(0.07f, 0.08f, 0.09f);
camera.Bind(preview, aspect, logger);
logger.Bind(logText, logScroll);
userList.Bind(userContent, m_font, controller.DeleteUser);
controller.Bind(manager, camera, logger, userList, cameraDropdown, serialDropdown, serialInput, baudInput, userNameInput, adminToggle, statusText, resultText);
refreshCameraButton.onClick.AddListener(controller.RefreshCameras);
openCameraButton.onClick.AddListener(controller.OpenCamera);
closeCameraButton.onClick.AddListener(controller.CloseCamera);
refreshSerialButton.onClick.AddListener(controller.RefreshSerialPorts);
connectButton.onClick.AddListener(controller.ConnectModule);
disconnectButton.onClick.AddListener(controller.DisconnectModule);
enrollButton.onClick.AddListener(controller.EnrollFace);
verifyButton.onClick.AddListener(controller.VerifyFace);
refreshUsersButton.onClick.AddListener(controller.RefreshUsers);
clearLogButton.onClick.AddListener(logger.Clear);
}
private static void EnsureEventSystem()
{
if (FindObjectOfType<EventSystem>() != null)
{
return;
}
var eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
private GameObject Panel(Transform parent, string name, Color color)
{
var go = Create(name, parent);
go.AddComponent<Image>().color = color;
return go;
}
private GameObject Row(Transform parent)
{
var row = Create("Row", parent);
var layout = row.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 8;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
row.AddComponent<LayoutElement>().preferredHeight = 44;
return row;
}
private Button Button(Transform parent, string text)
{
var go = Create(text, parent);
go.AddComponent<Image>().color = new Color(0.78f, 0.86f, 0.95f);
var button = go.AddComponent<Button>();
go.AddComponent<LayoutElement>().preferredHeight = 42;
var label = Text(go.transform, text, 20, TextAnchor.MiddleCenter);
Stretch(label.GetComponent<RectTransform>());
return button;
}
private InputField Input(Transform parent, string placeholder)
{
var go = Create("InputField", parent);
go.AddComponent<Image>().color = Color.white;
go.AddComponent<LayoutElement>().preferredHeight = 42;
var input = go.AddComponent<InputField>();
var text = Text(go.transform, string.Empty, 20, TextAnchor.MiddleLeft);
var textRect = text.GetComponent<RectTransform>();
Stretch(textRect);
textRect.offsetMin = new Vector2(10, 0);
textRect.offsetMax = new Vector2(-10, 0);
input.textComponent = text;
var placeholderText = Text(go.transform, placeholder, 20, TextAnchor.MiddleLeft);
placeholderText.color = new Color(0.45f, 0.48f, 0.52f);
var placeholderRect = placeholderText.GetComponent<RectTransform>();
Stretch(placeholderRect);
placeholderRect.offsetMin = new Vector2(10, 0);
placeholderRect.offsetMax = new Vector2(-10, 0);
input.placeholder = placeholderText;
return input;
}
private Toggle Toggle(Transform parent, string label, bool value)
{
var row = Row(parent);
row.GetComponent<HorizontalLayoutGroup>().childForceExpandWidth = false;
var toggleGo = Create("Toggle", row.transform);
toggleGo.AddComponent<LayoutElement>().preferredWidth = 34;
var toggle = toggleGo.AddComponent<Toggle>();
var background = Create("Background", toggleGo.transform).AddComponent<Image>();
background.color = Color.white;
Stretch(background.GetComponent<RectTransform>());
var checkmark = Create("Checkmark", background.transform).AddComponent<Image>();
checkmark.color = new Color(0.18f, 0.45f, 0.85f);
var checkRect = checkmark.GetComponent<RectTransform>();
checkRect.anchorMin = new Vector2(0.2f, 0.2f);
checkRect.anchorMax = new Vector2(0.8f, 0.8f);
checkRect.offsetMin = Vector2.zero;
checkRect.offsetMax = Vector2.zero;
toggle.targetGraphic = background;
toggle.graphic = checkmark;
toggle.isOn = value;
Text(row.transform, label, 20, TextAnchor.MiddleLeft).gameObject.AddComponent<LayoutElement>().flexibleWidth = 1f;
return toggle;
}
private Dropdown Dropdown(Transform parent, string initial)
{
var go = Create("Dropdown", parent);
var image = go.AddComponent<Image>();
image.color = Color.white;
go.AddComponent<LayoutElement>().preferredHeight = 42;
var dropdown = go.AddComponent<Dropdown>();
dropdown.targetGraphic = image;
var label = Text(go.transform, initial, 20, TextAnchor.MiddleLeft);
var labelRect = label.GetComponent<RectTransform>();
Stretch(labelRect);
labelRect.offsetMin = new Vector2(10, 0);
labelRect.offsetMax = new Vector2(-30, 0);
var arrow = Text(go.transform, "v", 18, TextAnchor.MiddleCenter);
var arrowRect = arrow.GetComponent<RectTransform>();
arrowRect.anchorMin = new Vector2(1f, 0f);
arrowRect.anchorMax = Vector2.one;
arrowRect.pivot = new Vector2(1f, 0.5f);
arrowRect.sizeDelta = new Vector2(30, 0);
arrowRect.anchoredPosition = Vector2.zero;
dropdown.template = CreateDropdownTemplate(go.transform, out var itemText);
dropdown.captionText = label;
dropdown.itemText = itemText;
dropdown.options.Add(new Dropdown.OptionData(initial));
dropdown.value = 0;
return dropdown;
}
private RectTransform CreateDropdownTemplate(Transform parent, out Text itemText)
{
var template = Panel(parent, "Template", Color.white);
template.SetActive(false);
var templateRect = template.GetComponent<RectTransform>();
templateRect.anchorMin = new Vector2(0f, 0f);
templateRect.anchorMax = new Vector2(1f, 0f);
templateRect.pivot = new Vector2(0.5f, 1f);
templateRect.sizeDelta = new Vector2(0f, 180f);
templateRect.anchoredPosition = new Vector2(0f, 2f);
var scrollRect = template.AddComponent<ScrollRect>();
scrollRect.horizontal = false;
var viewport = Panel(template.transform, "Viewport", new Color(1f, 1f, 1f, 0.98f));
viewport.AddComponent<Mask>().showMaskGraphic = false;
Stretch(viewport.GetComponent<RectTransform>());
var contentGo = Create("Content", viewport.transform);
var contentRect = contentGo.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0f, 1f);
contentRect.anchorMax = Vector2.one;
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.offsetMin = Vector2.zero;
contentRect.offsetMax = Vector2.zero;
contentGo.AddComponent<VerticalLayoutGroup>().childForceExpandWidth = true;
contentGo.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var itemGo = Create("Item", contentGo.transform);
var itemToggle = itemGo.AddComponent<Toggle>();
itemGo.AddComponent<LayoutElement>().preferredHeight = 36;
var itemBackground = itemGo.AddComponent<Image>();
itemBackground.color = Color.white;
itemToggle.targetGraphic = itemBackground;
var checkmark = Text(itemGo.transform, "✓", 18, TextAnchor.MiddleCenter);
var checkRect = checkmark.GetComponent<RectTransform>();
checkRect.anchorMin = new Vector2(0f, 0f);
checkRect.anchorMax = new Vector2(0f, 1f);
checkRect.pivot = new Vector2(0f, 0.5f);
checkRect.sizeDelta = new Vector2(28f, 0f);
checkRect.anchoredPosition = Vector2.zero;
itemToggle.graphic = checkmark;
var itemLabel = Text(itemGo.transform, "Option", 18, TextAnchor.MiddleLeft);
var itemLabelRect = itemLabel.GetComponent<RectTransform>();
Stretch(itemLabelRect);
itemLabelRect.offsetMin = new Vector2(32f, 0f);
itemLabelRect.offsetMax = new Vector2(-8f, 0f);
scrollRect.viewport = viewport.GetComponent<RectTransform>();
scrollRect.content = contentRect;
itemText = itemLabel;
return templateRect;
}
private ScrollRect ScrollView(Transform parent, out Transform content)
{
var root = Panel(parent, "ScrollView", new Color(0.12f, 0.13f, 0.15f));
var scrollRect = root.AddComponent<ScrollRect>();
scrollRect.horizontal = false;
root.AddComponent<LayoutElement>().preferredHeight = 160;
var viewport = Panel(root.transform, "Viewport", new Color(0.12f, 0.13f, 0.15f));
viewport.AddComponent<Mask>().showMaskGraphic = false;
Stretch(viewport.GetComponent<RectTransform>());
var contentGo = Panel(viewport.transform, "Content", new Color(0.12f, 0.13f, 0.15f));
content = contentGo.transform;
var contentRect = contentGo.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1f);
contentRect.offsetMin = new Vector2(8, 0);
contentRect.offsetMax = new Vector2(-8, 0);
contentGo.AddComponent<VerticalLayoutGroup>().spacing = 6;
contentGo.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
scrollRect.viewport = viewport.GetComponent<RectTransform>();
scrollRect.content = contentRect;
return scrollRect;
}
private Text Text(Transform parent, string value, int size, TextAnchor alignment)
{
var go = Create("Text", parent);
var text = go.AddComponent<Text>();
text.font = m_font;
text.fontSize = size;
text.color = Color.black;
text.alignment = alignment;
text.text = value;
text.horizontalOverflow = HorizontalWrapMode.Wrap;
text.verticalOverflow = VerticalWrapMode.Overflow;
return text;
}
private static GameObject Create(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
return go;
}
private static void Stretch(RectTransform rect)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a5af54f3c89457a8e08e47e0b46c1c1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,200 @@
using System.Collections;
using System.Collections.Generic;
using System;
using UnityEngine;
using UnityEngine.UI;
#if UNITY_ANDROID
using UnityEngine.Android;
#endif
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
{
/// <summary>
/// MVP 摄像头预览,使用 WebCamTexture。
/// </summary>
public sealed class FaceDetectionDemoCamera : MonoBehaviour
{
[SerializeField] private RawImage m_preview; // 预览图。
[SerializeField] private AspectRatioFitter m_aspectFitter; // 宽高比适配。
private WebCamTexture m_texture; // 当前摄像头纹理。
private FaceDetectionDemoLogger m_logger; // 日志。
private WebCamDevice[] m_devices; // 当前摄像头设备列表。
public bool IsOpen => m_texture != null && m_texture.isPlaying;
/// <summary>
/// 绑定 UI 和日志。
/// </summary>
public void Bind(RawImage preview, AspectRatioFitter aspectFitter, FaceDetectionDemoLogger logger)
{
m_preview = preview;
m_aspectFitter = aspectFitter;
m_logger = logger;
}
/// <summary>
/// 枚举摄像头名称。
/// </summary>
public string[] GetDeviceNames()
{
m_devices = WebCamTexture.devices;
var names = new List<string>();
for (var i = 0; i < m_devices.Length; i++)
{
names.Add(i + ": " + m_devices[i].name);
m_logger?.Info("摄像头[" + i + "] " + m_devices[i].name + ",前置=" + m_devices[i].isFrontFacing + ",自动对焦=" + m_devices[i].isAutoFocusPointSupported);
}
return names.ToArray();
}
/// <summary>
/// 请求权限后枚举摄像头。
/// </summary>
public void GetDeviceNamesWhenAuthorized(Action<string[]> callback)
{
StartCoroutine(GetDeviceNamesWhenAuthorizedRoutine(callback));
}
/// <summary>
/// 打开摄像头。
/// </summary>
public void Open(int deviceIndex)
{
Close();
if (m_devices == null || m_devices.Length == 0)
{
m_logger?.Error("没有可用摄像头");
return;
}
if (deviceIndex < 0 || deviceIndex >= m_devices.Length)
{
m_logger?.Error("摄像头索引无效: " + deviceIndex);
return;
}
StartCoroutine(OpenWhenAuthorized(deviceIndex));
}
private IEnumerator OpenWhenAuthorized(int deviceIndex)
{
yield return EnsureCameraAuthorization();
if (!HasCameraAuthorization())
{
m_logger?.Error("摄像头权限未授权");
yield break;
}
var deviceName = m_devices[deviceIndex].name;
m_texture = new WebCamTexture(deviceName, 1280, 720, 30);
m_texture.Play();
if (m_preview != null)
{
m_preview.texture = m_texture;
}
m_logger?.Info("摄像头已打开: " + deviceName);
}
private IEnumerator GetDeviceNamesWhenAuthorizedRoutine(Action<string[]> callback)
{
Close();
yield return EnsureCameraAuthorization();
if (!HasCameraAuthorization())
{
m_logger?.Error("摄像头权限未授权,无法枚举摄像头");
callback?.Invoke(new string[0]);
yield break;
}
string[] names = null;
for (var i = 0; i < 5; i++)
{
names = GetDeviceNames();
if (names.Length > 0)
{
break;
}
yield return new WaitForSeconds(0.3f);
}
callback?.Invoke(names ?? new string[0]);
}
private IEnumerator EnsureCameraAuthorization()
{
#if UNITY_ANDROID && !UNITY_EDITOR
if (!Permission.HasUserAuthorizedPermission(Permission.Camera))
{
m_logger?.Info("正在请求摄像头权限");
Permission.RequestUserPermission(Permission.Camera);
var timeout = 10f;
while (!Permission.HasUserAuthorizedPermission(Permission.Camera) && timeout > 0f)
{
timeout -= Time.deltaTime;
yield return null;
}
}
#else
yield return null;
#endif
}
private static bool HasCameraAuthorization()
{
#if UNITY_ANDROID && !UNITY_EDITOR
return Permission.HasUserAuthorizedPermission(Permission.Camera);
#else
return true;
#endif
}
/// <summary>
/// 关闭摄像头。
/// </summary>
public void Close()
{
if (m_texture != null)
{
if (m_texture.isPlaying)
{
m_texture.Stop();
}
Destroy(m_texture);
m_texture = null;
}
if (m_preview != null)
{
m_preview.texture = null;
}
}
private void Update()
{
if (m_texture == null || m_aspectFitter == null || m_texture.width <= 16 || m_texture.height <= 16)
{
return;
}
m_aspectFitter.aspectRatio = (float)m_texture.width / m_texture.height;
if (m_preview != null)
{
m_preview.rectTransform.localEulerAngles = new Vector3(0f, 0f, -m_texture.videoRotationAngle);
m_preview.uvRect = m_texture.videoVerticallyMirrored ? new Rect(0f, 1f, 1f, -1f) : new Rect(0f, 0f, 1f, 1f);
}
}
private void OnDestroy()
{
Close();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1c3d5d5322f2496d9f9867220dfbc8ea
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,546 @@
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;
}
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.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 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)
{
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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e34e58a5d9143f09af4602997294a2f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,79 @@
using System;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
{
/// <summary>
/// MVP 操作日志窗口。
/// </summary>
public sealed class FaceDetectionDemoLogger : MonoBehaviour
{
[SerializeField] private Text m_logText; // 日志文本。
[SerializeField] private ScrollRect m_scrollRect; // 日志滚动视图。
private readonly StringBuilder m_buffer = new StringBuilder(8192); // 日志缓存。
/// <summary>
/// 绑定 UI。
/// </summary>
public void Bind(Text logText, ScrollRect scrollRect)
{
m_logText = logText;
m_scrollRect = scrollRect;
}
/// <summary>
/// 写入普通日志。
/// </summary>
public void Info(string message)
{
Append("INFO", message);
}
/// <summary>
/// 写入错误日志。
/// </summary>
public void Error(string message)
{
Append("ERROR", message);
}
/// <summary>
/// 清空日志。
/// </summary>
public void Clear()
{
m_buffer.Length = 0;
Refresh();
}
private void Append(string level, string message)
{
m_buffer.Append('[')
.Append(DateTime.Now.ToString("HH:mm:ss"))
.Append("] ")
.Append(level)
.Append(" ")
.AppendLine(message ?? string.Empty);
Refresh();
}
private void Refresh()
{
if (m_logText != null)
{
m_logText.text = m_buffer.ToString();
}
if (m_scrollRect != null)
{
Canvas.ForceUpdateCanvases();
m_scrollRect.verticalNormalizedPosition = 0f;
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 93c5f598b0a34ae490496363c31f03ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using Aisu.SIM1.Face.Models;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.Samples.FaceDetectionDemo
{
/// <summary>
/// MVP 用户列表窗口。
/// </summary>
public sealed class FaceDetectionDemoUserList : MonoBehaviour
{
[SerializeField] private Transform m_content; // 列表容器。
[SerializeField] private Font m_font; // UI 字体。
private Action<int> m_deleteHandler; // 删除回调。
/// <summary>
/// 绑定 UI。
/// </summary>
public void Bind(Transform content, Font font, Action<int> deleteHandler)
{
m_content = content;
m_font = font;
m_deleteHandler = deleteHandler;
}
/// <summary>
/// 刷新用户列表。
/// </summary>
public void SetUsers(IList<FaceUserInfo> users)
{
Clear();
if (users == null || users.Count == 0)
{
AddText("暂无用户数据");
return;
}
for (var i = 0; i < users.Count; i++)
{
AddRow(users[i]);
}
}
/// <summary>
/// 清空列表。
/// </summary>
public void Clear()
{
if (m_content == null)
{
return;
}
for (var i = m_content.childCount - 1; i >= 0; i--)
{
Destroy(m_content.GetChild(i).gameObject);
}
}
private void AddRow(FaceUserInfo user)
{
var row = new GameObject("User_" + user.UserId, typeof(RectTransform), typeof(HorizontalLayoutGroup), typeof(Image));
row.transform.SetParent(m_content, false);
row.GetComponent<Image>().color = new Color(0.94f, 0.96f, 0.98f);
var layout = row.GetComponent<HorizontalLayoutGroup>();
layout.padding = new RectOffset(8, 8, 6, 6);
layout.spacing = 8;
layout.childControlWidth = true;
layout.childForceExpandWidth = false;
row.AddComponent<LayoutElement>().preferredHeight = 44;
AddText(row.transform, $"ID: {user.UserId} 姓名: {user.UserName} 管理员: {user.IsAdmin}", 1f);
AddButton(row.transform, "删除", () => m_deleteHandler?.Invoke(user.UserId));
}
private void AddText(string text)
{
AddText(m_content, text, 1f);
}
private Text AddText(Transform parent, string text, float flexibleWidth)
{
var go = new GameObject("Text", typeof(RectTransform), typeof(Text));
go.transform.SetParent(parent, false);
var label = go.GetComponent<Text>();
label.font = m_font;
label.fontSize = 22;
label.color = Color.black;
label.alignment = TextAnchor.MiddleLeft;
label.text = text;
go.AddComponent<LayoutElement>().flexibleWidth = flexibleWidth;
return label;
}
private void AddButton(Transform parent, string text, UnityEngine.Events.UnityAction action)
{
var go = new GameObject(text, typeof(RectTransform), typeof(Image), typeof(Button));
go.transform.SetParent(parent, false);
go.GetComponent<Image>().color = new Color(0.82f, 0.88f, 0.95f);
var button = go.GetComponent<Button>();
button.onClick.AddListener(action);
var layout = go.AddComponent<LayoutElement>();
layout.preferredWidth = 92;
layout.preferredHeight = 34;
var labelGo = new GameObject("Text", typeof(RectTransform), typeof(Text));
labelGo.transform.SetParent(go.transform, false);
var rect = labelGo.GetComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
var label = labelGo.GetComponent<Text>();
label.font = m_font;
label.fontSize = 20;
label.color = Color.black;
label.alignment = TextAnchor.MiddleCenter;
label.text = text;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a64d11f829c74808af47ccb103389f36
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 34ad6ee935bce014d8614b88076050bd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
{
"name": "Aisu.SIM1.Face.Samples",
"rootNamespace": "Aisu.SIM1.Face.Samples",
"references": [
"Aisu.SIM1.Face.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9fa219c8c2a74b94a983c6d4a6a1d001
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4ac476647a08a364289fa596d310bf3a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,19 @@
{
"name": "Aisu.SIM1.Face.Samples.Editor",
"rootNamespace": "Aisu.SIM1.Face.Editor",
"references": [
"Aisu.SIM1.Face.Runtime",
"Aisu.SIM1.Face.Samples"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 73502db286784cc3ac4a9e71f06f1002
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,39 @@
#if UNITY_EDITOR
using System.IO;
using Aisu.SIM1.Face.Core;
using Aisu.SIM1.Face.UI.Common;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
namespace Aisu.SIM1.Face.Editor
{
/// <summary>
/// 生成 SI-M1 全功能 Demo 场景。
/// </summary>
public static class FaceModuleDemoSceneGenerator
{
private const string ScenePath = "Assets/SI-M1 Face Demo/Scenes/FaceModuleDemo.unity"; // Demo 场景保存路径。
/// <summary>
/// 创建并保存 Demo 场景。
/// </summary>
[MenuItem("AISU/SI-M1 Face SDK/Regenerate Imported Demo Scene")]
public static void Generate()
{
Directory.CreateDirectory(Path.GetDirectoryName(ScenePath));
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
var managerGo = new GameObject("FaceModuleManager");
managerGo.AddComponent<FaceModuleManager>();
var bootstrapGo = new GameObject("FaceModuleDemoBootstrap");
bootstrapGo.AddComponent<FaceModuleDemoBootstrap>();
EditorSceneManager.SaveScene(scene, ScenePath);
AssetDatabase.Refresh();
Selection.activeObject = bootstrapGo;
Debug.Log("FaceModule demo scene generated: " + ScenePath);
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 230f42ee40b783c4784659a677f2fc59
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5e2eeeccfb35f134ba7a1a0b29dfb214
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,227 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!29 &1
OcclusionCullingSettings:
m_ObjectHideFlags: 0
serializedVersion: 2
m_OcclusionBakeSettings:
smallestOccluder: 5
smallestHole: 0.25
backfaceThreshold: 100
m_SceneGUID: 00000000000000000000000000000000
m_OcclusionCullingData: {fileID: 0}
--- !u!104 &2
RenderSettings:
m_ObjectHideFlags: 0
serializedVersion: 9
m_Fog: 0
m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1}
m_FogMode: 3
m_FogDensity: 0.01
m_LinearFogStart: 0
m_LinearFogEnd: 300
m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1}
m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1}
m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1}
m_AmbientIntensity: 1
m_AmbientMode: 0
m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1}
m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0}
m_HaloStrength: 0.5
m_FlareStrength: 1
m_FlareFadeSpeed: 3
m_HaloTexture: {fileID: 0}
m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0}
m_DefaultReflectionMode: 0
m_DefaultReflectionResolution: 128
m_ReflectionBounces: 1
m_ReflectionIntensity: 1
m_CustomReflection: {fileID: 0}
m_Sun: {fileID: 0}
m_IndirectSpecularColor: {r: 0.12731749, g: 0.13414757, b: 0.1210787, a: 1}
m_UseRadianceAmbientProbe: 0
--- !u!157 &3
LightmapSettings:
m_ObjectHideFlags: 0
serializedVersion: 12
m_GIWorkflowMode: 1
m_GISettings:
serializedVersion: 2
m_BounceScale: 1
m_IndirectOutputScale: 1
m_AlbedoBoost: 1
m_EnvironmentLightingMode: 0
m_EnableBakedLightmaps: 1
m_EnableRealtimeLightmaps: 0
m_LightmapEditorSettings:
serializedVersion: 12
m_Resolution: 2
m_BakeResolution: 40
m_AtlasSize: 1024
m_AO: 0
m_AOMaxDistance: 1
m_CompAOExponent: 1
m_CompAOExponentDirect: 0
m_ExtractAmbientOcclusion: 0
m_Padding: 2
m_LightmapParameters: {fileID: 0}
m_LightmapsBakeMode: 1
m_TextureCompression: 1
m_FinalGather: 0
m_FinalGatherFiltering: 1
m_FinalGatherRayCount: 256
m_ReflectionCompression: 2
m_MixedBakeMode: 2
m_BakeBackend: 1
m_PVRSampling: 1
m_PVRDirectSampleCount: 32
m_PVRSampleCount: 512
m_PVRBounces: 2
m_PVREnvironmentSampleCount: 256
m_PVREnvironmentReferencePointCount: 2048
m_PVRFilteringMode: 1
m_PVRDenoiserTypeDirect: 1
m_PVRDenoiserTypeIndirect: 1
m_PVRDenoiserTypeAO: 1
m_PVRFilterTypeDirect: 0
m_PVRFilterTypeIndirect: 0
m_PVRFilterTypeAO: 0
m_PVREnvironmentMIS: 1
m_PVRCulling: 1
m_PVRFilteringGaussRadiusDirect: 1
m_PVRFilteringGaussRadiusIndirect: 5
m_PVRFilteringGaussRadiusAO: 2
m_PVRFilteringAtrousPositionSigmaDirect: 0.5
m_PVRFilteringAtrousPositionSigmaIndirect: 2
m_PVRFilteringAtrousPositionSigmaAO: 1
m_ExportTrainingData: 0
m_TrainingDataDestination: TrainingData
m_LightProbeSampleCountMultiplier: 4
m_LightingDataAsset: {fileID: 0}
m_LightingSettings: {fileID: 0}
--- !u!196 &4
NavMeshSettings:
serializedVersion: 2
m_ObjectHideFlags: 0
m_BuildSettings:
serializedVersion: 2
agentTypeID: 0
agentRadius: 0.5
agentHeight: 2
agentSlope: 45
agentClimb: 0.4
ledgeDropHeight: 0
maxJumpAcrossDistance: 0
minRegionArea: 2
manualCellSize: 0
cellSize: 0.16666667
manualTileSize: 0
tileSize: 256
accuratePlacement: 0
maxJobWorkers: 0
preserveTilesOutsideBounds: 0
debug:
m_Flags: 0
m_NavMeshData: {fileID: 0}
--- !u!1 &240313580
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 240313582}
- component: {fileID: 240313581}
m_Layer: 0
m_Name: FaceModuleManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &240313581
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 240313580}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1c1d09a652384f148b76655d1acdd2ea, type: 3}
m_Name:
m_EditorClassIdentifier:
m_config:
TransportMode: 0
UseMockInEditor: 1
PortName: COM3
BaudRate: 115200
ReadBufferSize: 4096
UseEditorSerialDebugParameters: 1
EditorPortName: COM3
EditorBaudRate: 115200
EditorReadBufferSize: 4096
MockResponseDelayFrames: 1
RawLogEnabled: 1
m_settings: {fileID: 0}
m_openOnStart: 0
--- !u!4 &240313582
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 240313580}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 0
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &948012342
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 948012344}
- component: {fileID: 948012343}
m_Layer: 0
m_Name: FaceModuleDemoBootstrap
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &948012343
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948012342}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 9b9f4bf81dc7448f90bd6c85e9fe4519, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!4 &948012344
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 948012342}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_RootOrder: 1
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 89768ad84552b5c4d8e5b584c216af96
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3a1a87e4b672fa4094e73f8b45ee1c0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e8643660e200f554192fa67ef682dd1a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3dd02cac73721224e87791793b4aeb56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2e1ab66bdb929164dbce2d25e3992716
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7382f0bef5512b428ee4431880a6b1b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,173 @@
using Aisu.SIM1.Face.Core;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.Common
{
/// <summary>
/// Demo 场景运行时构建器。
/// </summary>
public sealed class FaceModuleDemoBootstrap : MonoBehaviour
{
#region Unity API
/// <summary>
/// 执行 Awake 方法。
/// </summary>
private void Awake()
{
Build();
}
#endregion
/// <summary>
/// 构建完整 UGUI 页面。
/// </summary>
public void Build()
{
if (FindObjectOfType<EventSystem>() == null)
{
var eventSystem = new GameObject("EventSystem");
eventSystem.AddComponent<EventSystem>();
eventSystem.AddComponent<StandaloneInputModule>();
}
var font = Resources.GetBuiltinResource<Font>("Arial.ttf");
var builder = new FaceModuleUiBuilder(font);
var manager = FindObjectOfType<FaceModuleManager>() ?? new GameObject("FaceModuleManager").AddComponent<FaceModuleManager>();
var canvasGo = FaceModuleUiBuilder.Create("Canvas", transform);
var canvas = canvasGo.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvasGo.AddComponent<GraphicRaycaster>();
var scaler = canvasGo.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
var safe = FaceModuleUiBuilder.Create("SafeArea", canvasGo.transform);
FaceModuleUiBuilder.Stretch(safe.GetComponent<RectTransform>());
var safeLayout = safe.AddComponent<VerticalLayoutGroup>();
safeLayout.padding = new RectOffset(16, 16, 16, 16);
safeLayout.spacing = 12;
safeLayout.childControlWidth = true;
safeLayout.childForceExpandWidth = true;
var top = FaceModuleUiBuilder.Create("TopStatusBar", safe.transform);
top.AddComponent<Image>().color = new Color(0.9f, 0.94f, 0.97f);
var topLayoutElement = top.AddComponent<LayoutElement>();
topLayoutElement.preferredHeight = 54;
var topLayout = top.AddComponent<HorizontalLayoutGroup>();
topLayout.padding = new RectOffset(12, 12, 8, 8);
topLayout.spacing = 20;
var statusBar = top.AddComponent<FaceModuleStatusBar>();
statusBar.Bind(
builder.Text(top.transform, "未连接", 20),
builder.Text(top.transform, "未 READY", 20),
builder.Text(top.transform, "状态: Invalid", 20),
builder.Text(top.transform, "命令:", 20),
builder.Text(top.transform, "错误:", 20));
var body = FaceModuleUiBuilder.Create("Body", safe.transform);
var bodyLayoutElement = body.AddComponent<LayoutElement>();
bodyLayoutElement.flexibleHeight = 1;
var bodyLayout = body.AddComponent<HorizontalLayoutGroup>();
bodyLayout.spacing = 12;
bodyLayout.childControlHeight = true;
bodyLayout.childForceExpandHeight = true;
var navigation = FaceModuleUiBuilder.Create("NavigationPanel", body.transform).AddComponent<FaceModuleNavigationPanel>();
navigation.gameObject.AddComponent<Image>().color = new Color(0.86f, 0.9f, 0.94f);
var navLayoutElement = navigation.gameObject.AddComponent<LayoutElement>();
navLayoutElement.preferredWidth = 240;
var navLayout = navigation.gameObject.AddComponent<VerticalLayoutGroup>();
navLayout.padding = new RectOffset(10, 10, 10, 10);
navLayout.spacing = 8;
var pageContainer = FaceModuleUiBuilder.Create("PageContainer", body.transform);
pageContainer.AddComponent<Image>().color = new Color(0.98f, 0.99f, 1f);
pageContainer.AddComponent<LayoutElement>().flexibleWidth = 1;
var bottom = FaceModuleUiBuilder.Create("BottomLogPanel", safe.transform);
bottom.AddComponent<Image>().color = new Color(0.08f, 0.09f, 0.1f);
bottom.AddComponent<LayoutElement>().preferredHeight = 220;
var bottomLayout = bottom.AddComponent<VerticalLayoutGroup>();
bottomLayout.padding = new RectOffset(8, 8, 8, 8);
bottomLayout.spacing = 6;
var logPanel = bottom.AddComponent<FaceModuleLogPanel>();
var autoScroll = builder.Toggle(bottom.transform, "自动滚动", true);
var scroll = builder.ScrollView(bottom.transform, out var logText);
logText.color = Color.white;
logPanel.Bind(logText, scroll, autoScroll);
var logButtons = builder.Row(bottom.transform);
builder.Button(logButtons.transform, "清空日志", logPanel.Clear);
builder.Button(logButtons.transform, "保存日志", logPanel.Save);
var loadingGo = FaceModuleUiBuilder.Create("LoadingView", canvasGo.transform);
loadingGo.AddComponent<Image>().color = new Color(0, 0, 0, 0.25f);
FaceModuleUiBuilder.Stretch(loadingGo.GetComponent<RectTransform>());
var loadingLayout = loadingGo.AddComponent<VerticalLayoutGroup>();
loadingLayout.childAlignment = TextAnchor.MiddleCenter;
var loading = loadingGo.AddComponent<FaceModuleLoadingView>();
loading.Bind(builder.Text(loadingGo.transform, "处理中...", 28, TextAnchor.MiddleCenter), builder.Slider(loadingGo.transform));
var dialogGo = FaceModuleUiBuilder.Create("ConfirmDialog", canvasGo.transform);
dialogGo.AddComponent<Image>().color = new Color(0, 0, 0, 0.35f);
FaceModuleUiBuilder.Stretch(dialogGo.GetComponent<RectTransform>());
var dialogLayout = dialogGo.AddComponent<VerticalLayoutGroup>();
dialogLayout.childAlignment = TextAnchor.MiddleCenter;
dialogLayout.spacing = 12;
var dialog = dialogGo.AddComponent<FaceModuleConfirmDialog>();
var dialogText = builder.Text(dialogGo.transform, "确认操作?", 26, TextAnchor.MiddleCenter);
var dialogButtons = builder.Row(dialogGo.transform);
var confirmButton = builder.Button(dialogButtons.transform, "确认", () => { });
var cancelButton = builder.Button(dialogButtons.transform, "取消", () => { });
dialog.Bind(dialogText, confirmButton, cancelButton);
var mainPanel = canvasGo.AddComponent<FaceModuleMainPanel>();
mainPanel.Bind(statusBar, navigation, logPanel, loading);
BuildPages(pageContainer.transform, builder, mainPanel);
mainPanel.Init(manager);
mainPanel.ShowPage("Connect");
}
/// <summary>
/// 构建 Demo 功能页面。
/// </summary>
private void BuildPages(Transform pageContainer, FaceModuleUiBuilder builder, FaceModuleMainPanel mainPanel)
{
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceConnectPage>(pageContainer, builder, mainPanel, "Connect", "连接");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceProtocolDebugPage>(pageContainer, builder, mainPanel, "ProtocolDebug", "协议调试");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceVerifyPage>(pageContainer, builder, mainPanel, "Verify", "人脸验证");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceEnrollPage>(pageContainer, builder, mainPanel, "Enroll", "人脸录入");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceUserManagePage>(pageContainer, builder, mainPanel, "UserManage", "用户管理");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceQrCodePage>(pageContainer, builder, mainPanel, "QrCode", "二维码");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceImagePage>(pageContainer, builder, mainPanel, "Image", "图片抓拍");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FacePhotoEnrollPage>(pageContainer, builder, mainPanel, "PhotoEnroll", "照片注册");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceFeaturePage>(pageContainer, builder, mainPanel, "Feature", "特征管理");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceUvcSettingsPage>(pageContainer, builder, mainPanel, "Uvc", "UVC 设置");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceAdvancedSettingsPage>(pageContainer, builder, mainPanel, "Advanced", "高级设置");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceEncryptionPage>(pageContainer, builder, mainPanel, "Encryption", "加密");
AddPage<global::Aisu.SIM1.Face.UI.Pages.FaceFirmwarePage>(pageContainer, builder, mainPanel, "Firmware", "固件升级");
}
/// <summary>
/// 添加 Demo 功能页面。
/// </summary>
private void AddPage<T>(Transform parent, FaceModuleUiBuilder builder, FaceModuleMainPanel mainPanel, string pageName, string label) where T : FaceModulePageBase
{
var pageGo = FaceModuleUiBuilder.Create(pageName + "Page", parent);
FaceModuleUiBuilder.Stretch(pageGo.GetComponent<RectTransform>());
var layout = pageGo.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(16, 16, 16, 16);
layout.spacing = 10;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
var page = pageGo.AddComponent<T>();
page.Build(builder);
mainPanel.RegisterPage(pageName, label, page, builder);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 55824872760a6554ba347ca37e222eff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,927 @@
using System;
using System.Collections.Generic;
using System.IO;
using Aisu.SIM1.Face.Core;
using Aisu.SIM1.Face.Protocol;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.Common
{
/// <summary>
/// FaceModule UI 公共状态。
/// </summary>
public sealed class FaceModuleUiState
{
// 公开数据字段。
public bool IsBusy;
// 公开数据字段。
public string CurrentPage;
// 公开数据字段。
public string LastResult;
// 公开数据字段。
public float Progress;
}
/// <summary>
/// 所有功能页面的基类。
/// </summary>
public abstract class FaceModulePageBase : MonoBehaviour
{
// 内部状态字段。
protected FaceModuleManager m_manager;
// 内部状态字段。
protected FaceModuleLogPanel m_logPanel;
// 内部状态字段。
protected FaceModuleLoadingView m_loadingView;
// 内部状态字段。
protected Text m_resultText;
/// <summary>
/// 初始化页面。
/// </summary>
public virtual void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
m_manager = manager;
m_logPanel = logPanel;
m_loadingView = loadingView;
}
/// <summary>
/// 页面显示。
/// </summary>
public virtual void Show()
{
gameObject.SetActive(true);
}
/// <summary>
/// 页面隐藏。
/// </summary>
public virtual void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 由代码生成页面内容。
/// </summary>
public virtual void Build(FaceModuleUiBuilder builder)
{
}
/// <summary>
/// 执行 RequireConnected 方法。
/// </summary>
protected bool RequireConnected()
{
if (m_manager != null && m_manager.IsOpen)
{
return true;
}
SetResult("模组未连接");
return false;
}
/// <summary>
/// 执行 RequireReady 方法。
/// </summary>
protected bool RequireReady()
{
if (RequireConnected() && m_manager.IsReady)
{
return true;
}
SetResult("模组未 READY");
return false;
}
/// <summary>
/// 执行 SetResult 方法。
/// </summary>
protected void SetResult(string message)
{
if (m_resultText != null)
{
m_resultText.text = message;
}
m_logPanel?.Info(message);
}
/// <summary>
/// 执行 SetError 方法。
/// </summary>
protected void SetError(Exception exception)
{
SetResult(exception.Message);
m_logPanel?.Error(exception.Message);
Debug.LogException(exception);
}
/// <summary>
/// 执行 ReadByte 方法。
/// </summary>
protected byte ReadByte(InputField input, byte fallback)
{
return input != null && byte.TryParse(input.text, out var value) ? value : fallback;
}
/// <summary>
/// 执行 ReadInt 方法。
/// </summary>
protected int ReadInt(InputField input, int fallback)
{
return input != null && int.TryParse(input.text, out var value) ? value : fallback;
}
/// <summary>
/// 执行 ReadText 方法。
/// </summary>
protected string ReadText(InputField input)
{
return input != null ? input.text : string.Empty;
}
/// <summary>
/// 执行 RunWithButton 方法。
/// </summary>
protected void RunWithButton(Button button, Func<System.Threading.Tasks.Task> action)
{
if (button != null)
{
button.interactable = false;
}
RunAsync(async () =>
{
try
{
await action();
}
finally
{
if (button != null)
{
button.interactable = true;
}
}
});
}
/// <summary>
/// 执行 RunAsync 方法。
/// </summary>
protected async void RunAsync(Func<System.Threading.Tasks.Task> action)
{
try
{
m_loadingView?.Show("处理中...");
await action();
}
catch (Exception ex)
{
SetError(ex);
}
finally
{
m_loadingView?.Hide();
}
}
}
/// <summary>
/// 顶部状态栏。
/// </summary>
public sealed class FaceModuleStatusBar : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_connectionText;
// Inspector 绑定字段。
[SerializeField] private Text m_readyText;
// Inspector 绑定字段。
[SerializeField] private Text m_statusText;
// Inspector 绑定字段。
[SerializeField] private Text m_commandText;
// Inspector 绑定字段。
[SerializeField] private Text m_errorText;
// 内部状态字段。
private FaceModuleManager m_manager;
private Action<FaceResultCode, string> m_errorHandler; // Manager 错误事件回调。
#region Unity API
/// <summary>
/// 执行 Update 方法。
/// </summary>
private void Update()
{
Refresh();
}
#endregion
/// <summary>
/// 初始化状态栏。
/// </summary>
public void Init(FaceModuleManager manager)
{
if (m_manager != null && m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
m_manager = manager;
if (m_manager != null)
{
m_errorHandler = (_, message) =>
{
if (m_errorText != null)
{
m_errorText.text = "错误: " + message;
}
};
m_manager.OnError += m_errorHandler;
}
Refresh();
}
private void OnDestroy()
{
if (m_manager != null && m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
}
/// <summary>
/// 绑定状态栏文本。
/// </summary>
public void Bind(Text connectionText, Text readyText, Text statusText, Text commandText, Text errorText)
{
m_connectionText = connectionText;
m_readyText = readyText;
m_statusText = statusText;
m_commandText = commandText;
m_errorText = errorText;
}
/// <summary>
/// 刷新显示。
/// </summary>
public void Refresh()
{
if (m_manager == null)
{
return;
}
if (m_connectionText != null) m_connectionText.text = m_manager.IsReady ? "硬件已连接" : (m_manager.IsOpen ? "端口已打开" : "未连接");
if (m_readyText != null) m_readyText.text = m_manager.IsReady ? "READY" : "未 READY";
if (m_statusText != null) m_statusText.text = "状态: " + m_manager.CurrentStatus;
if (m_commandText != null) m_commandText.text = "命令: " + m_manager.CurrentCommand;
if (m_errorText != null && !string.IsNullOrEmpty(m_manager.LastError)) m_errorText.text = "错误: " + m_manager.LastError;
}
}
/// <summary>
/// 左侧导航栏。
/// </summary>
public sealed class FaceModuleNavigationPanel : MonoBehaviour
{
private readonly List<Button> m_buttons = new List<Button>();
// 内部状态字段。
private FaceModuleMainPanel m_mainPanel;
/// <summary>
/// 初始化导航栏。
/// </summary>
public void Init(FaceModuleMainPanel mainPanel)
{
m_mainPanel = mainPanel;
}
/// <summary>
/// 添加页面导航按钮。
/// </summary>
public void AddNavigation(string pageName, string label, FaceModuleUiBuilder builder)
{
var button = builder.Button(transform, label, () => m_mainPanel.ShowPage(pageName));
m_buttons.Add(button);
}
}
/// <summary>
/// 底部日志面板。
/// </summary>
public sealed class FaceModuleLogPanel : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_logText;
// Inspector 绑定字段。
[SerializeField] private ScrollRect m_scrollRect;
// Inspector 绑定字段。
[SerializeField] private Toggle m_autoScrollToggle;
private readonly List<string> m_lines = new List<string>();
private FaceModuleManager m_manager; // 当前绑定的 Manager。
private Action<FaceResultCode, string> m_errorHandler; // Manager 错误事件回调。
private Action m_readyHandler; // Manager READY 事件回调。
/// <summary>
/// 绑定日志 UI。
/// </summary>
public void Bind(Text logText, ScrollRect scrollRect, Toggle autoScrollToggle)
{
m_logText = logText;
m_scrollRect = scrollRect;
m_autoScrollToggle = autoScrollToggle;
}
/// <summary>
/// 连接 Manager 日志事件。
/// </summary>
public void Init(FaceModuleManager manager)
{
UnbindManagerEvents();
m_manager = manager;
if (manager == null)
{
return;
}
m_errorHandler = (_, message) => Error(message);
m_readyHandler = () => Info("模组 READY");
manager.OnRawLog += Raw;
manager.OnError += m_errorHandler;
manager.OnReady += m_readyHandler;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>
/// 添加业务日志。
/// </summary>
public void Info(string message)
{
Append("INFO", message);
}
/// <summary>
/// 添加错误日志。
/// </summary>
public void Error(string message)
{
Append("ERROR", message);
}
/// <summary>
/// 添加原始收发日志。
/// </summary>
public void Raw(string message)
{
var tag = message.StartsWith("TX", StringComparison.OrdinalIgnoreCase) ? "TX" : "RX";
var body = message.Length > 3 ? message.Substring(3) : message;
Append(tag, body);
}
/// <summary>
/// 清空日志。
/// </summary>
public void Clear()
{
m_lines.Clear();
RefreshText();
}
/// <summary>
/// 保存日志到本地。
/// </summary>
public void Save()
{
var dir = Path.Combine(Application.persistentDataPath, "FaceModuleLogs");
Directory.CreateDirectory(dir);
File.WriteAllLines(Path.Combine(dir, DateTime.Now.ToString("yyyyMMdd_HHmmss") + ".log"), m_lines);
Info("日志已保存: " + dir);
}
/// <summary>
/// 执行 Append 方法。
/// </summary>
private void Append(string tag, string message)
{
m_lines.Add($"[{DateTime.Now:HH:mm:ss.fff}][{tag}] {message}");
if (m_lines.Count > 1000)
{
m_lines.RemoveAt(0);
}
RefreshText();
}
/// <summary>
/// 执行 RefreshText 方法。
/// </summary>
private void RefreshText()
{
if (m_logText != null)
{
m_logText.text = string.Join("\n", m_lines);
}
if (m_scrollRect != null && (m_autoScrollToggle == null || m_autoScrollToggle.isOn))
{
Canvas.ForceUpdateCanvases();
m_scrollRect.verticalNormalizedPosition = 0f;
}
}
private void UnbindManagerEvents()
{
if (m_manager == null)
{
return;
}
m_manager.OnRawLog -= Raw;
if (m_errorHandler != null)
{
m_manager.OnError -= m_errorHandler;
}
if (m_readyHandler != null)
{
m_manager.OnReady -= m_readyHandler;
}
}
}
/// <summary>
/// Loading / 进度提示。
/// </summary>
public sealed class FaceModuleLoadingView : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_messageText;
// Inspector 绑定字段。
[SerializeField] private Slider m_progressSlider;
/// <summary>
/// 绑定控件。
/// </summary>
public void Bind(Text messageText, Slider progressSlider)
{
m_messageText = messageText;
m_progressSlider = progressSlider;
Hide();
}
/// <summary>
/// 显示 Loading。
/// </summary>
public void Show(string message)
{
gameObject.SetActive(true);
if (m_messageText != null) m_messageText.text = message;
}
/// <summary>
/// 设置进度。
/// </summary>
public void SetProgress(float value)
{
if (m_progressSlider != null) m_progressSlider.value = Mathf.Clamp01(value);
}
/// <summary>
/// 隐藏 Loading。
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
}
/// <summary>
/// 二次确认弹窗。
/// </summary>
public sealed class FaceModuleConfirmDialog : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_messageText;
// Inspector 绑定字段。
[SerializeField] private Button m_confirmButton;
// Inspector 绑定字段。
[SerializeField] private Button m_cancelButton;
// 内部状态字段。
private Action m_onConfirm;
/// <summary>
/// 绑定控件。
/// </summary>
public void Bind(Text messageText, Button confirmButton, Button cancelButton)
{
m_messageText = messageText;
m_confirmButton = confirmButton;
m_cancelButton = cancelButton;
m_confirmButton.onClick.AddListener(Confirm);
m_cancelButton.onClick.AddListener(Hide);
Hide();
}
/// <summary>
/// 显示确认框。
/// </summary>
public void Show(string message, Action onConfirm)
{
m_onConfirm = onConfirm;
if (m_messageText != null) m_messageText.text = message;
gameObject.SetActive(true);
}
/// <summary>
/// 隐藏确认框。
/// </summary>
public void Hide()
{
gameObject.SetActive(false);
}
/// <summary>
/// 执行 Confirm 方法。
/// </summary>
private void Confirm()
{
Hide();
m_onConfirm?.Invoke();
}
}
/// <summary>
/// 运行时 UI 构建辅助器。
/// </summary>
public sealed class FaceModuleUiBuilder
{
// 内部状态字段。
private readonly Font m_font;
/// <summary>
/// 执行 FaceModuleUiBuilder 方法。
/// </summary>
public FaceModuleUiBuilder(Font font)
{
m_font = font ?? Resources.GetBuiltinResource<Font>("Arial.ttf");
}
/// <summary>
/// 执行 Text 方法。
/// </summary>
public Text Text(Transform parent, string text, int size = 22, TextAnchor anchor = TextAnchor.MiddleLeft)
{
var go = Create("Text", parent);
var component = go.AddComponent<Text>();
component.font = m_font;
component.text = text;
component.fontSize = size;
component.color = new Color(0.08f, 0.09f, 0.1f);
component.alignment = anchor;
component.horizontalOverflow = HorizontalWrapMode.Wrap;
component.verticalOverflow = VerticalWrapMode.Overflow;
return component;
}
/// <summary>
/// 执行 Button 方法。
/// </summary>
public Button Button(Transform parent, string label, UnityEngine.Events.UnityAction onClick)
{
var go = Create("Button_" + label, parent);
var image = go.AddComponent<Image>();
image.color = new Color(0.13f, 0.28f, 0.47f);
var button = go.AddComponent<Button>();
button.targetGraphic = image;
button.onClick.AddListener(onClick);
var text = Text(go.transform, label, 20, TextAnchor.MiddleCenter);
text.color = Color.white;
Stretch(text.rectTransform);
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 42;
layout.minHeight = 36;
return button;
}
/// <summary>
/// 执行 Input 方法。
/// </summary>
public InputField Input(Transform parent, string placeholder, string value = "")
{
var go = Create("Input_" + placeholder, parent);
var image = go.AddComponent<Image>();
image.color = Color.white;
var input = go.AddComponent<InputField>();
var text = Text(go.transform, value, 20);
text.color = Color.black;
Stretch(text.rectTransform, 10, 10, 4, 4);
input.textComponent = text;
var place = Text(go.transform, placeholder, 20);
place.color = new Color(0.45f, 0.48f, 0.52f);
Stretch(place.rectTransform, 10, 10, 4, 4);
input.placeholder = place;
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 38;
return input;
}
/// <summary>
/// 执行 Toggle 方法。
/// </summary>
public Toggle Toggle(Transform parent, string label, bool value)
{
var go = Create("Toggle_" + label, parent);
var toggle = go.AddComponent<Toggle>();
var row = go.AddComponent<HorizontalLayoutGroup>();
row.spacing = 8;
row.childControlWidth = false;
row.childControlHeight = true;
var box = Create("CheckmarkBox", go.transform);
var boxImage = box.AddComponent<Image>();
boxImage.color = Color.white;
var boxLayout = box.AddComponent<LayoutElement>();
boxLayout.preferredWidth = 28;
boxLayout.preferredHeight = 28;
var mark = Create("Checkmark", box.transform);
var markImage = mark.AddComponent<Image>();
markImage.color = new Color(0.1f, 0.5f, 0.25f);
Stretch(markImage.rectTransform, 5, 5, 5, 5);
toggle.targetGraphic = boxImage;
toggle.graphic = markImage;
toggle.isOn = value;
Text(go.transform, label, 20);
return toggle;
}
/// <summary>
/// 执行 Dropdown 方法。
/// </summary>
public Dropdown Dropdown(Transform parent, IList<string> options)
{
var go = Create("Dropdown", parent);
var image = go.AddComponent<Image>();
image.color = Color.white;
var dropdown = go.AddComponent<Dropdown>();
dropdown.captionText = Text(go.transform, options.Count > 0 ? options[0] : string.Empty, 20);
Stretch(dropdown.captionText.rectTransform, 10, 32, 4, 4);
BuildDropdownTemplate(dropdown, go.transform);
dropdown.options.Clear();
foreach (var option in options)
{
dropdown.options.Add(new Dropdown.OptionData(option));
}
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 38;
return dropdown;
}
private void BuildDropdownTemplate(Dropdown dropdown, Transform parent)
{
var template = Create("Template", parent);
template.SetActive(false);
var templateImage = template.AddComponent<Image>();
templateImage.color = Color.white;
var templateRect = template.GetComponent<RectTransform>();
templateRect.anchorMin = new Vector2(0, 0);
templateRect.anchorMax = new Vector2(1, 0);
templateRect.pivot = new Vector2(0.5f, 1);
templateRect.anchoredPosition = new Vector2(0, 2);
templateRect.sizeDelta = new Vector2(0, 180);
var scroll = template.AddComponent<ScrollRect>();
scroll.horizontal = false;
var viewport = Create("Viewport", template.transform);
viewport.AddComponent<Image>().color = Color.white;
var mask = viewport.AddComponent<Mask>();
mask.showMaskGraphic = false;
Stretch(viewport.GetComponent<RectTransform>());
var content = Create("Content", viewport.transform);
var contentRect = content.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentRect.anchoredPosition = Vector2.zero;
contentRect.sizeDelta = new Vector2(0, 0);
var contentLayout = content.AddComponent<VerticalLayoutGroup>();
contentLayout.childControlWidth = true;
contentLayout.childControlHeight = true;
contentLayout.childForceExpandWidth = true;
contentLayout.childForceExpandHeight = false;
content.AddComponent<ContentSizeFitter>().verticalFit = ContentSizeFitter.FitMode.PreferredSize;
var item = Create("Item", content.transform);
var itemToggle = item.AddComponent<Toggle>();
item.AddComponent<Image>().color = new Color(0.96f, 0.97f, 0.98f);
item.AddComponent<LayoutElement>().preferredHeight = 32;
var checkmark = Create("Item Checkmark", item.transform).AddComponent<Image>();
checkmark.color = new Color(0.1f, 0.5f, 0.25f);
var checkmarkRect = checkmark.rectTransform;
checkmarkRect.anchorMin = new Vector2(0, 0.5f);
checkmarkRect.anchorMax = new Vector2(0, 0.5f);
checkmarkRect.sizeDelta = new Vector2(18, 18);
checkmarkRect.anchoredPosition = new Vector2(14, 0);
var itemLabel = Text(item.transform, "Option", 18);
Stretch(itemLabel.rectTransform, 32, 8, 4, 4);
itemToggle.targetGraphic = item.GetComponent<Image>();
itemToggle.graphic = checkmark;
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.content = contentRect;
dropdown.template = templateRect;
dropdown.itemText = itemLabel;
}
/// <summary>
/// 执行 Slider 方法。
/// </summary>
public Slider Slider(Transform parent)
{
var go = Create("Slider", parent);
var slider = go.AddComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 1;
var bg = Create("Background", go.transform).AddComponent<Image>();
bg.color = new Color(0.8f, 0.82f, 0.85f);
Stretch(bg.rectTransform, 0, 0, 10, 10);
var fill = Create("Fill", go.transform).AddComponent<Image>();
fill.color = new Color(0.1f, 0.55f, 0.35f);
Stretch(fill.rectTransform, 0, 0, 10, 10);
slider.fillRect = fill.rectTransform;
var layout = go.AddComponent<LayoutElement>();
layout.preferredHeight = 28;
return slider;
}
/// <summary>
/// 执行 Section 方法。
/// </summary>
public GameObject Section(Transform parent, string title)
{
var section = Create("Section_" + title, parent);
var layout = section.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(12, 12, 10, 10);
layout.spacing = 8;
layout.childControlWidth = true;
layout.childForceExpandWidth = true;
section.AddComponent<Image>().color = new Color(0.94f, 0.96f, 0.98f);
Text(section.transform, title, 24);
return section;
}
/// <summary>
/// 执行 Row 方法。
/// </summary>
public GameObject Row(Transform parent)
{
var row = Create("Row", parent);
var layout = row.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 8;
layout.childControlHeight = true;
layout.childControlWidth = true;
return row;
}
/// <summary>
/// 执行 ScrollView 方法。
/// </summary>
public ScrollRect ScrollView(Transform parent, out Text contentText)
{
var root = Create("ScrollView", parent);
root.AddComponent<Image>().color = Color.white;
var viewport = Create("Viewport", root.transform);
var mask = viewport.AddComponent<Mask>();
mask.showMaskGraphic = false;
viewport.AddComponent<Image>().color = Color.white;
Stretch(viewport.GetComponent<RectTransform>());
var content = Create("Content", viewport.transform);
var contentRect = content.GetComponent<RectTransform>();
contentRect.anchorMin = new Vector2(0, 1);
contentRect.anchorMax = new Vector2(1, 1);
contentRect.pivot = new Vector2(0.5f, 1);
contentText = Text(content.transform, string.Empty, 18, TextAnchor.UpperLeft);
contentText.rectTransform.sizeDelta = new Vector2(0, 800);
Stretch(contentText.rectTransform);
var scroll = root.AddComponent<ScrollRect>();
scroll.viewport = viewport.GetComponent<RectTransform>();
scroll.content = contentRect;
scroll.horizontal = false;
var layout = root.AddComponent<LayoutElement>();
layout.preferredHeight = 180;
return scroll;
}
/// <summary>
/// 执行 Create 方法。
/// </summary>
public static GameObject Create(string name, Transform parent)
{
var go = new GameObject(name, typeof(RectTransform));
go.transform.SetParent(parent, false);
return go;
}
/// <summary>
/// 执行 Stretch 方法。
/// </summary>
public static void Stretch(RectTransform rect, float left = 0, float right = 0, float top = 0, float bottom = 0)
{
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = new Vector2(left, bottom);
rect.offsetMax = new Vector2(-right, -top);
}
}
/// <summary>
/// 主面板,负责页面初始化和切换。
/// </summary>
public sealed class FaceModuleMainPanel : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private FaceModuleManager m_manager;
// Inspector 绑定字段。
[SerializeField] private FaceModuleStatusBar m_statusBar;
// Inspector 绑定字段。
[SerializeField] private FaceModuleNavigationPanel m_navigationPanel;
// Inspector 绑定字段。
[SerializeField] private FaceModuleLogPanel m_logPanel;
// Inspector 绑定字段。
[SerializeField] private FaceModuleLoadingView m_loadingView;
private readonly Dictionary<string, FaceModulePageBase> m_pages = new Dictionary<string, FaceModulePageBase>();
/// <summary>
/// 初始化主面板。
/// </summary>
public void Init(FaceModuleManager manager)
{
m_manager = manager;
m_statusBar?.Init(m_manager);
m_logPanel?.Init(m_manager);
m_navigationPanel?.Init(this);
foreach (var pair in m_pages)
{
pair.Value.Init(m_manager, m_logPanel, m_loadingView);
pair.Value.Hide();
}
}
/// <summary>
/// 绑定公共组件。
/// </summary>
public void Bind(FaceModuleStatusBar statusBar, FaceModuleNavigationPanel navigationPanel, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
m_statusBar = statusBar;
m_navigationPanel = navigationPanel;
m_logPanel = logPanel;
m_loadingView = loadingView;
}
/// <summary>
/// 注册页面。
/// </summary>
public void RegisterPage(string pageName, string label, FaceModulePageBase page, FaceModuleUiBuilder builder)
{
m_pages[pageName] = page;
m_navigationPanel.AddNavigation(pageName, label, builder);
}
/// <summary>
/// 显示指定页面。
/// </summary>
public void ShowPage(string pageName)
{
foreach (var pair in m_pages)
{
if (pair.Key == pageName)
{
pair.Value.Show();
}
else
{
pair.Value.Hide();
}
}
m_logPanel?.Info("切换页面: " + pageName);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5590054c8f656004eb4838ef5bf27ab9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e3750291d4457c4439cae8c383d2aadd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,511 @@
using System;
using System.IO;
using Aisu.SIM1.Face.Models;
using Aisu.SIM1.Face.Protocol;
using Aisu.SIM1.Face.UI.Common;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.Pages
{
public sealed class FacePhotoEnrollPage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_filePathInput;
// 内部状态字段。
private Text m_fileSizeText;
// 内部状态字段。
private Dropdown m_bioTypeDropdown;
// 内部状态字段。
private InputField m_userNameInput;
// 内部状态字段。
private Toggle m_duplicateToggle;
// 内部状态字段。
private Toggle m_specifyUserIdToggle;
// 内部状态字段。
private InputField m_userIdInput;
// 内部状态字段。
private Slider m_progress;
// 内部状态字段。
private Text m_seqText;
// 内部状态字段。
private Text m_userIdText;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "照片 / 特征注册", 30);
m_filePathInput = builder.Input(transform, "文件路径", string.Empty);
var row = builder.Row(transform);
builder.Button(row.transform, "刷新文件信息", RefreshFileInfo);
builder.Button(row.transform, "使用 0xF7 注册", EnrollWithPhoto);
builder.Button(row.transform, "使用 0xD7 指定 ID 注册", EnrollWithPhotoAndId);
m_fileSizeText = builder.Text(transform, "文件大小: -");
m_bioTypeDropdown = builder.Dropdown(transform, new[] { "普通照片", "加密照片", "普通特征码", "压缩特征码" });
m_userNameInput = builder.Input(transform, "用户姓名", "User");
m_duplicateToggle = builder.Toggle(transform, "启用查重", true);
m_specifyUserIdToggle = builder.Toggle(transform, "指定 UserID", false);
m_userIdInput = builder.Input(transform, "指定 UserID", "1");
m_progress = builder.Slider(transform);
m_seqText = builder.Text(transform, "当前 Seq: -");
m_userIdText = builder.Text(transform, "注册 UserID: -");
m_resultText = builder.Text(transform, "注册结果: -");
}
/// <summary>刷新文件大小显示。</summary>
/// <summary>
/// 执行 RefreshFileInfo 方法。
/// </summary>
public void RefreshFileInfo()
{
var path = ReadText(m_filePathInput);
m_fileSizeText.text = File.Exists(path) ? "文件大小: " + new FileInfo(path).Length + " bytes" : "文件不存在";
}
/// <summary>使用 0xF7 注册。</summary>
/// <summary>
/// 执行 EnrollWithPhoto 方法。
/// </summary>
public void EnrollWithPhoto()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var bytes = ReadValidFile();
var result = await m_manager.Images.EnrollWithPhotoAsync(bytes, (BioType)m_bioTypeDropdown.value, ReadText(m_userNameInput), m_duplicateToggle.isOn, OnProgress);
ShowEnrollResult(result);
});
}
/// <summary>使用 0xD7 指定 ID 注册。</summary>
/// <summary>
/// 执行 EnrollWithPhotoAndId 方法。
/// </summary>
public void EnrollWithPhotoAndId()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var bytes = ReadValidFile();
var userId = ReadInt(m_userIdInput, 0);
if (!m_specifyUserIdToggle.isOn)
{
SetResult("请先启用指定 UserID");
return;
}
var result = await m_manager.Images.EnrollWithPhotoAndIdAsync(userId, bytes, (BioType)m_bioTypeDropdown.value, ReadText(m_userNameInput), m_duplicateToggle.isOn, OnProgress);
ShowEnrollResult(result);
});
}
/// <summary>
/// 执行 ReadValidFile 方法。
/// </summary>
private byte[] ReadValidFile()
{
var path = ReadText(m_filePathInput);
if (!File.Exists(path)) throw new FileNotFoundException("文件不存在", path);
var bytes = File.ReadAllBytes(path);
if (bytes.Length == 0) throw new InvalidOperationException("文件为空");
if (bytes.Length > 8 * 1024 * 1024) throw new InvalidOperationException("文件过大,建议小于 8MB");
return bytes;
}
/// <summary>
/// 执行 OnProgress 方法。
/// </summary>
private void OnProgress(int sent, int total)
{
m_progress.value = total > 0 ? (float)sent / total : 0;
m_seqText.text = "当前 Seq: " + Mathf.CeilToInt((float)sent / FaceModuleConstants.PhotoRegisterMtu);
}
/// <summary>
/// 执行 ShowEnrollResult 方法。
/// </summary>
private void ShowEnrollResult(FaceEnrollResult result)
{
m_userIdText.text = "注册 UserID: " + result.UserId;
SetResult(result.Success ? "注册成功" : FaceProtocolEncoding.ResultMessage(result.ResultCode));
}
}
public sealed class FaceFeaturePage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_userIdInput;
// 内部状态字段。
private Dropdown m_featureTypeDropdown;
// 内部状态字段。
private InputField m_filePathInput;
// 内部状态字段。
private InputField m_userNameInput;
// 内部状态字段。
private Slider m_progress;
// 内部状态字段。
private Text m_seqText;
// 内部状态字段。
private Text m_lengthText;
// 内部状态字段。
private byte[] m_lastFeature;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "特征管理", 30);
m_userIdInput = builder.Input(transform, "UserID", "1");
m_featureTypeDropdown = builder.Dropdown(transform, new[] { "彩色人脸模板", "彩色 + 红外人脸模板", "掌纹掌静脉模板" });
m_filePathInput = builder.Input(transform, "特征文件路径", string.Empty);
m_userNameInput = builder.Input(transform, "用户姓名", "FeatureUser");
var row = builder.Row(transform);
builder.Button(row.transform, "读取特征", ReadFeature);
builder.Button(row.transform, "保存特征文件", SaveFeature);
builder.Button(row.transform, "写入特征", WriteFeature);
m_progress = builder.Slider(transform);
m_seqText = builder.Text(transform, "当前 Seq: -");
m_lengthText = builder.Text(transform, "特征长度: -");
m_resultText = builder.Text(transform, "操作结果: -");
}
/// <summary>读取特征。</summary>
/// <summary>
/// 执行 ReadFeature 方法。
/// </summary>
public void ReadFeature()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
m_lastFeature = await m_manager.Features.ReadFeatureAsync(ReadInt(m_userIdInput, 0), FeatureTypeValue());
m_lengthText.text = "特征长度: " + (m_lastFeature == null ? 0 : m_lastFeature.Length);
SetResult(m_lastFeature.Length > 0 ? "特征读取完成" : "读取命令已完成,未收到特征数据");
});
}
/// <summary>保存特征文件。</summary>
/// <summary>
/// 执行 SaveFeature 方法。
/// </summary>
public void SaveFeature()
{
if (m_lastFeature == null || m_lastFeature.Length == 0)
{
SetResult("没有可保存的特征数据");
return;
}
var dir = Path.Combine(Application.persistentDataPath, "FaceModuleFeatures");
Directory.CreateDirectory(dir);
var name = $"{DateTime.Now:yyyyMMdd_HHmmss}_User_{ReadInt(m_userIdInput, 0)}_Feature_{FeatureTypeValue()}.bin";
File.WriteAllBytes(Path.Combine(dir, name), m_lastFeature);
SetResult("特征已保存: " + dir);
}
/// <summary>写入特征。</summary>
/// <summary>
/// 执行 WriteFeature 方法。
/// </summary>
public void WriteFeature()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var path = ReadText(m_filePathInput);
if (!File.Exists(path)) throw new FileNotFoundException("特征文件不存在", path);
var bytes = File.ReadAllBytes(path);
var result = await m_manager.Features.WriteFeatureAsync(bytes, FeatureTypeValue(), ReadText(m_userNameInput), OnProgress);
SetResult(result.Success ? "写入成功 UserID=" + result.UserId : FaceProtocolEncoding.ResultMessage(result.ResultCode));
});
}
/// <summary>
/// 执行 FeatureTypeValue 方法。
/// </summary>
private FeatureType FeatureTypeValue()
{
return (FeatureType)(m_featureTypeDropdown.value + 1);
}
/// <summary>
/// 执行 OnProgress 方法。
/// </summary>
private void OnProgress(int sent, int total)
{
m_progress.value = total > 0 ? (float)sent / total : 0;
m_seqText.text = "当前 Seq: " + Mathf.CeilToInt((float)sent / FaceModuleConstants.FeaturePacketSize);
m_lengthText.text = "特征长度: " + total;
}
}
public sealed class FaceUvcSettingsPage : FaceModulePageBase
{
// 内部状态字段。
private Dropdown m_usbTypeDropdown;
// 内部状态字段。
private InputField m_bitrateInput;
// 内部状态字段。
private InputField m_qualityInput;
// 内部状态字段。
private Toggle m_mirrorToggle;
// 内部状态字段。
private Toggle m_rotateToggle;
// 内部状态字段。
private Toggle m_faceBoxToggle;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "UVC 设置", 30);
m_usbTypeDropdown = builder.Dropdown(transform, new[] { "USB1.1 Bulk 0x11", "USB2.0 Bulk 0x20", "USB1.1 ISOC 0x91", "USB2.0 ISOC 0xA0" });
m_bitrateInput = builder.Input(transform, "UVC 码率 Mbps", "8");
m_qualityInput = builder.Input(transform, "MJPG 质量 10-100", "80");
m_mirrorToggle = builder.Toggle(transform, "镜像", false);
m_rotateToggle = builder.Toggle(transform, "180 度倒转", false);
m_faceBoxToggle = builder.Toggle(transform, "显示人脸框", true);
var row = builder.Row(transform);
builder.Button(row.transform, "读取 UVC 参数", ReadUvc);
builder.Button(row.transform, "设置 UVC 参数", SetUvc);
builder.Button(row.transform, "设置人脸框显示", SetFaceBox);
m_resultText = builder.Text(transform, "操作结果: -");
}
/// <summary>
/// 执行 ReadUvc 方法。
/// </summary>
public void ReadUvc()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var p = await m_manager.Uvc.ReadUsbUvcParametersAsync();
m_bitrateInput.text = p.BitrateMbps.ToString();
m_qualityInput.text = p.MjpgQuality.ToString();
m_mirrorToggle.isOn = p.Mirror;
m_rotateToggle.isOn = p.Rotate180;
SetResult("UVC 参数读取成功");
});
}
/// <summary>
/// 执行 SetUvc 方法。
/// </summary>
public void SetUvc()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var bitrate = ReadByte(m_bitrateInput, 8);
var quality = Mathf.Clamp(ReadByte(m_qualityInput, 80), 10, 100);
await m_manager.Uvc.SetUsbUvcParametersAsync(new UsbUvcParameters
{
UsbType = UsbTypeValue(),
BitrateMbps = bitrate,
MjpgQuality = (byte)quality,
Mirror = m_mirrorToggle.isOn,
Rotate180 = m_rotateToggle.isOn
});
SetResult("UVC 参数设置成功");
});
}
/// <summary>
/// 执行 SetFaceBox 方法。
/// </summary>
public void SetFaceBox()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Uvc.SetFaceLocationDisplayAsync(m_faceBoxToggle.isOn); SetResult("人脸框显示设置成功"); } });
}
/// <summary>
/// 执行 UsbTypeValue 方法。
/// </summary>
private byte UsbTypeValue()
{
switch (m_usbTypeDropdown.value)
{
case 0: return 0x11;
case 1: return 0x20;
case 2: return 0x91;
default: return 0xA0;
}
}
}
public sealed class FaceAdvancedSettingsPage : FaceModulePageBase
{
// 内部状态字段。
private Dropdown m_rgbDropdown;
// 内部状态字段。
private Toggle m_duplicateToggle;
// 内部状态字段。
private Toggle m_demoToggle;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "高级设置", 30);
m_rgbDropdown = builder.Dropdown(transform, new[] { "0", "1", "2 默认", "3", "4" });
m_rgbDropdown.value = 2;
m_duplicateToggle = builder.Toggle(transform, "启用查重", true);
m_demoToggle = builder.Toggle(transform, "Demo Mode: 所有人可解锁,但仍做活体检测", false);
var row = builder.Row(transform);
builder.Button(row.transform, "设置 RGB_LEVEL", SetRgb);
builder.Button(row.transform, "设置查重", SetDuplicate);
builder.Button(row.transform, "读取查重状态", ReadDuplicate);
builder.Button(row.transform, "设置 Demo Mode", SetDemoMode);
m_resultText = builder.Text(transform, "操作结果: -");
}
/// <summary>
/// 执行 SetRgb 方法。
/// </summary>
public void SetRgb()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Uvc.SetRgbLevelAsync((byte)m_rgbDropdown.value); SetResult("RGB_LEVEL 设置成功"); } });
}
/// <summary>
/// 执行 SetDuplicate 方法。
/// </summary>
public void SetDuplicate()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Uvc.SetDuplicateCheckAsync(m_duplicateToggle.isOn); SetResult("查重设置成功"); } });
}
/// <summary>
/// 执行 ReadDuplicate 方法。
/// </summary>
public void ReadDuplicate()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
m_duplicateToggle.isOn = await m_manager.Uvc.ReadDuplicateCheckAsync();
SetResult("查重状态已读取");
});
}
/// <summary>
/// 执行 SetDemoMode 方法。
/// </summary>
public void SetDemoMode()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Basic.SetDemoModeAsync(m_demoToggle.isOn); SetResult("Demo Mode 设置成功"); } });
}
}
public sealed class FaceEncryptionPage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_releaseKeyInput;
// 内部状态字段。
private InputField m_debugKeyInput;
// 内部状态字段。
private InputField m_seedInput;
// 内部状态字段。
private Text m_stateText;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "加密通信", 30);
m_releaseKeyInput = builder.Input(transform, "Release Key 16 bytes Hex", string.Empty);
m_debugKeyInput = builder.Input(transform, "Debug Key 16 bytes Hex", string.Empty);
m_seedInput = builder.Input(transform, "Seed 4 bytes Hex", string.Empty);
var row = builder.Row(transform);
builder.Button(row.transform, "设置 Release Key", SetReleaseKey);
builder.Button(row.transform, "设置 Debug Key", SetDebugKey);
builder.Button(row.transform, "生成随机 Seed", GenerateSeed);
builder.Button(row.transform, "Init Encryption", InitEncryption);
m_stateText = builder.Text(transform, "当前加密状态: 加密算法未接入,当前仅完成协议入口");
m_resultText = builder.Text(transform, "操作结果: -");
}
/// <summary>
/// 执行 SetReleaseKey 方法。
/// </summary>
public void SetReleaseKey()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Encryption.SetReleaseEncryptionKeyAsync(ParseHex(m_releaseKeyInput.text, 16)); SetResult("Release Key 设置成功"); } });
}
/// <summary>
/// 执行 SetDebugKey 方法。
/// </summary>
public void SetDebugKey()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Encryption.SetDebugEncryptionKeyAsync(ParseHex(m_debugKeyInput.text, 16)); SetResult("Debug Key 设置成功"); } });
}
/// <summary>
/// 执行 GenerateSeed 方法。
/// </summary>
public void GenerateSeed()
{
var bytes = new byte[4];
using (var rng = new System.Security.Cryptography.RNGCryptoServiceProvider())
{
rng.GetBytes(bytes);
}
m_seedInput.text = BitConverter.ToString(bytes).Replace("-", " ");
SetResult("Seed 已生成");
}
/// <summary>
/// 执行 InitEncryption 方法。
/// </summary>
public void InitEncryption()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
await m_manager.Encryption.InitEncryptionAsync(ParseHex(m_seedInput.text, 4), 0);
m_stateText.text = "当前加密状态: 协议入口已调用,供应商算法仍未接入";
SetResult("Init Encryption 完成");
});
}
/// <summary>
/// 执行 ParseHex 方法。
/// </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("需要 " + expectedLength + " bytes Hex");
var bytes = new byte[parts.Length];
for (var i = 0; i < parts.Length; i++) bytes[i] = Convert.ToByte(parts[i], 16);
return bytes;
}
}
public sealed class FaceFirmwarePage : FaceModulePageBase
{
// 内部状态字段。
private Slider m_progress;
// 内部状态字段。
private Text m_percentText;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "固件升级", 30);
builder.Text(transform, "风险提示: 升级过程中不要断电;固件文件应由 U 盘或模组侧准备。", 22);
builder.Button(transform, "启动 U 盘升级", StartUpgrade);
m_progress = builder.Slider(transform);
m_percentText = builder.Text(transform, "当前进度: 0%");
m_resultText = builder.Text(transform, "操作结果: -");
}
/// <summary>启动 U 盘固件升级。</summary>
/// <summary>
/// 执行 StartUpgrade 方法。
/// </summary>
public void StartUpgrade()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
await m_manager.Basic.UpgradeFirmwareAsync();
m_progress.value = 1;
m_percentText.text = "当前进度: 已启动";
SetResult("固件升级命令已发送");
});
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef28d55dc2ea2224989c843c65b02044
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,940 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Aisu.SIM1.Face.Core;
using Aisu.SIM1.Face.Models;
using Aisu.SIM1.Face.Protocol;
using Aisu.SIM1.Face.UI.Common;
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.Pages
{
public sealed class FaceConnectPage : FaceModulePageBase
{
// Inspector 绑定字段。
[SerializeField] private InputField m_portInput;
// Inspector 绑定字段。
[SerializeField] private InputField m_baudRateInput;
// Inspector 绑定字段。
[SerializeField] private Toggle m_mockToggle;
// Inspector 绑定字段。
[SerializeField] private Text m_connectionText;
// Inspector 绑定字段。
[SerializeField] private Text m_readyText;
// Inspector 绑定字段。
[SerializeField] private Text m_moduleStatusText;
// Inspector 绑定字段。
[SerializeField] private Text m_licenseText;
// Inspector 绑定字段。
[SerializeField] private Text m_versionText;
// Inspector 绑定字段。
[SerializeField] private Text m_snText;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "连接 / 基础状态", 30);
var form = builder.Section(transform, "通信配置");
m_portInput = builder.Input(form.transform, "串口号", "COM3");
m_baudRateInput = builder.Input(form.transform, "波特率", "115200");
m_mockToggle = builder.Toggle(form.transform, "Editor 使用 Mock 模式", IsEditorMockEnabled());
var row = builder.Row(form.transform);
builder.Button(row.transform, "打开连接", OpenConnection);
builder.Button(row.transform, "关闭连接", CloseConnection);
builder.Button(row.transform, "等待 READY", WaitReady);
var actions = builder.Section(transform, "基础命令");
var actionRow = builder.Row(actions.transform);
builder.Button(actionRow.transform, "RESET", ResetModule);
builder.Button(actionRow.transform, "GET STATUS", GetStatus);
builder.Button(actionRow.transform, "GET VERSION", GetVersion);
builder.Button(actionRow.transform, "GET SN", GetSn);
var info = builder.Section(transform, "状态");
m_connectionText = builder.Text(info.transform, "连接状态: 未连接");
m_readyText = builder.Text(info.transform, "READY: 未 READY");
m_moduleStatusText = builder.Text(info.transform, "模组状态: Invalid");
m_licenseText = builder.Text(info.transform, "授权: Mock 模式不需要授权");
m_versionText = builder.Text(info.transform, "版本: -");
m_snText = builder.Text(info.transform, "SN: -");
m_resultText = builder.Text(info.transform, "结果: -");
}
/// <summary>打开通信连接。</summary>
/// <summary>
/// 执行 OpenConnection 方法。
/// </summary>
public void OpenConnection()
{
RunAsync(async () =>
{
var useMock = m_mockToggle != null && m_mockToggle.isOn;
var baud = ReadInt(m_baudRateInput, 115200);
if (!useMock && !ValidateLicenseBeforeOpen())
{
return;
}
m_manager.Configure(useMock, ReadText(m_portInput), baud);
m_manager.Open();
if (useMock)
{
await m_manager.WaitReadyAsync();
RefreshState();
SetResult("Mock 模组已连接");
return;
}
try
{
await m_manager.WaitReadyAsync();
RefreshState();
SetResult("硬件连接成功,已收到 READY");
}
catch
{
try
{
var status = await m_manager.ProbeReadyByStatusAsync();
RefreshState();
m_moduleStatusText.text = "模组状态: " + status;
SetResult("硬件连接成功,未收到 READY NOTE,但 GET STATUS 探测成功");
}
catch
{
m_manager.Close();
RefreshState();
throw new TimeoutException("未收到硬件 READY,GET STATUS 也无响应,请检查串口、波特率、电源和接线");
}
}
});
}
/// <summary>关闭通信连接。</summary>
/// <summary>
/// 执行 CloseConnection 方法。
/// </summary>
public void CloseConnection()
{
m_manager.Close();
RefreshState();
SetResult("连接已关闭");
}
/// <summary>等待 READY NOTE。</summary>
/// <summary>
/// 执行 WaitReady 方法。
/// </summary>
public void WaitReady()
{
RunAsync(async () =>
{
await m_manager.WaitReadyAsync();
RefreshState();
SetResult("模组 READY");
});
}
/// <summary>复位模组。</summary>
/// <summary>
/// 执行 ResetModule 方法。
/// </summary>
public void ResetModule()
{
RunAsync(async () => { if (RequireConnected()) { await m_manager.Basic.ResetAsync(); SetResult("RESET 成功"); } });
}
/// <summary>获取模组状态。</summary>
/// <summary>
/// 执行 GetStatus 方法。
/// </summary>
public void GetStatus()
{
RunAsync(async () =>
{
if (!RequireConnected()) return;
var status = await m_manager.ProbeReadyByStatusAsync();
RefreshState();
m_moduleStatusText.text = "模组状态: " + status;
SetResult("状态获取成功");
});
}
/// <summary>获取版本。</summary>
/// <summary>
/// 执行 GetVersion 方法。
/// </summary>
public void GetVersion()
{
RunAsync(async () =>
{
if (!RequireConnected()) return;
m_versionText.text = "版本: " + await m_manager.Basic.GetVersionAsync();
SetResult("版本获取成功");
});
}
/// <summary>获取 SN。</summary>
/// <summary>
/// 执行 GetSn 方法。
/// </summary>
public void GetSn()
{
RunAsync(async () =>
{
if (!RequireConnected()) return;
m_snText.text = "SN: " + await m_manager.Basic.GetSnAsync();
SetResult("SN 获取成功");
});
}
/// <summary>
/// 执行 RefreshState 方法。
/// </summary>
private void RefreshState()
{
var state = m_manager.IsReady ? "硬件已连接" : (m_manager.IsOpen ? "端口已打开,未 READY" : "未连接");
m_connectionText.text = "连接状态: " + state;
m_readyText.text = "READY: " + (m_manager.IsReady ? "READY" : "未 READY");
RefreshLicenseState(m_mockToggle != null && m_mockToggle.isOn);
}
/// <summary>
/// 真实串口连接前检查授权。
/// </summary>
private bool ValidateLicenseBeforeOpen()
{
var path = FaceLicenseManager.GetDefaultLicensePath();
var status = FaceLicenseManager.LoadAndVerify(path);
if (status == FaceLicenseStatus.Valid)
{
RefreshLicenseState(false);
return true;
}
var message = "授权无效,无法打开真实串口。路径: " + path + ",原因: " + FaceLicenseManager.LastError;
if (m_licenseText != null)
{
m_licenseText.text = "授权: " + message;
}
SetResult(message);
return false;
}
/// <summary>
/// 刷新授权状态显示。
/// </summary>
private void RefreshLicenseState(bool useMock)
{
if (m_licenseText == null)
{
return;
}
if (useMock)
{
m_licenseText.text = "授权: Mock 模式不需要授权";
return;
}
if (FaceLicenseManager.Status == FaceLicenseStatus.Valid && FaceLicenseManager.Payload != null)
{
m_licenseText.text = "授权: 有效,剩余 " + FaceLicenseVerifier.GetRemainingDays(FaceLicenseManager.Payload) + " 天";
return;
}
m_licenseText.text = "授权: 未验证,文件路径 " + FaceLicenseManager.GetDefaultLicensePath();
}
private bool IsEditorMockEnabled()
{
if (m_manager == null)
{
return true;
}
return m_manager.Settings != null ? m_manager.Settings.UseMockInEditor : m_manager.Config.UseMockInEditor;
}
}
public sealed class FaceProtocolDebugPage : FaceModulePageBase
{
// Inspector 绑定字段。
[SerializeField] private InputField m_hexInput;
// Inspector 绑定字段。
[SerializeField] private Text m_parseText;
private readonly FacePacketParser m_parser = new FacePacketParser();
private FaceModuleManager m_boundManager; // 已订阅事件的 Manager。
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "协议调试", 30);
m_hexInput = builder.Input(transform, "EF AA 10 00 00 10", "EF AA 10 00 00 10");
var row = builder.Row(transform);
builder.Button(row.transform, "发送 Hex", SendHex);
builder.Button(row.transform, "清空输入", () => m_hexInput.text = string.Empty);
m_parseText = builder.Text(transform, "解析结果: -", 20, TextAnchor.UpperLeft);
m_resultText = builder.Text(transform, "结果: -");
}
public override void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
UnbindManagerEvents();
base.Init(manager, logPanel, loadingView);
m_boundManager = m_manager;
m_manager.OnRawLog += ParseRaw;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>发送原始 Hex 数据。</summary>
/// <summary>
/// 执行 SendHex 方法。
/// </summary>
public void SendHex()
{
try
{
if (!RequireConnected()) return;
var bytes = ParseHex(ReadText(m_hexInput));
m_manager.SendRawBytes(bytes);
SetResult("Hex 已发送");
}
catch (Exception ex) { SetError(ex); }
}
/// <summary>
/// 执行 ParseRaw 方法。
/// </summary>
private void ParseRaw(string line)
{
var index = line.IndexOf(' ');
if (index < 0) return;
try
{
var packets = m_parser.Push(ParseHex(line.Substring(index + 1)));
foreach (var packet in packets)
{
m_parseText.text = $"MsgID=0x{packet.MsgId:X2}\nSize={packet.Size}\nData={BitConverter.ToString(packet.Data)}\nChecksum=0x{packet.Checksum:X2}\nValid={packet.IsValid}";
}
}
catch { }
}
/// <summary>
/// 执行 ParseHex 方法。
/// </summary>
private static byte[] ParseHex(string text)
{
var parts = text.Split(new[] { ' ', '\t', '\r', '\n', '-' }, StringSplitOptions.RemoveEmptyEntries);
var bytes = new byte[parts.Length];
for (var i = 0; i < parts.Length; i++)
{
bytes[i] = Convert.ToByte(parts[i], 16);
}
return bytes;
}
private void UnbindManagerEvents()
{
if (m_boundManager != null)
{
m_boundManager.OnRawLog -= ParseRaw;
m_boundManager = null;
}
}
}
public sealed class FaceVerifyPage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_timeoutInput;
// 内部状态字段。
private InputField m_maxTimesInput;
// 内部状态字段。
private Toggle m_autoToggle;
// 内部状态字段。
private Text m_faceStateText;
// 内部状态字段。
private Text m_poseText;
// 内部状态字段。
private Text m_boxText;
// 内部状态字段。
private Text m_userIdText;
// 内部状态字段。
private Text m_userNameText;
// 内部状态字段。
private Text m_adminText;
// 内部状态字段。
private Text m_unlockText;
private FaceModuleManager m_boundManager; // 已订阅事件的 Manager。
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "人脸验证", 30);
m_timeoutInput = builder.Input(transform, "超时时间", "10");
m_maxTimesInput = builder.Input(transform, "最大识别次数", "30");
m_autoToggle = builder.Toggle(transform, "自动探测验证", false);
var row = builder.Row(transform);
builder.Button(row.transform, "开始验证", StartVerify);
builder.Button(row.transform, "停止 / RESET", StopVerify);
m_faceStateText = builder.Text(transform, "人脸状态: -");
m_poseText = builder.Text(transform, "Yaw/Pitch/Roll: -");
m_boxText = builder.Text(transform, "人脸框: -");
m_userIdText = builder.Text(transform, "UserID: -");
m_userNameText = builder.Text(transform, "UserName: -");
m_adminText = builder.Text(transform, "Admin: -");
m_unlockText = builder.Text(transform, "UnlockStatus: -");
m_resultText = builder.Text(transform, "结果: -");
}
public override void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
UnbindManagerEvents();
base.Init(manager, logPanel, loadingView);
m_boundManager = m_manager;
m_manager.OnFaceStateUpdated += OnFaceState;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>开始验证或自动验证。</summary>
/// <summary>
/// 执行 StartVerify 方法。
/// </summary>
public void StartVerify()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var result = m_autoToggle.isOn
? await m_manager.Verify.AutoVerifyAsync(true, ReadByte(m_timeoutInput, 10))
: await m_manager.Verify.VerifyAsync(ReadByte(m_timeoutInput, 10), ReadByte(m_maxTimesInput, 30));
ShowResult(result);
});
}
/// <summary>停止验证。</summary>
/// <summary>
/// 执行 StopVerify 方法。
/// </summary>
public void StopVerify()
{
RunAsync(async () => { if (RequireConnected()) { await m_manager.Basic.ResetAsync(); SetResult("已停止验证"); } });
}
/// <summary>
/// 执行 OnFaceState 方法。
/// </summary>
private void OnFaceState(FaceFaceStateNote state)
{
m_faceStateText.text = "人脸状态: " + FaceProtocolEncoding.FaceStateMessage(state.State);
m_poseText.text = $"Yaw/Pitch/Roll: {state.Yaw}/{state.Pitch}/{state.Roll}";
m_boxText.text = $"人脸框: L{state.Left} T{state.Top} R{state.Right} B{state.Bottom}";
}
/// <summary>
/// 执行 ShowResult 方法。
/// </summary>
private void ShowResult(FaceVerifyResult result)
{
m_userIdText.text = "UserID: " + result.UserId;
m_userNameText.text = "UserName: " + result.UserName;
m_adminText.text = "Admin: " + result.IsAdmin;
m_unlockText.text = "UnlockStatus: " + result.UnlockStatus;
SetResult(result.Success ? "验证成功" : FaceProtocolEncoding.ResultMessage(result.ResultCode));
}
private void UnbindManagerEvents()
{
if (m_boundManager != null)
{
m_boundManager.OnFaceStateUpdated -= OnFaceState;
m_boundManager = null;
}
}
}
public sealed class FaceEnrollPage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_nameInput;
// 内部状态字段。
private Toggle m_adminToggle;
// 内部状态字段。
private Dropdown m_modeDropdown;
// 内部状态字段。
private Dropdown m_directionDropdown;
// 内部状态字段。
private Dropdown m_enrollTypeDropdown;
// 内部状态字段。
private Toggle m_duplicateToggle;
// 内部状态字段。
private InputField m_timeoutInput;
// 内部状态字段。
private Text m_faceStateText;
// 内部状态字段。
private Text m_poseText;
// 内部状态字段。
private Text m_directionText;
// 内部状态字段。
private Text m_userIdText;
private FaceModuleManager m_boundManager; // 已订阅事件的 Manager。
private Action<FaceFaceStateNote> m_faceStateHandler; // 人脸状态事件回调。
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "人脸录入", 30);
m_nameInput = builder.Input(transform, "用户姓名", "User");
m_adminToggle = builder.Toggle(transform, "管理员", false);
m_modeDropdown = builder.Dropdown(transform, new[] { "交互录入 ENROLL", "单帧录入 ENROLL_SINGLE", "集成录入 ENROLL_ITG" });
m_directionDropdown = builder.Dropdown(transform, new[] { "正脸", "朝上", "朝下", "朝左", "朝右" });
m_enrollTypeDropdown = builder.Dropdown(transform, new[] { "交互", "单帧" });
m_duplicateToggle = builder.Toggle(transform, "允许重复录入", false);
m_timeoutInput = builder.Input(transform, "超时时间", "20");
var row = builder.Row(transform);
builder.Button(row.transform, "开始录入", StartEnroll);
builder.Button(row.transform, "取消录入", CancelEnroll);
m_faceStateText = builder.Text(transform, "人脸状态: -");
m_poseText = builder.Text(transform, "姿态: -");
m_directionText = builder.Text(transform, "已录入方向: -");
m_userIdText = builder.Text(transform, "最终 UserID: -");
m_resultText = builder.Text(transform, "ResultCode: -");
}
public override void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
UnbindManagerEvents();
base.Init(manager, logPanel, loadingView);
m_boundManager = m_manager;
m_faceStateHandler = state =>
{
m_faceStateText.text = FaceProtocolEncoding.FaceStateMessage(state.State);
m_poseText.text = $"Yaw/Pitch/Roll: {state.Yaw}/{state.Pitch}/{state.Roll}";
};
m_manager.OnFaceStateUpdated += m_faceStateHandler;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>开始录入。</summary>
/// <summary>
/// 执行 StartEnroll 方法。
/// </summary>
public void StartEnroll()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
if (FaceProtocolEncoding.GetUserNameByteCount(ReadText(m_nameInput)) > FaceModuleConstants.UserNameSize)
{
SetResult("用户名 UTF-8 编码超过 32 bytes");
return;
}
FaceEnrollResult result;
if (m_modeDropdown.value == 1)
{
result = await m_manager.Enrollment.EnrollSingleAsync(m_adminToggle.isOn, ReadText(m_nameInput), ReadByte(m_timeoutInput, 20));
}
else if (m_modeDropdown.value == 2)
{
result = await m_manager.Enrollment.EnrollIntegratedAsync(m_adminToggle.isOn, ReadText(m_nameInput), Direction(), ReadByte(m_timeoutInput, 20), m_enrollTypeDropdown.value == 1, !m_duplicateToggle.isOn);
}
else
{
result = await m_manager.Enrollment.EnrollAsync(m_adminToggle.isOn, ReadText(m_nameInput), Direction(), ReadByte(m_timeoutInput, 20));
}
m_userIdText.text = "最终 UserID: " + result.UserId;
m_directionText.text = "已录入方向: " + DirectionText(result.Direction);
if (result.Success)
{
await ConfirmEnrollResult(result.UserId);
}
else
{
SetResult(FaceProtocolEncoding.ResultMessage(result.ResultCode));
}
});
}
/// <summary>
/// 录入成功后读取用户信息,确认模组已写入用户表。
/// </summary>
private async System.Threading.Tasks.Task ConfirmEnrollResult(int userId)
{
try
{
var info = await m_manager.Users.GetUserInfoAsync(userId);
SetResult("录入成功,已确认用户: ID=" + info.UserId + " 姓名=" + info.UserName + " 管理员=" + info.IsAdmin);
}
catch (Exception ex)
{
SetResult("录入成功,但查询用户确认失败: " + ex.Message);
}
}
/// <summary>取消录入。</summary>
/// <summary>
/// 执行 CancelEnroll 方法。
/// </summary>
public void CancelEnroll()
{
RunAsync(async () => { if (RequireConnected()) { await m_manager.Basic.FaceResetAsync(); SetResult("已取消录入"); } });
}
/// <summary>
/// 执行 Direction 方法。
/// </summary>
private FaceDirection Direction()
{
switch (m_directionDropdown.value)
{
case 1: return FaceDirection.Up;
case 2: return FaceDirection.Down;
case 3: return FaceDirection.Left;
case 4: return FaceDirection.Right;
default: return FaceDirection.Middle;
}
}
/// <summary>
/// 执行 DirectionText 方法。
/// </summary>
private string DirectionText(FaceDirection direction)
{
return direction.ToString();
}
private void UnbindManagerEvents()
{
if (m_boundManager != null && m_faceStateHandler != null)
{
m_boundManager.OnFaceStateUpdated -= m_faceStateHandler;
}
m_boundManager = null;
m_faceStateHandler = null;
}
}
public sealed class FaceUserManagePage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_userIdInput;
// 内部状态字段。
private Text m_listText;
// 内部状态字段。
private Text m_detailText;
// 内部状态字段。
private FaceModuleConfirmDialog m_confirmDialog;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "用户管理", 30);
m_userIdInput = builder.Input(transform, "UserID", "1");
var row = builder.Row(transform);
builder.Button(row.transform, "查询用户", QueryUser);
builder.Button(row.transform, "删除用户", DeleteUser);
builder.Button(row.transform, "删除全部", DeleteAll);
var row2 = builder.Row(transform);
builder.Button(row2.transform, "获取所有 ID", QueryAll);
builder.Button(row2.transform, "获取所有 ID2", QueryAll2);
var scroll = builder.ScrollView(transform, out m_listText);
scroll.GetComponent<LayoutElement>().preferredHeight = 240;
m_detailText = builder.Text(transform, "用户详情: -");
m_resultText = builder.Text(transform, "结果: -");
}
public override void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
base.Init(manager, logPanel, loadingView);
m_confirmDialog = FindObjectOfType<FaceModuleConfirmDialog>(true);
}
/// <summary>
/// 执行 QueryUser 方法。
/// </summary>
public void QueryUser()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var info = await m_manager.Users.GetUserInfoAsync(ReadInt(m_userIdInput, 0));
m_detailText.text = $"用户详情: ID={info.UserId} Name={info.UserName} Admin={info.IsAdmin}";
SetResult("查询成功");
});
}
/// <summary>
/// 执行 DeleteUser 方法。
/// </summary>
public void DeleteUser()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
await m_manager.Users.DeleteUserAsync(ReadInt(m_userIdInput, 0));
SetResult("删除成功");
await RefreshIds(false);
});
}
/// <summary>
/// 执行 DeleteAll 方法。
/// </summary>
public void DeleteAll()
{
if (m_confirmDialog != null)
{
m_confirmDialog.Show("确定删除全部用户?该操作不可撤销。", DeleteAllConfirmed);
return;
}
DeleteAllConfirmed();
}
/// <summary>
/// 执行 DeleteAllConfirmed 方法。
/// </summary>
private void DeleteAllConfirmed()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
await m_manager.Users.DeleteAllAsync();
SetResult("全部用户已删除");
await RefreshIds(false);
});
}
/// <summary>
/// 执行 QueryAll 方法。
/// </summary>
public void QueryAll()
{
RunAsync(async () => { if (RequireReady()) await RefreshIds(false); });
}
/// <summary>
/// 执行 QueryAll2 方法。
/// </summary>
public void QueryAll2()
{
RunAsync(async () => { if (RequireReady()) await RefreshIds(true); });
}
/// <summary>
/// 执行 RefreshIds 方法。
/// </summary>
private async System.Threading.Tasks.Task RefreshIds(bool useV2)
{
var ids = useV2 ? await m_manager.Users.GetAllUserIds2Async() : await m_manager.Users.GetAllUserIdsAsync();
m_listText.text = string.Join("\n", ids.ConvertAll(id => "UserID: " + id));
SetResult("用户列表已刷新");
}
}
public sealed class FaceQrCodePage : FaceModulePageBase
{
// 内部状态字段。
private InputField m_timeoutInput;
// 内部状态字段。
private Text m_qrText;
private FaceModuleManager m_boundManager; // 已订阅事件的 Manager。
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "二维码", 30);
m_timeoutInput = builder.Input(transform, "扫码超时时间", "10");
var row = builder.Row(transform);
builder.Button(row.transform, "开始扫码", Scan);
builder.Button(row.transform, "自动探测二维码", AutoScan);
builder.Button(row.transform, "停止", Stop);
builder.Button(row.transform, "复制内容", Copy);
m_qrText = builder.Text(transform, "二维码内容: -", 22, TextAnchor.UpperLeft);
m_resultText = builder.Text(transform, "扫码状态: -");
}
public override void Init(FaceModuleManager manager, FaceModuleLogPanel logPanel, FaceModuleLoadingView loadingView)
{
UnbindManagerEvents();
base.Init(manager, logPanel, loadingView);
m_boundManager = m_manager;
m_manager.OnQrCodeReceived += OnQrCodeReceived;
}
private void OnDestroy()
{
UnbindManagerEvents();
}
/// <summary>
/// 执行 Scan 方法。
/// </summary>
public void Scan()
{
RunAsync(async () =>
{
if (!RequireReady()) return;
var qr = await m_manager.QrCode.ScanQrCodeAsync(ReadByte(m_timeoutInput, 10));
m_qrText.text = "二维码内容: " + qr;
SetResult(string.IsNullOrEmpty(qr) ? "二维码为空" : "扫码成功");
});
}
/// <summary>
/// 执行 AutoScan 方法。
/// </summary>
public void AutoScan()
{
RunAsync(async () => { if (RequireReady()) { await m_manager.Verify.AutoVerifyAsync(true, ReadByte(m_timeoutInput, 10)); SetResult("自动探测已启动"); } });
}
/// <summary>
/// 执行 Stop 方法。
/// </summary>
public void Stop()
{
RunAsync(async () => { if (RequireConnected()) { await m_manager.Basic.ResetAsync(); SetResult("已停止"); } });
}
/// <summary>
/// 执行 Copy 方法。
/// </summary>
public void Copy()
{
GUIUtility.systemCopyBuffer = m_qrText.text.Replace("二维码内容: ", string.Empty);
SetResult("已复制到剪贴板");
}
private void OnQrCodeReceived(string qr)
{
m_qrText.text = "二维码内容: " + qr;
}
private void UnbindManagerEvents()
{
if (m_boundManager != null)
{
m_boundManager.OnQrCodeReceived -= OnQrCodeReceived;
m_boundManager = null;
}
}
}
public sealed class FaceImagePage : FaceModulePageBase
{
// 内部状态字段。
private Dropdown m_dpiDropdown;
// 内部状态字段。
private Slider m_progress;
// 内部状态字段。
private Text m_sizeText;
// 内部状态字段。
private RawImage m_preview;
// 内部状态字段。
private Text m_userIdText;
// 内部状态字段。
private byte[] m_lastBytes;
// 内部状态字段。
private string m_lastType;
public override void Build(FaceModuleUiBuilder builder)
{
builder.Text(transform, "图片抓拍", 30);
m_dpiDropdown = builder.Dropdown(transform, new[] { "480x640", "600x800", "1200x1600" });
var row = builder.Row(transform);
builder.Button(row.transform, "抓拍普通图片", () => Snap("Normal"));
builder.Button(row.transform, "抓拍人脸图片", () => Snap("Face"));
builder.Button(row.transform, "抓拍大图", () => Snap("Large"));
builder.Button(row.transform, "保存图片", SaveImage);
builder.Button(row.transform, "清空图片", ClearImage);
m_progress = builder.Slider(transform);
m_sizeText = builder.Text(transform, "图片大小: -");
m_userIdText = builder.Text(transform, "本地匹配 UserID: -");
var previewGo = FaceModuleUiBuilder.Create("Preview", transform);
previewGo.AddComponent<LayoutElement>().preferredHeight = 360;
previewGo.AddComponent<Image>().color = Color.white;
m_preview = FaceModuleUiBuilder.Create("RawImage", previewGo.transform).AddComponent<RawImage>();
FaceModuleUiBuilder.Stretch(m_preview.rectTransform);
m_resultText = builder.Text(transform, "结果: -");
}
/// <summary>
/// 执行 Snap 方法。
/// </summary>
public void Snap(string type)
{
RunAsync(async () =>
{
if (!RequireReady()) return;
FaceImageResult result;
if (type == "Face") result = await m_manager.Images.SnapUploadFaceImageAsync();
else if (type == "Large") result = await m_manager.Images.SnapUploadLargeImageAsync((ImageDpi)m_dpiDropdown.value);
else result = await m_manager.Images.SnapUploadImageAsync();
m_lastBytes = result.Bytes;
m_lastType = type;
m_preview.texture = result.Texture;
m_sizeText.text = "图片大小: " + (m_lastBytes == null ? 0 : m_lastBytes.Length) + " bytes";
m_userIdText.text = "本地匹配 UserID: " + result.UserId;
m_progress.value = 1;
SetResult(result.Bytes.Length > 0 ? "图片接收完成" : "抓拍命令已完成,未收到图片数据");
});
}
/// <summary>
/// 执行 SaveImage 方法。
/// </summary>
public void SaveImage()
{
if (m_lastBytes == null || m_lastBytes.Length == 0)
{
SetResult("没有可保存的图片");
return;
}
var dir = Path.Combine(Application.persistentDataPath, "FaceModuleImages");
Directory.CreateDirectory(dir);
File.WriteAllBytes(Path.Combine(dir, DateTime.Now.ToString("yyyyMMdd_HHmmss_") + m_lastType + ImageExtension(m_lastBytes)), m_lastBytes);
SetResult("图片已保存: " + dir);
}
/// <summary>
/// 执行 ClearImage 方法。
/// </summary>
public void ClearImage()
{
m_lastBytes = null;
m_preview.texture = null;
m_sizeText.text = "图片大小: -";
m_userIdText.text = "本地匹配 UserID: -";
m_progress.value = 0;
SetResult("已清空图片");
}
private static string ImageExtension(byte[] bytes)
{
if (bytes != null && bytes.Length >= 4 && bytes[0] == 0x89 && bytes[1] == 0x50 && bytes[2] == 0x4E && bytes[3] == 0x47)
{
return ".png";
}
return ".jpg";
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39833480969093f4986d10e2814fe719
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6f03a13c9bc3e394788e8c26d2f5cfeb
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,78 @@
using UnityEngine;
using UnityEngine.UI;
namespace Aisu.SIM1.Face.UI.ViewModels
{
/// <summary>
/// 用户列表项视图。
/// </summary>
public sealed class FaceUserListItemView : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_userIdText;
// 内部状态字段。
private int m_userId;
// 公开数据字段。
public int UserId => m_userId;
/// <summary>
/// 绑定用户 ID。
/// </summary>
public void Bind(int userId)
{
m_userId = userId;
if (m_userIdText != null)
{
m_userIdText.text = "UserID: " + userId;
}
}
}
/// <summary>
/// 日志列表项视图。
/// </summary>
public sealed class FaceLogItemView : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private Text m_logText;
/// <summary>
/// 绑定日志文本。
/// </summary>
public void Bind(string log)
{
if (m_logText != null)
{
m_logText.text = log;
}
}
}
/// <summary>
/// 图片预览视图。
/// </summary>
public sealed class FaceImagePreviewView : MonoBehaviour
{
// Inspector 绑定字段。
[SerializeField] private RawImage m_previewImage;
// Inspector 绑定字段。
[SerializeField] private Text m_sizeText;
/// <summary>
/// 显示图片。
/// </summary>
public void Bind(Texture2D texture, int byteLength)
{
if (m_previewImage != null)
{
m_previewImage.texture = texture;
}
if (m_sizeText != null)
{
m_sizeText.text = byteLength + " bytes";
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 206ba1b1876c1a744abd773518b8fc61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,9 +1,9 @@
{
{
"name": "xin.aisu.si-m1.face",
"displayName": "AISU SI-M1 Face SDK",
"version": "0.1.5",
"version": "0.1.0",
"unity": "2021.3",
"description": "Unity UPM package for the AISU SI-M1 face recognition module protocol, transport, command services, and lifecycle manager. Distributed as compiled DLLs without source code.",
"description": "Unity UPM package for the AISU SI-M1 face recognition module protocol, transport, command services, lifecycle manager, tests, and demo sample.",
"author": {
"name": "AISU"
},
@ -14,5 +14,17 @@
"recognition",
"serial",
"unity"
]
],
"samples": [
{
"displayName": "SI-M1 Face Demo",
"description": "Full demo scene and UI for connecting, registering, recognizing, querying, deleting, and releasing the SI-M1 face module.",
"path": "Samples~/SI-M1 Face Demo"
},
{
"displayName": "Face Detection Demo",
"description": "Minimal camera preview, serial connection, enrollment, recognition, and user data test scene.",
"path": "Samples~/Face Detection Demo"
}
]
}