feat: update CAN integration and T2G seat firmware
This commit is contained in:
+113
-33
@@ -20,6 +20,7 @@ const state = {
|
||||
progressTimer: null,
|
||||
seatTwin: null,
|
||||
seatActuatorState: {},
|
||||
seatFeedbackSource: "simulated",
|
||||
availableSeatAxes: null,
|
||||
};
|
||||
|
||||
@@ -41,7 +42,7 @@ const TWIN_DEFAULT_POSITIONS = {
|
||||
};
|
||||
const TWIN_ANGLE_RANGES = {
|
||||
backrest_recline: [95, 150],
|
||||
legrest_height: [95, 163],
|
||||
legrest_height: [95, 167],
|
||||
};
|
||||
|
||||
function initJointTwinLayout() {
|
||||
@@ -104,13 +105,13 @@ function twinPostureAngles(device) {
|
||||
device?.position?.[axis]?.angle_deg
|
||||
?? twinAngleForPosition(axis, position(axis, fallback)),
|
||||
);
|
||||
const cushion = 2 + position("cushion_tilt", 50) * 0.09;
|
||||
const cushion = position("cushion_tilt", 50) * 0.09;
|
||||
return {
|
||||
cushion_tilt: cushion,
|
||||
backrest_recline: included("backrest_recline", 20) - 90 + cushion,
|
||||
legrest_height: Math.abs(
|
||||
180 - cushion - included("legrest_height", 0),
|
||||
),
|
||||
backrest_recline: included("backrest_recline", 20)
|
||||
- TWIN_ANGLE_RANGES.backrest_recline[0],
|
||||
legrest_height: included("legrest_height", 0)
|
||||
- TWIN_ANGLE_RANGES.legrest_height[0],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1492,15 +1493,15 @@ function renderTwinCalloutValues(device) {
|
||||
Number.isInteger(number) ? String(number) : number.toFixed(1)
|
||||
);
|
||||
const values = {
|
||||
longitudinal: [`${longitudinal}%`, longitudinal < 45 ? "后移区" : longitudinal > 55 ? "前移区" : "中位区"],
|
||||
longitudinal: [`${longitudinal}%`, longitudinal < 45 ? "前移区" : longitudinal > 55 ? "后移区" : "中位区"],
|
||||
cushion_tilt: [`${formatAngle(postureAngles.cushion_tilt)}°`, `${cushion}%`],
|
||||
backrest_recline: [
|
||||
`${backrestEstimated ? "≈" : ""}${formatAngle(postureAngles.backrest_recline)}°`,
|
||||
`${backrestEstimated ? "估算 " : ""}${backrest}% · 相对竖直`,
|
||||
`${backrestEstimated ? "估算 " : ""}${backrest}% · 相对归零位`,
|
||||
],
|
||||
legrest_height: [
|
||||
`${legrestEstimated ? "≈" : ""}${formatAngle(postureAngles.legrest_height)}°`,
|
||||
`${legrestEstimated ? "估算 " : ""}${legrest}% · 相对地板`,
|
||||
`${legrestEstimated ? "估算 " : ""}${legrest}% · 相对垂直零位`,
|
||||
],
|
||||
legrest_extension: [`${extension}%`, "伸缩行程"],
|
||||
backrest_included_angle: [
|
||||
@@ -1522,10 +1523,10 @@ function renderTwinCalloutValues(device) {
|
||||
function renderTwinReadout() {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
const seat = twin.selected;
|
||||
const device = twin.actuatorState[seat];
|
||||
$("#twinSeatName").textContent = seat === "driver" ? "主驾" : "副驾";
|
||||
$("#twinSeatZone").textContent = seat === "driver" ? "front_left" : "front_right";
|
||||
const seat = "passenger";
|
||||
const device = twin.actuatorState.passenger;
|
||||
$("#twinSeatName").textContent = "副驾";
|
||||
$("#twinSeatZone").textContent = "front_right";
|
||||
const postureAngles = twinPostureAngles(device);
|
||||
Object.entries(TWIN_DEFAULT_POSITIONS).forEach(([axis, fallback]) => {
|
||||
const motion = device?.position?.[axis];
|
||||
@@ -1554,7 +1555,7 @@ function renderTwinReadout() {
|
||||
function updateTwinExplicitAngles(result) {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
const targets = { driver: {}, passenger: {} };
|
||||
const targets = { passenger: {} };
|
||||
const requestedSeats = new Set();
|
||||
const capabilityAxes = {
|
||||
"seat.set_backrest_recline": "backrest_recline",
|
||||
@@ -2245,7 +2246,7 @@ function initSeatTwin() {
|
||||
vehicleSystems,
|
||||
selected: "passenger",
|
||||
actuatorState: { passenger: initialDevice() },
|
||||
explicitAngleTargets: { driver: {}, passenger: {} },
|
||||
explicitAngleTargets: { passenger: {} },
|
||||
view: { yaw: -0.62, pitch: 0.31, distance: 10.2, targetX: 0 },
|
||||
dragging: false,
|
||||
pointer: { x: 0, y: 0 },
|
||||
@@ -2429,7 +2430,7 @@ function initSeatTwin() {
|
||||
model.angles[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
|
||||
});
|
||||
model.slide.position.z = (model.positions.longitudinal - 50) * 0.012;
|
||||
const cushionAngle = 2 + model.positions.cushion_tilt * 0.09;
|
||||
const cushionAngle = model.positions.cushion_tilt * 0.09;
|
||||
model.cushionPivot.rotation.x = degrees(-cushionAngle);
|
||||
model.backrestPivot.rotation.x = (
|
||||
model.cushionPivot.rotation.x
|
||||
@@ -2503,6 +2504,26 @@ function selectProvider(provider) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSeatFeedbackModeLabel() {
|
||||
const label = $("#seatFeedbackModeLabel");
|
||||
if (!label) return;
|
||||
if (state.seatFeedbackSource === "physical_can") {
|
||||
label.textContent = "PHYSICAL CAN · BENCH SYNCHRONIZED";
|
||||
return;
|
||||
}
|
||||
if (state.seatFeedbackSource === "physical_partial") {
|
||||
label.textContent = "PHYSICAL CAN · PARTIAL FEEDBACK";
|
||||
return;
|
||||
}
|
||||
if (state.seatFeedbackSource === "unavailable") {
|
||||
label.textContent = "PHYSICAL CAN · SYNC UNAVAILABLE";
|
||||
return;
|
||||
}
|
||||
label.textContent = state.executionMode === "hardware"
|
||||
? "PHYSICAL CAN FEEDBACK"
|
||||
: "SIMULATED FEEDBACK";
|
||||
}
|
||||
|
||||
function selectExecutionMode(mode) {
|
||||
if (mode === "hardware" && !state.runtime?.hardware?.configured) return;
|
||||
state.executionMode = mode;
|
||||
@@ -2510,7 +2531,8 @@ function selectExecutionMode(mode) {
|
||||
button.classList.toggle("active", button.dataset.executionMode === mode);
|
||||
});
|
||||
const hardware = mode === "hardware";
|
||||
$("#approvalField").hidden = !hardware;
|
||||
const approvalRequired = Boolean(state.runtime?.hardware?.approval_required);
|
||||
$("#approvalField").hidden = !hardware || !approvalRequired;
|
||||
$("#executionBadge").className = `status-badge ${hardware ? "" : "neutral"}`.trim();
|
||||
$("#executionBadge").innerHTML = hardware
|
||||
? '<i data-lucide="radio-tower"></i>HIL · 真实 CAN'
|
||||
@@ -2523,9 +2545,13 @@ function selectExecutionMode(mode) {
|
||||
$("#feedbackModeLabel").textContent = hardware
|
||||
? "T2G BENCH · PHYSICAL CAN FEEDBACK"
|
||||
: "MATRIX-BACKED · SIMULATED FEEDBACK";
|
||||
$("#seatFeedbackModeLabel").textContent = hardware
|
||||
? "PHYSICAL CAN FEEDBACK"
|
||||
: "SIMULATED FEEDBACK";
|
||||
const runButtonLabel = $("#runButton span");
|
||||
if (runButtonLabel) {
|
||||
runButtonLabel.textContent = hardware
|
||||
? (approvalRequired ? "授权并真实执行" : "真实执行")
|
||||
: "生成并执行";
|
||||
}
|
||||
updateSeatFeedbackModeLabel();
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
@@ -2715,7 +2741,7 @@ async function loadRuntime() {
|
||||
if (state.runtime.openai_available) {
|
||||
badge.classList.remove("danger");
|
||||
badge.innerHTML = `<i data-lucide="sparkles"></i>${state.runtime.openai_model}`;
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 结构化输出`;
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 失败时自动降级本地规则`;
|
||||
selectProvider("openai");
|
||||
} else {
|
||||
badge.classList.add("danger");
|
||||
@@ -2730,18 +2756,21 @@ async function loadRuntime() {
|
||||
hardwareButton.disabled = !hardware.configured;
|
||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||
const approvalText = hardware.approval_required ? "需授权令牌" : "调试免令牌";
|
||||
$("#hardwareNote").textContent = hardware.configured
|
||||
? `${hardware.backend} · ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText}`
|
||||
: `真实执行已锁定:${profileText};请完成服务端启用、令牌、适配器和固件镜像配置`;
|
||||
? `${hardware.backend} · ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · ${approvalText}`
|
||||
: `真实执行已锁定:${profileText};请完成服务端启用、适配器、固件镜像${hardware.approval_required ? "和令牌" : ""}配置`;
|
||||
$("#hardwareAliasNote").textContent = "副驾座椅 → t2g_primary_seat(统一电机、加热和通风执行器)";
|
||||
$("#autoFlashNote").textContent = hardware.auto_flash_enabled
|
||||
? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。"
|
||||
: "仅接受已烧录且身份匹配的统一固件;需要自动烧录时请启用服务端开关。";
|
||||
: "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。";
|
||||
$("#hardwareBadge").className = `status-badge ${hardware.configured ? "" : "danger"}`.trim();
|
||||
$("#hardwareBadge").innerHTML = hardware.configured
|
||||
? '<i data-lucide="shield-check"></i>硬件已配置'
|
||||
: '<i data-lucide="shield-off"></i>硬件锁定';
|
||||
$("#hardwareCoverage").textContent = hardware.configured ? "可选 · 需授权" : "已阻断";
|
||||
$("#hardwareCoverage").textContent = hardware.configured
|
||||
? `可选 · ${approvalText}`
|
||||
: "已阻断";
|
||||
$("#hardwareCoverage").className = hardware.configured ? "available" : "blocked";
|
||||
selectExecutionMode("dry_run");
|
||||
refreshIcons();
|
||||
@@ -2753,6 +2782,37 @@ async function loadRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
async function synchronizePhysicalSeatState() {
|
||||
if (!state.runtime?.hardware?.configured) return false;
|
||||
const query = new URLSearchParams({
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/hardware/seat-state?${query}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
renderSeatState(payload.actuator_state);
|
||||
state.seatFeedbackSource = payload.complete
|
||||
? "physical_can"
|
||||
: "physical_partial";
|
||||
updateSeatFeedbackModeLabel();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
if (!state.seatFeedbackSource.startsWith("physical")) {
|
||||
state.seatFeedbackSource = "unavailable";
|
||||
updateSeatFeedbackModeLabel();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuthStatus() {
|
||||
const response = await fetch("/api/auth/status", { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
@@ -3413,15 +3473,17 @@ function renderExecution(result) {
|
||||
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
|
||||
if (motionCount) executionDetail += `,含 ${motionCount} 个座椅位置电机反馈`;
|
||||
if (firmwareResult?.telemetry?.programmed_this_request) executionDetail += ";本次已自动烧录并校验统一固件";
|
||||
else if (firmwareResult) executionDetail += ";板上统一固件身份已校验";
|
||||
else if (firmwareResult) executionDetail += ";板上统一固件协议签名已校验";
|
||||
if (result.status === "partially_succeeded") {
|
||||
executionDetail += ";未接入域请求已保留为 deferred request";
|
||||
}
|
||||
if (!reportSucceeded) {
|
||||
const failure = report.spec.results.find((item) => item.status === "failed");
|
||||
const failureTelemetry = failure?.telemetry || {};
|
||||
executionDetail = failureTelemetry.error_type === "FirmwareProgrammingError"
|
||||
? "固件身份验证或自动烧录失败,座椅控制帧未发送。"
|
||||
executionDetail = failureTelemetry.error_type === "CanGatewayError"
|
||||
? `CAN 适配器或总线访问失败:${failure?.message || "无法打开或访问 CAN 适配器。"}`
|
||||
: failureTelemetry.error_type === "FirmwareProgrammingError"
|
||||
? `固件协议签名验证或自动烧录失败,座椅控制帧未发送:${failure?.message || "请检查 CMSIS-DAP 探针和 IAR 配置。"}`
|
||||
: failureTelemetry.calibration_required
|
||||
? `大幅相对移动已转换为绝对目标,但 Motor${failureTelemetry.motor_selector} 的 Position_Valid=0;未发送绝对移动命令,请先在台架清空并备好急停后执行显式端点标定。`
|
||||
: failureTelemetry.failure_category === "stall_no_hall_activity"
|
||||
@@ -3432,7 +3494,7 @@ function renderExecution(result) {
|
||||
? `Motor${failureTelemetry.motor_selector} ${failureTelemetry.motor_result_name},板端事件 ${failureTelemetry.last_motor_event_name || "UNAVAILABLE"};${failureTelemetry.stop_all_attempted ? "已尝试发送 STOP_ALL。" : "请检查硬件回读。"}`
|
||||
: failureTelemetry.stop_all_attempted
|
||||
? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。"
|
||||
: "真实台架未返回满足目标的物理反馈。";
|
||||
: failure?.message || "真实台架未返回满足目标的物理反馈。";
|
||||
}
|
||||
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
|
||||
$("#resultNavMeta").textContent = reportSucceeded
|
||||
@@ -3464,6 +3526,14 @@ function renderExecution(result) {
|
||||
|
||||
function renderResult(result) {
|
||||
state.result = result;
|
||||
const reportMode = result.execution_report?.spec?.mode;
|
||||
if (reportMode === "hil") {
|
||||
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
|
||||
? "physical_can"
|
||||
: "physical_partial";
|
||||
} else if (result.function_configs?.seat) {
|
||||
state.seatFeedbackSource = "simulated";
|
||||
}
|
||||
const executionSucceeded = ["succeeded", "partially_succeeded"].includes(result.status);
|
||||
const domains = Object.keys(result.function_configs || {});
|
||||
const domainLabels = {
|
||||
@@ -3493,6 +3563,7 @@ function renderResult(result) {
|
||||
renderTrace(result.trace_events || []);
|
||||
renderValidation(result.validation || []);
|
||||
renderSeatState(result.actuator_state);
|
||||
updateSeatFeedbackModeLabel();
|
||||
renderDomainState(result.domain_state);
|
||||
updateTwinExplicitAngles(result);
|
||||
renderSavedPresets(result);
|
||||
@@ -3518,8 +3589,10 @@ async function submitRequest(event) {
|
||||
$("#resultTitle").textContent = "正在执行车舱编排";
|
||||
startProgress();
|
||||
const approvalTokenInput = $("#approvalToken");
|
||||
const approvalToken = state.executionMode === "hardware"
|
||||
? approvalTokenInput.value
|
||||
const approvalRequired = state.executionMode === "hardware"
|
||||
&& Boolean(state.runtime?.hardware?.approval_required);
|
||||
const approvalToken = approvalRequired
|
||||
? approvalTokenInput?.value || ""
|
||||
: null;
|
||||
if (approvalTokenInput) approvalTokenInput.value = "";
|
||||
|
||||
@@ -3533,7 +3606,7 @@ async function submitRequest(event) {
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
||||
execution_mode: state.executionMode,
|
||||
...(state.executionMode === "hardware"
|
||||
...(approvalRequired
|
||||
? { approval_token: approvalToken }
|
||||
: {}),
|
||||
}),
|
||||
@@ -3545,6 +3618,9 @@ async function submitRequest(event) {
|
||||
}
|
||||
if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
|
||||
renderResult(payload);
|
||||
if (state.executionMode === "hardware") {
|
||||
await synchronizePhysicalSeatState();
|
||||
}
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
showToast(completed
|
||||
? (payload.execution_report?.spec?.mode === "hil" ? "真实台架执行与反馈确认已完成" : "可执行动作的 dry-run 已完成")
|
||||
@@ -3558,7 +3634,10 @@ async function submitRequest(event) {
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.classList.remove("loading");
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? "授权并真实执行" : "生成并执行"}</span>`;
|
||||
const hardwareLabel = state.runtime?.hardware?.approval_required
|
||||
? "授权并真实执行"
|
||||
: "真实执行";
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? hardwareLabel : "生成并执行"}</span>`;
|
||||
refreshIcons();
|
||||
}
|
||||
}
|
||||
@@ -3684,6 +3763,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
$$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download)));
|
||||
await loadAuthStatus();
|
||||
await loadRuntime();
|
||||
await synchronizePhysicalSeatState();
|
||||
if (new URLSearchParams(window.location.search).get("autorun") === "1") {
|
||||
$("#requestForm").requestSubmit();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-22" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-23" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -92,7 +92,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">主驾和副驾暂时共享同一组实体电机、加热与通风执行器</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
@@ -108,8 +108,8 @@
|
||||
|
||||
<div class="field-group approval-field" id="approvalField" hidden>
|
||||
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
|
||||
<input id="approvalToken" name="approval_token" type="password" inputmode="numeric" maxlength="6" autocomplete="off" spellcheck="false" placeholder="请输入 123456" />
|
||||
<p class="provider-note">演示授权令牌为 123456,仅用于本次安全校验,不会写入日志、会话或执行制品。</p>
|
||||
<input id="approvalToken" name="approval_token" type="password" maxlength="128" autocomplete="off" spellcheck="false" placeholder="请输入服务端配置令牌" />
|
||||
<p class="provider-note">仅在服务端启用令牌门禁时显示;令牌不会写入日志、会话或执行制品。</p>
|
||||
<p class="provider-note" id="autoFlashNote">正在检查自动烧录配置</p>
|
||||
</div>
|
||||
|
||||
@@ -298,9 +298,9 @@
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾动态座椅</span><small>ACTIVE · FRONT RIGHT</small></div>
|
||||
<div class="twin-callout-layer" aria-hidden="true">
|
||||
<div class="twin-callout" data-callout="longitudinal"><span>水平位置</span><b>50%</b><small>导轨</small></div>
|
||||
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>7°</b><small>50%</small></div>
|
||||
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>20.5°</b><small>20% · 相对竖直</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>78.5°</b><small>0% · 相对地板</small></div>
|
||||
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>4.5°</b><small>50%</small></div>
|
||||
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>13.6°</b><small>20% · 相对归零位</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>0°</b><small>0% · 相对垂直零位</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_extension"><span>腿托伸缩</span><b>0%</b><small>行程</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="backrest_included_angle" hidden><span>靠背-坐垫夹角</span><b>104°</b><small>本轮指定夹角</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="legrest_included_angle" hidden><span>坐垫-腿托夹角</span><b>95°</b><small>本轮指定夹角</small></div>
|
||||
@@ -338,18 +338,10 @@
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><span class="section-kicker">ACTUATOR STATE</span><h3>前排座椅状态</h3></div>
|
||||
<div><span class="section-kicker">ACTUATOR STATE</span><h3>副驾座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾</strong><span>front_left</span></div></div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div>
|
||||
<div class="thermal-states">
|
||||
|
||||
Reference in New Issue
Block a user