优化无线/有线CAN,以及控制指令解析

This commit is contained in:
minghanchen
2026-08-26 20:11:09 +08:00
parent 9ac8fdface
commit 4a0570706f
49 changed files with 8409 additions and 1289 deletions
+125 -42
View File
@@ -2478,8 +2478,8 @@ function twinDeviceForSeat(seat, actuatorState = state.seatTwin?.actuatorState |
...device,
position: {
...position,
// The seat-domain compatibility key is cushion_tilt, but seat-ota
// board axis 1 is physical height. Keep the 3D cushion neutral and
// The seat-domain compatibility key is cushion_tilt, but the native
// CAN0 firmware axis 1 is physical height. Keep the 3D cushion neutral and
// animate/display the firmware feedback as height.
cushion_tilt: { position_pct: 0, position_valid: true },
height: seatHeight,
@@ -4323,6 +4323,33 @@ function selectedVehicleTransport() {
|| null;
}
function isHardwareExecutionReady() {
return Boolean(
state.runtime?.hardware?.configured || selectedVehicleTransport()?.configured,
);
}
function hardwareExecutionIssue(
hardware = state.runtime?.hardware || {},
transport = selectedVehicleTransport() || {},
) {
const stateError = String(transport?.state_sync?.last_error || "").trim();
if (/VCI_OpenDevice failed/i.test(stateError)) {
return "ZLG 设备打开失败:请检查 USB 读写权限,或关闭占用适配器的其他程序";
}
if (stateError) return stateError;
if (transport.state_configured === true && transport.configured !== true) {
return "CAN 状态监听已配置,但真实控制授权尚未配置";
}
if (state.controlTransport === "wireless_mqtt") {
return "无线 MQTT 控制后端尚未配置";
}
if (hardware.enabled === true && hardware.adapter_configured !== true) {
return "副驾 CH0 CAN 适配器尚未配置";
}
return "ZLG 有线 CAN 后端尚未通过执行配置校验";
}
function isDriverOnlyCh1Runtime() {
const transport = selectedVehicleTransport();
return transport?.configured === true
@@ -4343,7 +4370,9 @@ function renderControlTransport() {
// shown as offline, but it must never disable or silently change the
// operator's wired/wireless selection.
button.disabled = false;
button.dataset.connectionState = runtime?.state_sync?.state === "online" ? "online" : "offline";
button.dataset.connectionState = ["online", "ready"].includes(runtime?.state_sync?.state)
? "online"
: "offline";
button.title = runtime?.state_configured === true
? runtime?.state_sync?.last_error || ""
: `${id === "wireless_mqtt" ? "无线 MQTT" : "有线 CAN"} 当前未配置(仍可手动选择)`;
@@ -4362,8 +4391,16 @@ function renderControlTransport() {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
note.textContent = `MQTT ${connection} · 状态 ${selected?.condition_topic || "n7/n7-headunit/can/state"} · 控制 ${selected?.control_topic || "n7/n7-headunit/can/command"}`;
} else {
const connection = selected?.state_sync?.state === "online" ? "已连接" : "未连接";
note.textContent = `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
const connection = selected?.state_sync?.state === "online"
? "已连接"
: selected?.state_sync?.state === "ready"
? "已配置"
: "未连接";
const dualChannel = selected?.target_policy
=== "dual_channel_wired_can0_passenger_can1_vehicle";
note.textContent = dualChannel
? `ZLG 有线 ${connection} · CAN0 副驾经典 CAN 500k · CAN1 车辆 CAN FD 500k/2M`
: `ZLG 有线 ${connection} · CH${selected?.channel ?? 1} · CAN FD 500k/2M`;
}
}
@@ -4445,11 +4482,7 @@ function updateSeatFeedbackModeLabel() {
}
function selectExecutionMode(mode, { remember = true } = {}) {
const selectedTransport = selectedVehicleTransport();
const hardwareExecutionReady = Boolean(
state.runtime?.hardware?.configured || selectedTransport?.configured,
);
if (mode === "hardware" && !hardwareExecutionReady) return false;
const hardwareExecutionReady = isHardwareExecutionReady();
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return false;
if (remember) state.preferredExecutionMode = mode;
state.executionMode = mode;
@@ -4476,6 +4509,9 @@ function selectExecutionMode(mode, { remember = true } = {}) {
? "真实执行"
: simulatedHardware ? "运行故障仿真" : "生成并执行";
}
if (remember && mode === "hardware" && !hardwareExecutionReady) {
showToast(`已切换到真实台架;当前不可执行:${hardwareExecutionIssue()}`);
}
updateSeatFeedbackModeLabel();
refreshIcons();
return true;
@@ -5132,7 +5168,9 @@ function modeEditorReadableCommands(flow) {
Object.values(plans).forEach((plan) => {
modeEditorPlanCommands(plan).forEach((item) => {
const debugCommand = String(item?.signals?.debug_command || item?.message || "CAN frame");
const channel = modeEditorCanId(item?.signals?.frame_id) === "0x6A0"
const channel = new Set(["0x29E", "0x300", "0x3CE"]).has(
modeEditorCanId(item?.signals?.frame_id),
)
? "CAN0"
: "CAN1";
const readable = { channel, category: modeEditorCommandCategory(debugCommand), command: debugCommand };
@@ -5798,7 +5836,13 @@ async function loadRuntime() {
syncReasoningControls();
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
const hardwareButton = $("#hardwareMode");
hardwareButton.disabled = !hardwareExecutionReady;
// “真实台架”是操作员手动选择的执行目标。硬件暂不可用时仍允许
// 选择该目标并展示诊断信息;真正的控制提交继续由后端安全门拦截。
hardwareButton.disabled = false;
hardwareButton.dataset.executionState = hardwareExecutionReady ? "available" : "blocked";
hardwareButton.title = hardwareExecutionReady
? "真实台架已通过后端执行配置校验"
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
const simulation = state.runtime.simulated_hardware || {};
$("#simulationMode").disabled = !simulation.enabled;
$("#simulationMode").title = simulation.enabled
@@ -5822,7 +5866,7 @@ async function loadRuntime() {
? `${mqttDualChannel ? "MQTT CAN0 副驾座椅 · MQTT CAN1 其余车辆" : `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s`} · ${vehicleRouteText}${mqttDualChannel ? " · 远端自动烧录禁用" : ` · ${profileText} · ${flashText}`}${accessText}${concurrencyText}`
: seatHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
: `真实执行已锁定:${profileText};当前${mqttSelected ? "无线 MQTT" : "有线 CAN"}控制未通过后端审批配置`;
: `可手动选择真实台架;当前不可执行:${hardwareExecutionIssue(hardware, vehicleCan)}`;
$("#hardwareAliasNote").textContent = ch1DriverOnly && vehicleHardwareReady && !seatHardwareReady
? `${mqttSelected ? "MQTT" : "CH1"} → 车身 CAN / 主驾座椅 · CH0 与副驾 T2G 路径禁止`
: linkedHardwareReady
@@ -5839,9 +5883,7 @@ async function loadRuntime() {
: "已阻断";
$("#hardwareCoverage").className = hardwareExecutionReady ? "available" : "blocked";
const preferredMode = state.preferredExecutionMode || DEFAULT_EXECUTION_MODE;
const modeAvailable = preferredMode === "hardware"
? hardwareExecutionReady
: preferredMode === "simulated_hardware"
const modeAvailable = preferredMode === "simulated_hardware"
? simulation.enabled
: true;
selectExecutionMode(modeAvailable ? preferredMode : "dry_run", { remember: false });
@@ -6725,6 +6767,7 @@ const CONFIG_CAPABILITY_LABELS = {
"window.set_motion": "控制车窗动作",
"window.set_position": "设置车窗开度",
"outlet.set_position_percent": "设置电动风口位置",
"outlet.set_switch_state": "开关单个电动风口",
"outlet.set_calibrated_position": "设置电动风口标定位置",
"interior_light.set_indoor_power": "设置室内灯",
"interior_light.set_wall_power": "设置壁灯",
@@ -6991,6 +7034,38 @@ function renderCode(result) {
}
}
function operationVerificationStatus(item = {}) {
const explicit = String(item.telemetry?.verification_status || "");
if (["verified_success", "submitted_unverified", "failed_or_blocked"].includes(explicit)) {
return explicit;
}
if (item.status !== "succeeded") return "failed_or_blocked";
if (item.telemetry?.physical_effect_confirmed === true || item.telemetry?.feedback_frame) {
return "verified_success";
}
if (item.telemetry?.command_frame
|| ["submitted", "queued", "published"].includes(item.telemetry?.delivery_status)) {
return "submitted_unverified";
}
return "verified_success";
}
function executionVerificationStatus(result = {}) {
const explicit = String(
result.execution_outcome
|| result.execution_report?.spec?.verification_status
|| "",
);
if (["verified_success", "submitted_unverified", "failed_or_blocked"].includes(explicit)) {
return explicit;
}
const operations = result.execution_report?.spec?.results || [];
const statuses = operations.map(operationVerificationStatus);
if (statuses.includes("failed_or_blocked")) return "failed_or_blocked";
if (statuses.includes("submitted_unverified")) return "submitted_unverified";
return "verified_success";
}
function renderExecution(result) {
const summary = $("#executionSummary");
const list = $("#resultList");
@@ -7011,9 +7086,11 @@ function renderExecution(result) {
refreshIcons();
return;
}
const verificationStatus = executionVerificationStatus(result);
const submittedUnverified = verificationStatus === "submitted_unverified";
const reportSucceeded = report.spec.status === "succeeded";
const partiallySucceeded = result.status === "partially_succeeded";
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
summary.className = `execution-summary ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile);
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
@@ -7024,14 +7101,12 @@ function renderExecution(result) {
const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware");
const succeededOperations = operationResults.filter((item) => item.status === "succeeded");
const failedOperations = operationResults.filter((item) => item.status === "failed");
const mqttSubmittedOperations = operationResults.filter(
(item) => ["queued", "published"].includes(item.telemetry?.delivery_status),
const submittedOperations = operationResults.filter(
(item) => operationVerificationStatus(item) === "submitted_unverified",
);
const physicallyConfirmedOperations = operationResults.filter(
(item) => item.telemetry?.physical_effect_confirmed === true,
);
const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
&& mqttSubmittedOperations.length > physicallyConfirmedOperations.length;
const succeededDomains = [...new Set(succeededOperations.map((item) => item.domain))];
const failedDomains = [...new Set(failedOperations.map((item) => item.domain))];
let executionDetail = modeSave
@@ -7041,8 +7116,10 @@ function renderExecution(result) {
: simulatedHil
? `${executedDomains.join(" / ")} 已完成隔离 HIL 仿真,未访问物理 CAN`
: physical
? result.control_transport === "wireless_mqtt"
? `${executedDomains.join(" / ")} 已向 MQTT 提交 ${mqttSubmittedOperations.length || succeededOperations.length} 个白名单控制步骤${mqttPublishedUnconfirmed ? ";车辆效果尚未确认,以状态话题回读为准" : `,并确认 ${physicallyConfirmedOperations.length || succeededOperations.length} 个车辆状态`}`
? submittedUnverified
? `${executedDomains.join(" / ")} 已通过${result.control_transport === "wireless_mqtt" ? " MQTT" : "有线 CAN"}提交 ${submittedOperations.length || succeededOperations.length} 个白名单控制步骤;车辆效果尚未确认,以实时状态回读为准`
: result.control_transport === "wireless_mqtt"
? `${executedDomains.join(" / ")} 已向 MQTT 提交 ${succeededOperations.length} 个白名单控制步骤,并确认 ${physicallyConfirmedOperations.length || succeededOperations.length} 个车辆状态`
: selectedVehicleTransport()?.target_policy === "ch1_body_can_driver_only"
? `${executedDomains.join(" / ")} 已通过 CH1 下发,并以车端状态字段确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
@@ -7056,7 +7133,7 @@ function renderExecution(result) {
if (result.deferred_requests?.length) {
executionDetail += ";未接入域请求已保留为 deferred request";
}
if (!reportSucceeded && !partiallySucceeded) {
if (verificationStatus === "failed_or_blocked" && !partiallySucceeded) {
const failure = report.spec.results.find((item) => item.status === "failed");
const failureTelemetry = failure?.telemetry || {};
executionDetail = simulatedHil
@@ -7082,26 +7159,31 @@ function renderExecution(result) {
: presetSave ? "姿态预设已保存"
: simulatedHil ? "模拟台架执行成功"
: physical
? mqttPublishedUnconfirmed ? "MQTT 指令已提交" : "真实台架执行成功"
? submittedUnverified
? `${result.control_transport === "wireless_mqtt" ? "MQTT" : "CAN"} 指令已提交,等待状态确认`
: "真实台架执行并验证成功"
: "虚拟执行成功"
: partiallySucceeded ? (physical ? "真实台架部分执行成功" : "部分执行成功")
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
const executionIcon = reportSucceeded
const executionIcon = submittedUnverified
? "circle-alert"
: reportSucceeded
? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big")
: partiallySucceeded ? "circle-alert" : "circle-x";
summary.innerHTML = `<i data-lucide="${executionIcon}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`;
$("#resultNavMeta").textContent = reportSucceeded
? mqttPublishedUnconfirmed
? `${mqttSubmittedOperations.length} 项已提交 · MQTT`
? submittedUnverified
? `${submittedOperations.length || succeededOperations.length} 项已提交 · 等待状态确认`
: `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
: partiallySucceeded ? `${succeededOperations.length} 项成功 / ${failedOperations.length} 项失败 · 查看明细`
: "执行失败 · 查看回读";
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : partiallySucceeded ? "PARTIAL" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
$("#resultPanelState").textContent = submittedUnverified ? "SUBMITTED" : reportSucceeded ? "VERIFIED" : partiallySucceeded ? "PARTIAL" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${submittedUnverified || partiallySucceeded ? "partial" : reportSucceeded ? "succeeded" : "failed"}`;
report.spec.results.forEach((item) => {
const row = document.createElement("div");
row.className = "result-item";
row.innerHTML = `<i data-lucide="${item.status === "succeeded" ? "circle-check" : "circle-x"}"></i>`;
const itemVerification = operationVerificationStatus(item);
row.innerHTML = `<i data-lucide="${itemVerification === "verified_success" ? "circle-check" : itemVerification === "submitted_unverified" ? "circle-alert" : "circle-x"}"></i>`;
const text = document.createElement("div");
const title = document.createElement("strong");
title.textContent = item.ref;
@@ -7250,11 +7332,7 @@ function renderResult(result) {
state.result = result;
const reportMode = result.execution_report?.spec?.mode;
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
const mqttPublishedUnconfirmed = result.control_transport === "wireless_mqtt"
&& (result.execution_report?.spec?.results || []).some(
(item) => ["queued", "published"].includes(item.telemetry?.delivery_status)
&& item.telemetry?.physical_effect_confirmed !== true,
);
const submittedUnverified = executionVerificationStatus(result) === "submitted_unverified";
if (feedbackSource === "simulated_hardware") {
state.seatFeedbackSource = "simulated_hardware";
if (result.actuator_state?.passenger) state.seatFeedbackSources.passenger = "simulated_hardware";
@@ -7289,10 +7367,10 @@ function renderResult(result) {
? "长期模式配置已保存"
: result.preset_config
? "座椅姿态预设已保存"
: result.scene_config
? `${domains.length} 域场景闭环完成`
: mqttPublishedUnconfirmed
? `${domainLabels[domains[0]] || "车舱"}控制指令已提交`
: submittedUnverified
? `${domains.length > 1 ? `${domains.length} 域场景` : domainLabels[domains[0]] || "车舱"}控制指令已提交`
: result.scene_config
? `${domains.length} 域场景闭环完成`
: `${domainLabels[domains[0]] || "车舱"}单域闭环完成`
: "请求未执行";
const conversationLabel = result.conversation_id?.slice(-8) || "no-context";
@@ -7302,8 +7380,8 @@ function renderResult(result) {
$("#requestMeta").textContent += ` / reasoning:${result.reasoning_effort_used}`;
}
setRunStatus(
partiallySucceeded ? "partial" : executionSucceeded ? "succeeded" : "failed",
result.status.toUpperCase(),
partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed",
submittedUnverified ? "SUBMITTED_UNVERIFIED" : result.status.toUpperCase(),
);
renderDecision(result);
renderTrace(result.trace_events || []);
@@ -7353,6 +7431,8 @@ async function postControlPhase(path, body, executionMode, onEvent = null) {
phaseEvent.message || phaseEvent.error || "流式控制请求失败",
);
requestError.code = phaseEvent.error;
requestError.outcome = phaseEvent.outcome;
requestError.detail = phaseEvent.detail;
requestError.retryAfter = Number(phaseEvent.retry_after_seconds || 0);
throw requestError;
}
@@ -7572,6 +7652,7 @@ async function submitRequest(event) {
setModeExecutionState(submittedModeProfileId, completed ? "succeeded" : "failed");
}
const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
const submittedUnverified = executionVerificationStatus(payload) === "submitted_unverified";
showToast(supplementWarning || (completed
? (payload.mode_profile
? `${payload.mode_profile.name}已创建并添加到功能模式`
@@ -7579,6 +7660,8 @@ async function submitRequest(event) {
? "已执行可用设备;部分设备未完成,详见执行结果"
: simulated
? "模拟台架执行完成"
: submittedUnverified
? "CAN 控制指令已提交;等待车辆实时状态确认"
: payload.execution_report?.spec?.mode === "hil"
? "真实台架执行与反馈确认已完成"
: "可执行动作的 dry-run 已完成")