Compare commits
2 Commits
d5244a654f
...
affb537801
| Author | SHA1 | Date | |
|---|---|---|---|
| affb537801 | |||
| 72858e3ada |
2
.gitignore
vendored
2
.gitignore
vendored
@ -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.
@ -11,7 +11,7 @@ PluginImporter:
|
||||
enabled: 1
|
||||
settings: {}
|
||||
Editor:
|
||||
enabled: 0
|
||||
enabled: 1
|
||||
settings:
|
||||
DefaultValueInitialized: true
|
||||
WindowsStoreApps:
|
||||
|
||||
8
Samples~.meta
Normal file
8
Samples~.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 98eb2c19172144b47b7bc037fc6f5923
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/Face Detection Demo.meta
Normal file
8
Samples~/Face Detection Demo.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0e42a6f3e84ef2b9f0d2a01f5d7c31
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44fd32577c8a40df9df37c2ef1e3d003
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
57
Samples~/Face Detection Demo/README.md
Normal file
57
Samples~/Face Detection Demo/README.md
Normal 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. 点击用户行的 `删除`,确认可删除用户。
|
||||
|
||||
|
||||
|
||||
7
Samples~/Face Detection Demo/README.md.meta
Normal file
7
Samples~/Face Detection Demo/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28af189cc75c46d3857843802ab27446
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/Face Detection Demo/Scenes.meta
Normal file
8
Samples~/Face Detection Demo/Scenes.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b3b5204665d4d94be3e40f59ce773c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
162
Samples~/Face Detection Demo/Scenes/FaceDetectionDemo.unity
Normal file
162
Samples~/Face Detection Demo/Scenes/FaceDetectionDemo.unity
Normal 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:
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d5d10f2a28b74fd98aacbe5c80561894
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/Face Detection Demo/Scripts.meta
Normal file
8
Samples~/Face Detection Demo/Scripts.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7800e597b5f45d7a7da08b3f936a1f2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a5af54f3c89457a8e08e47e0b46c1c1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
200
Samples~/Face Detection Demo/Scripts/FaceDetectionDemoCamera.cs
Normal file
200
Samples~/Face Detection Demo/Scripts/FaceDetectionDemoCamera.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c3d5d5322f2496d9f9867220dfbc8ea
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e34e58a5d9143f09af4602997294a2f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 93c5f598b0a34ae490496363c31f03ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a64d11f829c74808af47ccb103389f36
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo.meta
Normal file
8
Samples~/SI-M1 Face Demo.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 34ad6ee935bce014d8614b88076050bd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
16
Samples~/SI-M1 Face Demo/Aisu.SIM1.Face.Samples.asmdef
Normal file
16
Samples~/SI-M1 Face Demo/Aisu.SIM1.Face.Samples.asmdef
Normal 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
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9fa219c8c2a74b94a983c6d4a6a1d001
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/Editor.meta
Normal file
8
Samples~/SI-M1 Face Demo/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4ac476647a08a364289fa596d310bf3a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73502db286784cc3ac4a9e71f06f1002
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 230f42ee40b783c4784659a677f2fc59
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/Scenes.meta
Normal file
8
Samples~/SI-M1 Face Demo/Scenes.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e2eeeccfb35f134ba7a1a0b29dfb214
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
227
Samples~/SI-M1 Face Demo/Scenes/FaceModuleDemo.unity
Normal file
227
Samples~/SI-M1 Face Demo/Scenes/FaceModuleDemo.unity
Normal 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}
|
||||
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89768ad84552b5c4d8e5b584c216af96
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/Scripts.meta
Normal file
8
Samples~/SI-M1 Face Demo/Scripts.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3a1a87e4b672fa4094e73f8b45ee1c0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/Scripts/Core.meta
Normal file
8
Samples~/SI-M1 Face Demo/Scripts/Core.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e8643660e200f554192fa67ef682dd1a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
1347
Samples~/SI-M1 Face Demo/Scripts/Core/FaceModuleKeyboardTester.cs
Normal file
1347
Samples~/SI-M1 Face Demo/Scripts/Core/FaceModuleKeyboardTester.cs
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3dd02cac73721224e87791793b4aeb56
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/UI.meta
Normal file
8
Samples~/SI-M1 Face Demo/UI.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e1ab66bdb929164dbce2d25e3992716
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/UI/Common.meta
Normal file
8
Samples~/SI-M1 Face Demo/UI/Common.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f7382f0bef5512b428ee4431880a6b1b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
173
Samples~/SI-M1 Face Demo/UI/Common/FaceModuleDemoBootstrap.cs
Normal file
173
Samples~/SI-M1 Face Demo/UI/Common/FaceModuleDemoBootstrap.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55824872760a6554ba347ca37e222eff
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
927
Samples~/SI-M1 Face Demo/UI/Common/FaceModuleUiCommon.cs
Normal file
927
Samples~/SI-M1 Face Demo/UI/Common/FaceModuleUiCommon.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5590054c8f656004eb4838ef5bf27ab9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/UI/Pages.meta
Normal file
8
Samples~/SI-M1 Face Demo/UI/Pages.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3750291d4457c4439cae8c383d2aadd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
511
Samples~/SI-M1 Face Demo/UI/Pages/FaceModuleAdvancedPages.cs
Normal file
511
Samples~/SI-M1 Face Demo/UI/Pages/FaceModuleAdvancedPages.cs
Normal 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("固件升级命令已发送");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef28d55dc2ea2224989c843c65b02044
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
940
Samples~/SI-M1 Face Demo/UI/Pages/FaceModulePages.cs
Normal file
940
Samples~/SI-M1 Face Demo/UI/Pages/FaceModulePages.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Samples~/SI-M1 Face Demo/UI/Pages/FaceModulePages.cs.meta
Normal file
11
Samples~/SI-M1 Face Demo/UI/Pages/FaceModulePages.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39833480969093f4986d10e2814fe719
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Samples~/SI-M1 Face Demo/UI/ViewModels.meta
Normal file
8
Samples~/SI-M1 Face Demo/UI/ViewModels.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f03a13c9bc3e394788e8c26d2f5cfeb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 206ba1b1876c1a744abd773518b8fc61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
20
package.json
20
package.json
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user