refactor(serial): 新增 hand_gateway 编解码模块,修正模块名拼写

This commit is contained in:
lenn
2026-06-22 17:44:07 +08:00
parent b343e74a12
commit 91307f1985
9 changed files with 625 additions and 170 deletions

View File

@@ -19,44 +19,15 @@
const FLOOR_Y = -1.15;
const MODEL_FLOOR_CLEARANCE = 0.035;
const MODEL_TARGET_HEIGHT = 7.4;
const MODEL_TARGET_HEIGHT = 6.55;
const MODEL_MIN_SCALE = 0.02;
const MODEL_MAX_SCALE = 80;
const FIXED_CAMERA_POSITION = new THREE.Vector3(0.8, 3.25, 12.2);
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.55, 0);
const FIXED_CAMERA_POSITION = new THREE.Vector3(-0.113, -0.149, 10.911);
const FIXED_CAMERA_TARGET = new THREE.Vector3(0, 2.35, 0);
const MODEL_FRONT_ROTATION_Y = -Math.PI / 2;
const MODEL_UPRIGHT_ROTATION_Z = Math.PI * 1.5;
const MODEL_UPRIGHT_ROTATION_Z = Math.PI * 1.5 - THREE.MathUtils.degToRad(2.2);
$: copy =
locale === "zh-CN"
? {
title: "3D 模型舱",
subtitle: "Dark Grid / Future Lab",
loading: "正在加载模型",
ready: "模型已载入",
missing: "等待模型文件",
error: "模型加载失败",
modelPath: "模型路径",
hint: "请使用 glTF 2.0 的 .glb/.gltf旧版 glTF 1.0 需要先转换"
}
: {
title: "3D Model Bay",
subtitle: "Dark Grid / Future Lab",
loading: "Loading model",
ready: "Model loaded",
missing: "Waiting for model file",
error: "Model load failed",
modelPath: "Model path",
hint: "Use glTF 2.0 .glb/.gltf assets; older glTF 1.0 files need conversion first"
};
$: statusText =
loadState === "ready"
? copy.ready
: loadState === "missing"
? copy.missing
: loadState === "error"
? copy.error
: `${copy.loading} ${Math.round(loadProgress)}%`;
$: canvasLabel = locale === "zh-CN" ? "3D 模型" : "3D Model";
function disposeObject3D(object: THREE.Object3D): void {
object.traverse((child) => {
@@ -347,20 +318,9 @@
</script>
<div class="model-stage" bind:this={rootEl}>
<canvas class="model-canvas" bind:this={canvasEl} aria-label={copy.title}></canvas>
<canvas class="model-canvas" bind:this={canvasEl} aria-label={canvasLabel}></canvas>
<div class="model-vignette" aria-hidden="true"></div>
<div class="model-scanlines" aria-hidden="true"></div>
<section class="model-hud" aria-label={copy.title}>
<p class="model-kicker">{copy.subtitle}</p>
<h2>{copy.title}</h2>
<div class="model-status-row">
<span class="status-light" class:is-ready={loadState === "ready"}></span>
<span>{statusText}</span>
</div>
<p class="model-path">{copy.modelPath}: {modelUrl}</p>
<p class="model-hint">{loadError || copy.hint}</p>
</section>
</div>
<style>
@@ -405,82 +365,4 @@
mix-blend-mode: screen;
}
.model-hud {
position: absolute;
top: clamp(1.2rem, 2.8vw, 2.2rem);
left: clamp(1.2rem, 2.8vw, 2.4rem);
z-index: 2;
display: grid;
gap: 0.42rem;
max-inline-size: min(22rem, 42vw);
padding: 0.9rem 1rem 1rem;
border: 1px solid rgb(94 231 255 / 0.24);
border-radius: 0.7rem;
background:
linear-gradient(180deg, rgb(8 18 28 / 0.82), rgb(3 9 15 / 0.72)),
radial-gradient(circle at 0 0, rgb(94 231 255 / 0.1), transparent 44%);
box-shadow:
inset 0 1px 0 rgb(255 255 255 / 0.06),
0 0 28px rgb(94 231 255 / 0.08);
backdrop-filter: blur(10px);
}
.model-kicker,
.model-path,
.model-hint {
margin: 0;
color: rgb(198 226 239 / 0.72);
font-size: 0.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
line-height: 1.35;
}
h2 {
margin: 0;
color: rgb(241 251 255 / 0.96);
font-size: clamp(1.15rem, 1.1vw + 0.88rem, 1.72rem);
line-height: 1.05;
font-weight: 650;
}
.model-status-row {
display: inline-flex;
align-items: center;
gap: 0.44rem;
color: rgb(229 249 255 / 0.94);
font-size: 0.78rem;
letter-spacing: 0.04em;
}
.status-light {
inline-size: 0.58rem;
block-size: 0.58rem;
border-radius: 50%;
background: rgb(255 188 92 / 0.95);
box-shadow: 0 0 0 2px rgb(255 188 92 / 0.16), 0 0 12px rgb(255 188 92 / 0.18);
}
.status-light.is-ready {
background: rgb(166 255 122 / 0.95);
box-shadow: 0 0 0 2px rgb(166 255 122 / 0.16), 0 0 14px rgb(166 255 122 / 0.22);
}
.model-path {
color: rgb(94 231 255 / 0.78);
text-transform: none;
word-break: break-word;
}
.model-hint {
color: rgb(198 226 239 / 0.66);
text-transform: none;
letter-spacing: 0.04em;
}
@media (max-width: 960px) {
.model-hud {
max-inline-size: min(20rem, calc(100% - 2.4rem));
}
}
</style>

View File

@@ -306,6 +306,10 @@
return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window;
}
function serialProtocolForStage(mode: StageViewMode = stageViewMode): "tactileA" | "handGateway" {
return mode === "model3d" ? "handGateway" : "tactileA";
}
function clearDevkitSpatialForce(): void {
devkitSpatialForce = null;
if (devkitSpatialForceClearTimer != null && typeof window !== "undefined") {
@@ -1445,7 +1449,10 @@
connectionNotice = "";
try {
const result = await invoke<SerialConnectResult>("serial_connect", { port: event.detail });
const result = await invoke<SerialConnectResult>("serial_connect", {
port: event.detail,
protocol: serialProtocolForStage()
});
connectionState = result.connected ? "online" : "offline";
serialPortValue = result.port;
connectionNotice = "";
@@ -1800,17 +1807,48 @@
matrixDisplayMode = event.detail ? "dots" : "numeric";
}
function handleStageModeChange(event: CustomEvent<StageViewMode>): void {
async function handleStageModeChange(event: CustomEvent<StageViewMode>): Promise<void> {
const previousMode = stageViewMode;
stageViewMode = event.detail;
if (stageViewMode === "model3d") {
isPrecisionTestOpen = false;
isConfigPanelOpen = false;
}
if (
previousMode === stageViewMode ||
connectionState !== "online" ||
!serialPortValue ||
!isTauriRuntime()
) {
return;
}
connectionState = "connecting";
connectionNotice = "";
try {
await invoke<SerialConnectResult>("serial_disconnect");
const result = await invoke<SerialConnectResult>("serial_connect", {
port: serialPortValue,
protocol: serialProtocolForStage(stageViewMode)
});
connectionState = result.connected ? "online" : "offline";
serialPortValue = result.port;
clearHudPanels();
console.info(`[serial] switched protocol to ${serialProtocolForStage(stageViewMode)}`);
} catch (error) {
connectionState = "offline";
connectionNotice = resolveSerialNotice(error, "connect");
connectionNoticeTone = "warn";
clearHudPanels();
console.error("Serial protocol switch failed:", error);
}
}
onMount(() => {
let disposed = false;
let unlistenHudStream: UnlistenFn | null = null;
let unlistenHandGatewayRaw: UnlistenFn | null = null;
let unlistenDevkitPztAngle: UnlistenFn | null = null;
let stopMockFeed: (() => void) | null = null;
@@ -1835,6 +1873,23 @@
.catch((error) => {
console.error("Failed to listen for hud_stream:", error);
});
void listen<number[]>("hand_gateway_raw", (event) => {
const rawHex = event.payload
.map((byte) => byte.toString(16).padStart(2, "0").toUpperCase())
.join(" ");
console.info(`[hand_gateway][raw] ${rawHex}`);
})
.then((unlisten) => {
if (disposed) {
unlisten();
return;
}
unlistenHandGatewayRaw = unlisten;
})
.catch((error) => {
console.error("Failed to listen for hand_gateway_raw:", error);
});
void listen<DevKitPztAngleEvent>("devkit_pzt_angle", (event) => {
const angleDeg = Number(event.payload.angle);
if (!Number.isFinite(angleDeg)) {
@@ -1875,6 +1930,7 @@
clearDevkitSpatialForce();
stopMockFeed?.();
unlistenHudStream?.();
unlistenHandGatewayRaw?.();
unlistenDevkitPztAngle?.();
if (devkitStatusTimer != null) {
window.clearInterval(devkitStatusTimer);