feat: update CAN integration and T2G seat firmware

This commit is contained in:
vee
2026-08-01 15:55:21 +08:00
parent 8cda026ac2
commit 4aded5c21d
1443 changed files with 1007122 additions and 4978 deletions
+113 -33
View File
@@ -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();
}