优化无线/有线CAN,以及控制指令解析
This commit is contained in:
+125
-42
@@ -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 已完成")
|
||||
|
||||
Reference in New Issue
Block a user