feat: validate real-bench seat orchestration and self-repair

This commit is contained in:
vee
2026-08-09 19:00:16 +08:00
parent 3bceb0d5a1
commit 3701192a11
41 changed files with 10403 additions and 3392 deletions
+23 -8
View File
@@ -3437,7 +3437,7 @@ function updateSeatFeedbackModeLabel() {
}
function selectExecutionMode(mode) {
if (mode === "hardware" && !(state.runtime?.hardware?.configured && state.runtime?.vehicle_can?.configured)) return;
if (mode === "hardware" && !state.runtime?.hardware_execution_allowed) return;
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
state.executionMode = mode;
$$('[data-execution-mode]').forEach((button) => {
@@ -3446,6 +3446,7 @@ function selectExecutionMode(mode) {
const hardware = mode === "hardware";
const simulatedHardware = mode === "simulated_hardware";
const approvalRequired = Boolean(state.runtime?.hardware?.approval_required);
const vehicleCanReady = Boolean(state.runtime?.vehicle_can?.configured);
$("#approvalField").hidden = !hardware || !approvalRequired;
$("#simulationFaultField").hidden = !simulatedHardware;
$("#executionBadge").className = `status-badge ${hardware ? "" : simulatedHardware ? "warning" : "neutral"}`.trim();
@@ -3458,7 +3459,9 @@ function selectExecutionMode(mode) {
? "真实台架安全门禁"
: simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
$("#safetyText").textContent = hardware
? "同一 CAN 盒由单进程持有:通道 0 控制副驾座椅并校验板端反馈,通道 1 以 ISO CAN FD 500 kbit/s / 2 Mbit/s 控制空调等车身设备并校验状态帧。"
? vehicleCanReady
? "同一 CAN 盒由单进程持有:通道 0 控制副驾座椅并校验板端反馈,通道 1 以 ISO CAN FD 500 kbit/s / 2 Mbit/s 控制空调等车身设备并校验状态帧。"
: "当前仅通道 0 连接副驾座椅台架并校验板端反馈;车端 CAN 未启用,模式中的其他车舱领域保持虚拟执行。"
: simulatedHardware
? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。"
: "四域 CAN 均为 dry-run,真实发送被强制关闭。";
@@ -3716,6 +3719,8 @@ async function loadRuntime() {
const hardware = state.runtime.hardware || {};
const vehicleCan = state.runtime.vehicle_can || {};
const linkedHardwareReady = Boolean(hardware.configured && vehicleCan.configured);
const seatHardwareReady = Boolean(hardware.configured);
const hardwareExecutionReady = Boolean(state.runtime.hardware_execution_allowed);
const badge = $("#modelBadge");
const gptButton = $('[data-provider="openai"]');
if (state.runtime.openai_available) {
@@ -3734,7 +3739,7 @@ async function loadRuntime() {
}
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
const hardwareButton = $("#hardwareMode");
hardwareButton.disabled = !linkedHardwareReady;
hardwareButton.disabled = !hardwareExecutionReady;
const simulation = state.runtime.simulated_hardware || {};
$("#simulationMode").disabled = !simulation.enabled;
$("#simulationMode").title = simulation.enabled
@@ -3751,21 +3756,31 @@ async function loadRuntime() {
: "";
$("#hardwareNote").textContent = linkedHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · CH${vehicleCan.channel} 车身 CAN FD ${vehicleCan.arbitration.bitrate / 1000}k/${vehicleCan.data.bitrate / 1000000}M · ${profileText} · ${flashText} · ${approvalText}${accessText}${concurrencyText}`
: seatHardwareReady
? `${hardware.backend} · CH${hardware.channel} 座椅 ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · 车端 CAN 未启用,其他领域保持虚拟执行${accessText}${concurrencyText}`
: `联动真实执行已锁定:${profileText};请完成座椅固件、双通道 ZLG 适配器、车身 CAN FD${hardware.approval_required ? "和令牌" : ""}配置`;
$("#hardwareAliasNote").textContent = "CH0 → 副驾座椅控制器 · CH1 → 空调/车窗/门锁/灯光/遮阳帘(ISO CAN FD)";
$("#hardwareAliasNote").textContent = linkedHardwareReady
? "CH0 → 副驾座椅控制器 · CH1 → 车身 CAN FD"
: seatHardwareReady
? `CH${hardware.channel} → 副驾座椅控制器 · 车身控制暂用虚拟 CAN`
: "尚未配置可执行的物理 CAN 通道";
$("#autoFlashNote").textContent = hardware.auto_flash_enabled
? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。"
: "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。";
$("#hardwareBadge").className = `status-badge ${linkedHardwareReady ? "" : "danger"}`.trim();
$("#hardwareBadge").className = `status-badge ${hardwareExecutionReady ? "" : "danger"}`.trim();
$("#hardwareBadge").innerHTML = linkedHardwareReady
? '<i data-lucide="shield-check"></i>双通道已配置'
: seatHardwareReady
? '<i data-lucide="shield-check"></i>座椅台架已配置'
: '<i data-lucide="shield-off"></i>硬件锁定';
$("#hardwareCoverage").textContent = linkedHardwareReady
? `CH0 座椅 + CH${vehicleCan.channel} 车身 · ${approvalText}${accessText}${concurrencyText}`
: "已阻断";
$("#hardwareCoverage").className = linkedHardwareReady ? "available" : "blocked";
: seatHardwareReady
? `CH${hardware.channel} 座椅 · 物理反馈${accessText}${concurrencyText}`
: "已阻断";
$("#hardwareCoverage").className = hardwareExecutionReady ? "available" : "blocked";
const modeAvailable = state.executionMode === "hardware"
? linkedHardwareReady
? hardwareExecutionReady
: state.executionMode === "simulated_hardware"
? simulation.enabled
: true;
+4 -4
View File
@@ -13,20 +13,20 @@
<link rel="stylesheet" href="/console-v2.css?v=desktop-refactor-68" />
<link rel="stylesheet" href="/self-extension.css?v=extension-9" />
<link rel="stylesheet" href="/self-calibration.css?v=calibration-4" />
<link rel="stylesheet" href="/self-repair.css?v=repair-15" />
<link rel="stylesheet" href="/self-repair.css?v=repair-17" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-1" defer></script>
<script src="/app.js?v=seat-hil-43" defer></script>
<script src="/app.js?v=seat-hil-44" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-6"></script>
<script src="/pwa.js?v=android-pad-1" defer></script>
<script src="/console-v2.js?v=desktop-refactor-28" defer></script>
<script src="/self-extension.js?v=extension-6" defer></script>
<script src="/self-calibration.js?v=calibration-2" defer></script>
<script src="/self-repair.js?v=repair-15" defer></script>
<script src="/self-repair-seat3d.js?v=repair-15" defer></script>
<script src="/self-repair.js?v=repair-17" defer></script>
<script src="/self-repair-seat3d.js?v=repair-17" defer></script>
</head>
<body>
<header class="topbar">
+4
View File
@@ -1270,6 +1270,10 @@
.v2-repair-dialog th,
.v2-repair-dialog td { padding: 7px; border-bottom: 1px solid var(--repair-line); text-align: left; }
.v2-repair-dialog th { color: var(--repair-muted); background: var(--repair-soft-strong); }
.v2-repair-confirm-dialog { width: min(460px, calc(100vw - 32px)); }
.v2-repair-confirm-dialog #repairConfirmMessage { margin: 0; color: var(--repair-ink); font-size: 13px; line-height: 1.7; }
.v2-repair-confirm-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }
.v2-repair-confirm-actions button { min-width: 104px; }
.v2-repair-report-path { padding: 7px 9px; overflow-wrap: anywhere; color: var(--repair-muted); background: var(--repair-soft-strong); border-radius: 8px; font: 8px "IBM Plex Mono", Consolas, monospace; }
.v2-repair-event-list { display: grid; gap: 5px; margin-top: 10px; }
.v2-repair-event-list article { display: grid; gap: 2px; padding: 7px 9px; background: var(--repair-soft); border-radius: 8px; }
+69 -15
View File
@@ -50,6 +50,7 @@
: "shadow";
let polling = false;
let toastTimer = 0;
let confirmationResolve = null;
function isAdmin() {
return window.CognitiveAgentConsoleV2?.getRole() === "admin"
@@ -181,7 +182,11 @@
</section>
<dialog class="v2-repair-dialog" id="repairSoftwareDialog"><header><div><strong>软件自修复验证记录</strong><small>成功用例自动保存诊断快照</small></div><button type="button" data-repair-close="repairSoftwareDialog">×</button></header><div id="repairSoftwareRecordsBody"></div></dialog>
<dialog class="v2-repair-dialog" id="repairHardwareDialog"><header><div><strong>硬件自诊断记录</strong><small>支路状态变化与最终报告</small></div><button type="button" data-repair-close="repairHardwareDialog">×</button></header><div id="repairHardwareRecordsBody"></div></dialog>`;
<dialog class="v2-repair-dialog" id="repairHardwareDialog"><header><div><strong>硬件自诊断记录</strong><small>支路状态变化与最终报告</small></div><button type="button" data-repair-close="repairHardwareDialog">×</button></header><div id="repairHardwareRecordsBody"></div></dialog>
<dialog class="v2-repair-dialog v2-repair-confirm-dialog" id="repairConfirmDialog" aria-labelledby="repairConfirmTitle">
<header><div><strong id="repairConfirmTitle">确认台架安全条件</strong><small>确认后才会向实物链路提交请求</small></div></header>
<div><p id="repairConfirmMessage"></p><div class="v2-repair-confirm-actions"><button type="button" id="repairConfirmCancel">取消</button><button class="primary" type="button" id="repairConfirmAccept">确认执行</button></div></div>
</dialog>`;
moduleSwitcher.insertAdjacentElement("afterend", root);
const select = $("#repairExecutionMode");
@@ -402,9 +407,12 @@
const session = latest.hardware_session || {};
const bridge = latest.bridge || {};
const manager = latest.bridge_manager || {};
const adapterOnline = hardware.link?.adapter === "OK"
|| Boolean(bridge.bus?.opened)
|| (bridge.online && bridge.bus_traffic);
const layers = [
["1", "ZCANPRO桥接", bridge.online ? "正常" : "离线", bridge.online ? "OK" : "UNKNOWN"],
["2", "CAN设备 / 通道", bridge.bus?.opened ? "设备与通道已打开" : "未连接", bridge.bus?.opened ? "OK" : "UNKNOWN"],
["2", "CAN设备 / 通道", adapterOnline ? "设备与通道已打开" : "未连接", adapterOnline ? "OK" : "UNKNOWN"],
["3", "总线实时流量", bridge.bus_traffic ? "正常" : "未检测", bridge.bus_traffic ? "OK" : "WARN"],
["4", "座椅ECU协议", bridge.protocol_online ? "0x301~0x305在线" : "未检测", bridge.protocol_online ? "OK" : "UNKNOWN"],
];
@@ -431,13 +439,25 @@
const bridge = latest.bridge || {};
const manager = latest.bridge_manager || {};
const healing = latest.healing || {};
const hardwarePhase = latest.hardware_session?.progress?.phase;
const integration = latest.integration || {};
$("#repairCanStatus").textContent = bridge.online ? "在线" : "离线";
$("#repairCanDot").dataset.state = bridge.online ? "online" : manager.state === "STARTING" ? "working" : "offline";
$("#repairProtocolStatus").textContent = bridge.protocol_online ? "协议在线" : "未检测";
$("#repairProtocolDot").dataset.state = bridge.protocol_online ? "online" : "offline";
$("#repairAiStatus").textContent = healing.phase_text || "等待验证";
$("#repairAiDot").dataset.state = ["PASSED"].includes(healing.phase) ? "online" : ["IDLE"].includes(healing.phase) ? "offline" : "working";
const hardwareState = {
BASELINE: ["硬件检测中", "working"],
RUNNING: ["硬件检测中", "working"],
PASSED: ["21针通过", "online"],
FAILED: ["硬件异常", "offline"],
}[hardwarePhase];
const aiState = activeView === "hardware" && hardwareState
? hardwareState
: [healing.phase_text || "等待验证", healing.phase === "PASSED"
? "online"
: healing.phase === "IDLE" ? "offline" : "working"];
$("#repairAiStatus").textContent = aiState[0];
$("#repairAiDot").dataset.state = aiState[1];
$("#repairConnectionText").textContent = bridge.online ? "CAN 桥接在线" : manager.message || "CAN 桥接离线";
$(".v2-repair-footer").classList.toggle("offline", !bridge.online);
const benchOption = $('#repairExecutionMode option[value="bench"]');
@@ -504,6 +524,25 @@
}
}
function finishConfirmation(accepted) {
const dialog = $("#repairConfirmDialog");
const resolve = confirmationResolve;
confirmationResolve = null;
if (dialog?.open) dialog.close(accepted ? "confirmed" : "cancelled");
if (resolve) resolve(accepted);
}
function confirmAction(message, confirmLabel = "确认执行") {
const dialog = $("#repairConfirmDialog");
if (!dialog || confirmationResolve) return Promise.resolve(false);
$("#repairConfirmMessage").textContent = message;
$("#repairConfirmAccept").textContent = confirmLabel;
dialog.showModal();
return new Promise((resolve) => {
confirmationResolve = resolve;
});
}
function bindEvents() {
$$('[data-repair-view]').forEach((button) => button.addEventListener("click", () => showView(button.dataset.repairView)));
$("#repairExecutionMode").addEventListener("change", (event) => {
@@ -511,33 +550,48 @@
sessionStorage.setItem(MODE_KEY, executionMode);
if (latest) renderAll();
});
$("#repairStart").addEventListener("click", () => {
if (executionMode === "bench" && !window.confirm("即将向实物 ECU 写入受控故障并驱动座椅恢复。请确认运动区域无人、无遮挡且急停可用。")) return;
$("#repairStart").addEventListener("click", async () => {
if (executionMode === "bench" && !(await confirmAction("即将向实物 ECU 写入受控故障并驱动座椅恢复。请确认运动区域无人、无遮挡且急停可用。"))) return;
runAction(() => apiPost("/healing/start", { case_id: selectedCaseId, execution_mode: executionMode }), executionMode === "bench" ? "实物自修复用例已启动" : "影子故障已注入,开始自修复");
});
$("#repairRunAll").addEventListener("click", () => runAction(() => apiPost("/healing/run-all", { execution_mode: executionMode }), "六项自动验证已启动"));
$("#repairReset").addEventListener("click", () => {
if (executionMode === "bench" && !window.confirm("四轴将依次回到机械零位。请确认运动区域无人、无遮挡且急停可用。")) return;
$("#repairRunAll").addEventListener("click", async () => {
if (executionMode === "bench" && !(await confirmAction(
"将依次执行6个实物故障用例;每项开始前会自动停止输出并完成四轴归零。请确认运动区域无人、无遮挡且急停可用。",
"确认实物批量验证",
))) return;
runAction(() => apiPost("/healing/run-all", { execution_mode: executionMode }), "六项自动验证已启动");
});
$("#repairReset").addEventListener("click", async () => {
if (executionMode === "bench" && !(await confirmAction("四轴将依次回到机械零位。请确认运动区域无人、无遮挡且急停可用。", "确认归零"))) return;
runAction(() => apiPost("/healing/reset", { execution_mode: executionMode }), executionMode === "bench" ? "四轴归零请求已提交" : "自修复验证已重置");
});
$("#repairFirmwareFlash").addEventListener("click", () => {
if (!window.confirm("即将构建并烧录台架测试固件。请确认座椅运动区域无人、急停可用且 CMSIS-DAP 已连接。")) return;
$("#repairFirmwareFlash").addEventListener("click", async () => {
if (!(await confirmAction("即将构建并烧录台架测试固件。请确认座椅运动区域无人、急停可用且 CMSIS-DAP 已连接。", "确认构建/烧录"))) return;
runAction(() => apiPost("/firmware/build-flash", { confirmed_bench_clear: true }), "固件构建/烧录流程已完成");
});
$("#repairHardwareToggle").addEventListener("click", () => {
$("#repairHardwareToggle").addEventListener("click", async () => {
if (!latest) return;
if (latest.hardware_session?.active) {
runAction(() => apiPost("/hardware/stop"), "硬件诊断已停止并保存报告");
return;
}
if (!window.confirm("硬件诊断可能自动烧录测试固件,并短时驱动四路电机、风扇与低档加热。请确认台架区域安全且急停可用。")) return;
if (!(await confirmAction("硬件诊断可能自动烧录测试固件,并短时驱动四路电机、风扇与低档加热。请确认台架区域安全且急停可用。", "确认启动诊断"))) return;
runAction(() => apiPost("/hardware/start", { confirmed_bench_clear: true }), "21 针主动硬件诊断已启动");
});
$("#repairHardwareSelfTest").addEventListener("click", () => runAction(() => apiPost("/hardware/self-test"), "72 针映射和诊断规则自检完成"));
$("#repairEmergencyStop").addEventListener("click", () => {
if (!window.confirm("确认提交 STOP ALL 和热管理关闭请求?")) return;
$("#repairEmergencyStop").addEventListener("click", async () => {
if (!(await confirmAction("确认提交 STOP ALL 和热管理关闭请求?", "确认紧急停止"))) return;
runAction(() => apiPost("/emergency-stop"), "紧急停止请求已提交");
});
$("#repairConfirmCancel").addEventListener("click", () => finishConfirmation(false));
$("#repairConfirmAccept").addEventListener("click", () => finishConfirmation(true));
$("#repairConfirmDialog").addEventListener("cancel", (event) => {
event.preventDefault();
finishConfirmation(false);
});
$("#repairConfirmDialog").addEventListener("close", () => {
if (confirmationResolve) finishConfirmation(false);
});
$("#repairSoftwareRecords").addEventListener("click", () => $("#repairSoftwareDialog").showModal());
$("#repairHardwareRecords").addEventListener("click", () => $("#repairHardwareDialog").showModal());
$$('[data-repair-close]').forEach((button) => button.addEventListener("click", () => $(`#${button.dataset.repairClose}`).close()));
+5 -5
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v39";
const CACHE_VERSION = "cognitive-agent-pad-v40";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
@@ -6,7 +6,7 @@ const APP_SHELL = [
"/index.html",
"/styles.css?v=android-pad-4",
"/network.js?v=android-pad-1",
"/app.js?v=seat-hil-43",
"/app.js?v=seat-hil-44",
"/gesture-control.js?v=android-pad-6",
"/pwa.js?v=android-pad-1",
"/console-v2.css?v=desktop-refactor-68",
@@ -15,9 +15,9 @@ const APP_SHELL = [
"/self-extension.js?v=extension-6",
"/self-calibration.css?v=calibration-4",
"/self-calibration.js?v=calibration-2",
"/self-repair.css?v=repair-15",
"/self-repair.js?v=repair-15",
"/self-repair-seat3d.js?v=repair-15",
"/self-repair.css?v=repair-17",
"/self-repair.js?v=repair-17",
"/self-repair-seat3d.js?v=repair-17",
"/assets/self-calibration-model.bundle.js",
"/assets/models/n7_seat.glb",
"/manifest.webmanifest",
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,825 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-07T18:17:38.577477Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260807181738",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-ROCKING-HORSE-START",
"group": "mode",
"utterance": "开启木马模式",
"started_at": "2026-08-07T18:18:01.681906Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181801-e5096b",
"provider_used": "local_rules",
"matched_modes": [
"rocking_horse"
],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 2,
"value_x100": 153,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 12 0D 35 00 99 00 5A",
"timestamp": 106973.993523,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 3,
"value_x100": 216,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 12 0D 35 00 D8 00 5A",
"timestamp": 106974.0196,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 2,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 0,
"value_x100": 240,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 12 0D 35 00 F0 00 5A",
"timestamp": 106974.045704,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 6,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 1,
"value_x100": 588,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 12 0D 35 00 4C 02 5A",
"timestamp": 106974.072431,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106974.092381,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106974.092381,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:02.485490Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:02.550409Z",
"duration_ms": 869
},
{
"id": "MODE-ROCKING-HORSE-STOP",
"group": "mode",
"utterance": "关闭木马模式",
"started_at": "2026-08-07T18:18:02.552655Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181802-7609ee",
"provider_used": "local_rules",
"matched_modes": [
"rocking_horse"
],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 3,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 338,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 35 00 52 01 00",
"timestamp": 106974.915491,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 5,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 541,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 35 00 1D 02 00",
"timestamp": 106974.942088,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 5,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 541,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 08 35 00 1D 02 00",
"timestamp": 106974.968094,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 1123,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 08 35 00 63 04 00",
"timestamp": 106974.993545,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.005142,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.005142,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:03.397863Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:03.474083Z",
"duration_ms": 921
},
{
"id": "MODE-DYNAMIC-CRADLE-START",
"group": "mode",
"utterance": "开启动态摇篮模式",
"started_at": "2026-08-07T18:18:03.517741Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181803-e97616",
"provider_used": "local_rules",
"matched_modes": [
"dynamic_cradle"
],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 8,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 3,
"value_x100": 804,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 12 0D 38 00 24 03 5A",
"timestamp": 106975.856713,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 8,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 0,
"value_x100": 802,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 12 0D 38 00 22 03 5A",
"timestamp": 106975.881732,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 17,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 1,
"value_x100": 1684,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 12 0D 38 00 94 06 5A",
"timestamp": 106975.907916,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"backrest_recline": {
"position_pct": 6,
"position_valid": true,
"motion_state": "moving",
"fault": false,
"task_state": 2,
"motor_result": 1,
"motor_selector": 2,
"value_x100": 556,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 12 0D 38 00 2C 02 5A",
"timestamp": 106975.934169,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.955173,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106975.955173,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:04.348455Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:04.413787Z",
"duration_ms": 896
},
{
"id": "MODE-DYNAMIC-CRADLE-STOP",
"group": "mode",
"utterance": "关闭动态摇篮模式",
"started_at": "2026-08-07T18:18:04.416034Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181804-bcf153",
"provider_used": "local_rules",
"matched_modes": [
"dynamic_cradle"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 1122,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 08 38 00 62 04 00",
"timestamp": 106976.740494,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 22,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 2246,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 08 38 00 C6 08 00",
"timestamp": 106976.76544,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"backrest_recline": {
"position_pct": 7,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 742,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 38 00 E6 02 00",
"timestamp": 106976.792049,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 11,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 1144,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 38 00 78 04 00",
"timestamp": 106976.817508,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106976.829112,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106976.829112,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:05.221617Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:05.302650Z",
"duration_ms": 887
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-07T18:17:38.579391Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181738-c1fa94",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 31 00 00 00 00",
"timestamp": 106973.120321,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 32 00 00 00 00",
"timestamp": 106973.146175,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 2F 00 00 00 00",
"timestamp": 106973.171397,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 30 00 00 00 00",
"timestamp": 106973.198366,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106973.204067,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106973.204067,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:01.596265Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:01.678120Z",
"duration_ms": 23099
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-07T18:18:05.303817Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260807181805-8f6d46",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 18 3C 00 00 00 00",
"timestamp": 106996.774961,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 18 3D 00 00 00 00",
"timestamp": 106996.800164,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 18 3A 00 00 00 00",
"timestamp": 106996.826407,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 18 3B 00 00 00 00",
"timestamp": 106996.852753,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
}
},
"ventilation": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106996.861946,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"heating": {
"state": "off",
"level": 0,
"feedback_value": 1,
"feedback_frame": {
"frame_id": "0x4FA",
"payload_size": 8,
"data_hex": "09 00 00 00 00 00 00 00",
"timestamp": 106996.861946,
"channel": 0,
"is_fd": false,
"bitrate_switch": false
}
},
"captured_at": "2026-08-07T18:18:25.255987Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-07T18:18:25.336490Z",
"duration_ms": 20033
},
"finished_at": "2026-08-07T18:18:25.336534Z",
"summary": {
"total": 4,
"passed": 4,
"failed": 0,
"safety_blocked": 0,
"executed": 4,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,358 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-09T10:04:15.082291Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260809100415",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-25-HORIZONTAL",
"group": "motion",
"utterance": "把副驾座椅水平位置调到25%",
"started_at": "2026-08-09T10:04:16.311200Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100416-4da1f3",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:21.364491Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:21.365063Z",
"duration_ms": 5054
},
{
"id": "MOTION-M1-20-PAN",
"group": "motion",
"utterance": "把副驾坐盆调到20%",
"started_at": "2026-08-09T10:04:21.365115Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100421-b0b99e",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 20,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:24.207471Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:24.208128Z",
"duration_ms": 2843
},
{
"id": "COMBINED-ALL",
"group": "combined",
"utterance": "打开副驾加热1档、通风1档,座椅水平到30%,坐盆到40%,靠背到37%,腿托到42%",
"started_at": "2026-08-09T10:04:24.208183Z",
"status": "failed",
"errors": [
"M3运动反馈未达到目标"
],
"http_status": 201,
"request_id": "req-20260809100424-27194b",
"provider_used": "local_rules",
"failure_telemetry": {
"transport": "zcanpro_shared_bridge",
"hardware_feedback": false,
"simulated": false,
"bridge_result": {
"details": {
"command_id": "t2g_move_passenger_legrest_height_42pct",
"stop_all_attempted": true,
"summary": "M3运动反馈未达到目标",
"target": 4200,
"terminal": {
"active": false,
"direction": 0,
"driver_fault": false,
"duty": 0,
"far": false,
"motor": 3,
"near": false,
"position": 2349,
"position_valid": true,
"result": 6,
"sequence": 50339,
"stall": false,
"task": 7
},
"tolerance": 100
},
"log_dir": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_180424_546250",
"report_path": "D:\\huang.xuewei\\Git\\Cognitive_Agent\\artifacts\\self-repair\\hardware\\20260809_180424_546250\\report.json",
"request_id": "SEAT_ORCH_1786269864212_546250",
"result": "FAIL",
"summary": "M3运动反馈未达到目标"
},
"error_type": null,
"stop_all_attempted": true
},
"finished_at": "2026-08-09T10:04:47.676002Z",
"duration_ms": 23468
},
{
"id": "MODE-EASY-OPEN",
"group": "mode",
"utterance": "打开轻松进出模式",
"started_at": "2026-08-09T10:04:47.676048Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100447-c855cc",
"provider_used": "local_rules",
"matched_modes": [
"easy_entry_exit"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "on",
"level": 1
},
"heating": {
"state": "on",
"level": 1
},
"captured_at": "2026-08-09T10:05:18.547335Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:05:18.548156Z",
"duration_ms": 30872
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:04:15.082316Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100415-146708",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:04:16.310470Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:04:16.311124Z",
"duration_ms": 1229
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:05:18.548210Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100518-e21b3f",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:05:36.941483Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:05:36.942155Z",
"duration_ms": 18394
},
"finished_at": "2026-08-09T10:05:36.942191Z",
"summary": {
"total": 4,
"passed": 3,
"failed": 1,
"safety_blocked": 0,
"executed": 4,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
@@ -0,0 +1,363 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-09T10:09:27.978007Z",
"base_url": "http://127.0.0.1:8003",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260809100927",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-25-HORIZONTAL",
"group": "motion",
"utterance": "把副驾座椅水平位置调到25%",
"started_at": "2026-08-09T10:09:29.207379Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100929-f3faee",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:09:34.257766Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:09:34.258318Z",
"duration_ms": 5051
},
{
"id": "MOTION-M1-20-PAN",
"group": "motion",
"utterance": "把副驾坐盆调到20%",
"started_at": "2026-08-09T10:09:34.258367Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100934-e7b1ff",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 25,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 20,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:09:37.186209Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:09:37.186800Z",
"duration_ms": 2928
},
{
"id": "COMBINED-ALL",
"group": "combined",
"utterance": "打开副驾加热1档、通风1档,座椅水平到30%,坐盆到40%,靠背到37%,腿托到42%",
"started_at": "2026-08-09T10:09:37.186855Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100937-a7f166",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 40,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 37,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 42,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "on",
"level": 1
},
"heating": {
"state": "on",
"level": 1
},
"captured_at": "2026-08-09T10:09:58.061243Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:09:58.061831Z",
"duration_ms": 20875
},
{
"id": "MODE-EASY-OPEN",
"group": "mode",
"utterance": "打开轻松进出模式",
"started_at": "2026-08-09T10:09:58.061892Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100958-e2d7f8",
"provider_used": "local_rules",
"matched_modes": [
"easy_entry_exit"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "on",
"level": 1
},
"heating": {
"state": "on",
"level": 1
},
"captured_at": "2026-08-09T10:10:30.421120Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:10:30.421790Z",
"duration_ms": 32360
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:09:27.978032Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809100927-fbc991",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:09:29.206596Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:09:29.207187Z",
"duration_ms": 1229
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-09T10:10:30.421849Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260809101030-196c5e",
"provider_used": "local_rules",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped"
}
},
"ventilation": {
"state": "off",
"level": 0
},
"heating": {
"state": "off",
"level": 0
},
"captured_at": "2026-08-09T10:10:48.779417Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-09T10:10:48.780316Z",
"duration_ms": 18358
},
"finished_at": "2026-08-09T10:10:48.780363Z",
"summary": {
"total": 4,
"passed": 4,
"failed": 0,
"safety_blocked": 0,
"executed": 4,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
+9 -8
View File
@@ -1,8 +1,9 @@
# 自修复本机配置
自修复业务代码与诊断规则随仓库交付,本机固件工程、IAR 和 ZCANPRO 不写死在
代码中。未设置下列环境变量时,影子自修复与硬件规则自检可正常使用,所有依赖
实物或烧录工具的操作保持不可用。
自修复业务代码、诊断规则和固件工程随仓库交付。服务会自动识别仓库内固件、
标准安装位置的 IAR Embedded Workbench 9.2/9.4 与 ZCANPRO;非标准安装位置仍可
通过下列环境变量覆盖。未识别到工具时,影子自修复与硬件规则自检可正常使用,
所有依赖实物或烧录工具的操作保持不可用。
| 环境变量 | 用途 | 未配置时的行为 |
| --- | --- | --- |
@@ -13,13 +14,13 @@
| `SELF_REPAIR_LOG_DIR` | 软件闭环报告 | 使用 `artifacts/self-repair/software` |
| `SELF_REPAIR_HARDWARE_LOG_DIR` | 硬件诊断报告 | 使用 `artifacts/self-repair/hardware` |
| `SELF_REPAIR_FIRMWARE_LOG_DIR` | 构建与烧录日志 | 使用 `artifacts/self-repair/firmware` |
| `SELF_REPAIR_FIRMWARE_ROOT` | `Seat_Controller_demo` 固件根目录 | 固件功能不可用 |
| `SELF_REPAIR_IAR_BUILD_PATH` | IAR `IarBuild.exe` | 构建/烧录不可用 |
| `SELF_REPAIR_CSPY_BAT_PATH` | IAR `cspybat.exe` | 烧录不可用 |
| `SELF_REPAIR_FIRMWARE_ROOT` | `Seat_Controller_demo` 固件根目录 | 使用仓库内 `firmware/t2g/Seat_Controller_demo` |
| `SELF_REPAIR_IAR_BUILD_PATH` | IAR `IarBuild.exe` | 尝试识别标准 9.2/9.4 安装,失败则构建/烧录不可用 |
| `SELF_REPAIR_CSPY_BAT_PATH` | IAR `cspybat.exe` | 尝试识别标准 9.2/9.4 安装,失败则烧录不可用 |
| `SELF_REPAIR_CSPY_GENERAL_PATH` | C-SPY general `.xcl` | 默认从固件工程推导 |
| `SELF_REPAIR_CSPY_DRIVER_PATH` | C-SPY driver `.xcl` | 默认从固件工程推导 |
| `SELF_REPAIR_ZCANPRO_EXE` | `ZCANPRO.exe` | 实物 CAN bridge 不可自动启动 |
| `SELF_REPAIR_ZCANPRO_SCRIPT_CONFIG` | ZCANPRO 扩展脚本配置文件 | bridge 自动启动不可用 |
| `SELF_REPAIR_ZCANPRO_EXE` | `ZCANPRO.exe` | 尝试识别标准安装,失败则实物 CAN bridge 不可自动启动 |
| `SELF_REPAIR_ZCANPRO_SCRIPT_CONFIG` | ZCANPRO 扩展脚本配置文件 | 使用标准安装目录下 `config/extend_script.ini` |
| `SELF_REPAIR_AUTO_START_BRIDGE` | 服务启动后允许一次 bridge 拉起 | 默认 `false` |
PowerShell 示例(路径按本机安装位置填写):
@@ -15,6 +15,8 @@
static volatile safety_fault_t latchedFault = SAFETY_FAULT_NONE;
static bool watchdogReset = false;
static bool watchdogInitialized = false;
static bool hilWatchdogResetArmed = false;
static bool hilWatchdogRecovered = false;
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
static bool debuggerWasAttached = false;
#endif
@@ -40,6 +42,30 @@ static bool SafetyManager_WriteDebugMarker(uint32_t marker)
return (SafetyManager_ReadDebugMarker() == marker);
}
static bool SafetyManager_WaitForWatchdogState(bool enabled)
{
uint32_t remaining = 200ul;
uint32_t expected = enabled ? 1ul : 0ul;
/* WDT ENABLE is an asynchronous request bit; ENABLED is the synchronized
* LF-clock state. Configuration writes made before disable completes are
* ignored by the hardware. */
while((WDT->unCTL.stcField.u1ENABLED != expected) && (remaining > 0ul))
{
Cy_SysTick_DelayInUs(1u);
remaining--;
}
return (WDT->unCTL.stcField.u1ENABLED == expected);
}
static void SafetyManager_ClearHilWatchdogCredential(void)
{
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = 0u;
__DSB();
}
void SafetyManager_Init(uint32_t resetReason)
{
const uint32_t watchdogMask = CY_SYSRESET_WDT |
@@ -47,18 +73,58 @@ void SafetyManager_Init(uint32_t resetReason)
CY_SYSRESET_MCWDT1 |
CY_SYSRESET_MCWDT2 |
CY_SYSRESET_MCWDT3;
bool debuggerAttached = SafetyManager_DebuggerAttached();
uint32_t marker = SafetyManager_ReadDebugMarker();
bool watchdogStopped;
bool debuggerAttached;
uint32_t marker;
uint32_t hilMagic;
uint32_t hilToken;
uint32_t hilCheck;
bool authorizedHilReset;
bool markerOk = true;
/* A watchdog instance is retained across its own reset on this device.
* Stop it before reading credentials or initializing peripherals; the
* 100 ms bench timeout otherwise permits a reset loop. */
Cy_WDT_Disable();
watchdogStopped = SafetyManager_WaitForWatchdogState(false);
debuggerAttached = SafetyManager_DebuggerAttached();
marker = SafetyManager_ReadDebugMarker();
hilMagic = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register;
hilToken = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register;
hilCheck = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register;
authorizedHilReset =
(hilMagic == SEAT_MOVE_HIL_WDT_MARKER) &&
(hilCheck == (hilMagic ^ hilToken ^ 0xFFFFFFFFu)) &&
(((resetReason & watchdogMask) != 0ul) ||
((resetReason & CY_SYSRESET_SOFT) != 0ul));
latchedFault = SAFETY_FAULT_NONE;
watchdogInitialized = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul);
hilWatchdogResetArmed = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul) ||
authorizedHilReset;
hilWatchdogRecovered = authorizedHilReset;
if((resetReason & CY_SYSRESET_PORVDDD) != 0ul)
{
markerOk = SafetyManager_WriteDebugMarker(0u);
marker = SafetyManager_ReadDebugMarker();
SafetyManager_ClearHilWatchdogCredential();
hilWatchdogRecovered = false;
}
else if(hilWatchdogRecovered)
{
/* Consume before CAN comes online: transport retries cannot turn one
* authenticated reset into a second physical reset. */
SafetyManager_ClearHilWatchdogCredential();
}
else if(hilMagic == SEAT_MOVE_HIL_WDT_MARKER)
{
/* XRES/programming without a valid reset reason is not completion. */
SafetyManager_ClearHilWatchdogCredential();
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
debuggerWasAttached = debuggerAttached;
@@ -69,7 +135,7 @@ void SafetyManager_Init(uint32_t resetReason)
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
else if(watchdogReset)
else if(watchdogReset && !hilWatchdogRecovered)
{
latchedFault = SAFETY_FAULT_BOOT_RECOVERY;
}
@@ -79,21 +145,54 @@ void SafetyManager_Init(uint32_t resetReason)
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
/* The system-WDT configuration register is retained across some reset
* paths and AUTO_SERVICE is not reset by Cy_WDT_Init(). If that bit is
* left enabled, stopping the two software service paths can never prove a
* real watchdog reset because hardware services the counter at WARN_LIMIT.
* Configure only while stopped, and make software the sole WDT owner. */
if(!watchdogStopped)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Init();
Cy_WDT_Unlock();
Cy_WDT_SetUpperLimit(SEAT_MOVE_WDT_TIMEOUT_TICKS);
Cy_WDT_SetUpperAction(CY_WDT_LOW_UPP_ACTION_RESET);
Cy_WDT_SetWarnAction(CY_WDT_WARN_ACTION_NONE);
Cy_WDT_SetAutoService(CY_WDT_DISABLE);
Cy_WDT_SetDebugRun(CY_WDT_ENABLE);
Cy_WDT_Lock();
if((WDT->unUPPER_LIMIT.stcField.u32UPPER_LIMIT !=
SEAT_MOVE_WDT_TIMEOUT_TICKS) ||
(WDT->unCONFIG.stcField.u1UPPER_ACTION !=
(uint32_t)CY_WDT_LOW_UPP_ACTION_RESET) ||
(WDT->unCONFIG.stcField.u1AUTO_SERVICE !=
(uint32_t)CY_WDT_DISABLE))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Enable();
if(!SafetyManager_WaitForWatchdogState(true))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_ClearWatchdog();
Cy_SysTick_DelayInUs(1000u);
if(Cy_WDT_GetCount() == 0ul)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
watchdogInitialized = true;
SafetyManager_KickWatchdog();
}
void SafetyManager_KickWatchdog(void)
{
if(false != watchdogInitialized)
if((false != watchdogInitialized) && !hilWatchdogResetArmed)
{
Cy_WDT_ClearWatchdog();
}
@@ -171,3 +270,63 @@ bool SafetyManager_WasWatchdogReset(void)
{
return watchdogReset;
}
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token)
{
uint32_t check;
if(!watchdogInitialized || hilWatchdogResetArmed)
{
return false;
}
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register =
SEAT_MOVE_HIL_WDT_MARKER;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = token;
check = SEAT_MOVE_HIL_WDT_MARKER ^ (uint32_t)token ^ 0xFFFFFFFFu;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = check;
__DSB();
if((BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register !=
SEAT_MOVE_HIL_WDT_MARKER) ||
(BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register != check))
{
SafetyManager_ClearHilWatchdogCredential();
return false;
}
/* Long-running motor calibration/coast loops normally service WDT
* directly so they remain safe even while the application loop is busy.
* The armed HIL case must suspend both service paths or a real watchdog
* reset can never occur. Startup re-enables this after the reset. */
Motor_SetWatchdogServiceEnabled(false);
Cy_WDT_ClearWatchdog();
hilWatchdogResetArmed = true;
return true;
}
void SafetyManager_CommitHilWatchdogReset(void)
{
if(!hilWatchdogResetArmed)
{
return;
}
/* This bench's connected CMSIS-DAP can suppress the WDT reset output even
* after C-SPY detaches. First prove physical WDT expiry, then use the
* reference project's authenticated processor-reset fallback. */
while(Cy_WDT_GetCount() < SEAT_MOVE_WDT_TIMEOUT_TICKS)
{
__NOP();
}
__DSB();
NVIC_SystemReset();
for(;;)
{
__NOP();
}
}
bool SafetyManager_WasHilWatchdogRecovered(void)
{
return hilWatchdogRecovered;
}
@@ -23,5 +23,8 @@ bool SafetyManager_IsWatchdogInitialized(void);
bool SafetyManager_IsInhibited(void);
safety_fault_t SafetyManager_GetFault(void);
bool SafetyManager_WasWatchdogReset(void);
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token);
void SafetyManager_CommitHilWatchdogReset(void);
bool SafetyManager_WasHilWatchdogRecovered(void);
#endif /* SAFETY_MANAGER_H */
@@ -39,6 +39,7 @@ static uint32_t s_fanStartupRemainingMs = 0u;
static uint32_t s_feedbackElapsedMs = 0u;
static uint32_t s_fanCurrentSampleElapsedMs = 0u;
static bool s_fanCurrentSampleValid = false;
static uint16_t s_heaterCurrentPeakRaw = 0u;
static void SeatControllerThermal_HandleControl29e(
const bsp_seat_move_can_frame_t *frame);
@@ -81,6 +82,7 @@ bool SeatControllerThermal_Init(void)
g_seatControllerFanCurrentSenseRaw = fanCurrentRaw;
s_fanCurrentSampleValid = true;
s_fanCurrentSampleElapsedMs = 0u;
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
SeatControllerThermal_StopFan(1u);
SeatControllerThermal_StopHeater(1u);
@@ -121,6 +123,16 @@ void SeatControllerThermal_HandleFrame(const bsp_seat_move_can_frame_t *frame)
void SeatControllerThermal_Service1ms(void)
{
SeatBackrestHeater_Task1ms();
/*
* Heater PWM can be shorter than the 100 ms CAN feedback interval. Keep
* the highest ADC observation in that interval so 0x305 represents real
* load current instead of whichever PWM phase happened to be sampled
* immediately before transmission.
*/
if(g_seatBackrestHeaterSenseRaw > s_heaterCurrentPeakRaw)
{
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
}
SeatControllerThermal_ServiceFanCurrent();
if((g_seatBackrestHeaterState ==
SEAT_BACKREST_HEATER_STATE_FAULT_SAFE_OFF) ||
@@ -194,6 +206,24 @@ void SeatControllerThermal_ForceSafeOff(void)
s_feedbackPending = true;
}
bool SeatControllerThermal_HilInjectFanReentry(void)
{
/* Recreate the field defect after the user's OFF has already been
* applied: a stale pending-start callback requests fan level 2 again. */
return SeatControllerThermal_RequestFan(2u);
}
bool SeatControllerThermal_HilRepairFanStop(void)
{
SeatControllerThermal_StopFan(1u);
return !s_fanRunning;
}
bool SeatControllerThermal_HilFanStopped(void)
{
return !s_fanRunning;
}
static void SeatControllerThermal_HandleControl29e(
const bsp_seat_move_can_frame_t *frame)
{
@@ -486,7 +516,7 @@ static void SeatControllerThermal_ServiceFeedback(void)
currentData0 = (uint32_t)currentFlags |
((uint32_t)outputState << 8u) |
((uint32_t)g_seatControllerFanCurrentSenseRaw << 16u);
currentData1 = (uint32_t)g_seatBackrestHeaterSenseRaw |
currentData1 = (uint32_t)s_heaterCurrentPeakRaw |
((uint32_t)fanErrors << 16u) |
((uint32_t)heaterErrors << 24u);
@@ -497,6 +527,7 @@ static void SeatControllerThermal_ServiceFeedback(void)
currentData1))
{
g_seatControllerThermalFeedbackTxCount++;
s_heaterCurrentPeakRaw = g_seatBackrestHeaterSenseRaw;
s_feedbackPending = false;
s_feedbackElapsedMs = 0u;
}
@@ -10,6 +10,11 @@ bool SeatControllerThermal_Init(void);
void SeatControllerThermal_HandleFrame(const bsp_seat_move_can_frame_t *frame);
void SeatControllerThermal_Service1ms(void);
void SeatControllerThermal_ForceSafeOff(void);
/* Bench-only fault-injection helpers reached exclusively through the armed
* 0x2F0 self-healing service. */
bool SeatControllerThermal_HilInjectFanReentry(void);
bool SeatControllerThermal_HilRepairFanStop(void);
bool SeatControllerThermal_HilFanStopped(void);
extern volatile uint8_t g_seatControllerFanFeedbackRaw;
extern volatile uint8_t g_seatControllerHeaterFeedbackRaw;
@@ -631,6 +631,19 @@ void SeatMotorManager_Service(void)
}
if(info.lastEvent == MOTOR_EVENT_TIMEOUT)
{
/* Some seat actuators open their internal limit contact instead
* of producing a high-current endpoint event. The low-level
* no-motion timer then reports TIMEOUT even when discrete Hall
* feedback stopped only a few pulses from the requested pose.
* Accept only the same tightly bounded Hall-domain arrival proof
* used for endpoint/stall events; every other timeout remains a
* failure. */
if(SeatMotor_TaskIsPosition(task) &&
SeatMotor_TargetWithinHallTolerance(task, &info))
{
SeatMotor_SetComplete(mode);
continue;
}
SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_TIMEOUT);
continue;
}
@@ -18,6 +18,11 @@
#define SEAT_MOVE_CLEAR_FAULT_KEY (0xC33Cu)
#define SEAT_MOVE_TIMED_LEVEL_MIN_X10 (300u)
#define SEAT_MOVE_TIMED_LEVEL_MAX_X10 (1000u)
#define SEAT_MOVE_HIL_RESPONSE_OK (0u)
#define SEAT_MOVE_HIL_RESPONSE_PRECONDITION (1u)
#define SEAT_MOVE_HIL_RESPONSE_EXECUTION (2u)
#define SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED (4u)
#define SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED (5u)
typedef enum
{
@@ -181,6 +186,10 @@ static uint32_t s_nextSystemStatusMs = 0u;
static uint32_t s_nextMotorPollMs = 0u;
static uint32_t s_nextDiagnosticPollMs = 0u;
static uint32_t s_nextDynamicStatusMs = 0u;
static uint8_t s_hilArmedCase = 0u;
static uint16_t s_hilArmToken = 0u;
static uint32_t s_hilArmExpiresMs = 0u;
static bool s_hilFaultActive[7];
static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline);
static bool SeatMove_SequenceNewer(uint16_t candidate, uint16_t reference);
@@ -210,6 +219,13 @@ static uint8_t SeatMove_PositionValidMask(void);
static uint8_t SeatMove_ActiveCount(void);
static bool SeatMove_CalibrationBusy(void);
static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame);
static void SeatMove_HandleHilSelfHeal(
const bsp_seat_move_can_frame_t *frame);
static bool SeatMove_SendHilSelfHealResponse(uint8_t caseIndex,
uint8_t operation,
uint16_t token,
uint8_t result,
uint8_t evidence);
static void SeatMove_StopMotor(uint8_t motor, uint16_t sequence);
static void SeatMove_QueueCommand(const seat_move_command_t *command);
static bool SeatMove_DispatchCommand(const seat_move_command_t *command);
@@ -236,6 +252,10 @@ bool SeatMoveApplication_Init(void)
(void)memset(s_lastStatus, 0, sizeof(s_lastStatus));
(void)memset(s_lastStatusValid, 0, sizeof(s_lastStatusValid));
(void)memset(s_diagnosticAlive, 0, sizeof(s_diagnosticAlive));
(void)memset(s_hilFaultActive, 0, sizeof(s_hilFaultActive));
s_hilArmedCase = 0u;
s_hilArmToken = 0u;
s_hilArmExpiresMs = 0u;
s_storeSavePending = false;
s_storeSaveNotBeforeMs = 0u;
SeatMotorManager_Init();
@@ -305,6 +325,10 @@ void SeatMoveApplication_Service(void)
{
SeatMove_HandleCommand(&frame);
}
else if(frame.identifier == SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID)
{
SeatMove_HandleHilSelfHeal(&frame);
}
else
{
SeatControllerThermal_HandleFrame(&frame);
@@ -328,6 +352,10 @@ void Motor_CalibrationServiceHook(void)
{
SeatMove_HandleCommand(&frame);
}
else if(frame.identifier == SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID)
{
SeatMove_HandleHilSelfHeal(&frame);
}
else
{
SeatControllerThermal_HandleFrame(&frame);
@@ -1042,6 +1070,270 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
g_seatMoveAcceptedCount++;
}
static bool SeatMove_SendHilSelfHealResponse(uint8_t caseIndex,
uint8_t operation,
uint16_t token,
uint8_t result,
uint8_t evidence)
{
uint8_t bytes[8];
uint8_t index;
uint32_t data0;
uint32_t data1;
bytes[0] = 0x5Au;
bytes[1] = caseIndex;
bytes[2] = operation;
bytes[3] = (uint8_t)(token & 0xFFu);
bytes[4] = (uint8_t)(token >> 8u);
bytes[5] = result;
bytes[6] = evidence;
bytes[7] = 0u;
for(index = 0u; index < 7u; index++)
{
bytes[7] = (uint8_t)(bytes[7] + bytes[index]);
}
data0 = (uint32_t)bytes[0] |
((uint32_t)bytes[1] << 8u) |
((uint32_t)bytes[2] << 16u) |
((uint32_t)bytes[3] << 24u);
data1 = (uint32_t)bytes[4] |
((uint32_t)bytes[5] << 8u) |
((uint32_t)bytes[6] << 16u) |
((uint32_t)bytes[7] << 24u);
return BspSeatMoveCan_Send(SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID,
data0,
data1);
}
static void SeatMove_HandleHilSelfHeal(
const bsp_seat_move_can_frame_t *frame)
{
uint8_t bytes[8];
uint8_t checksum = 0u;
uint8_t caseIndex;
uint8_t operation;
uint8_t result = SEAT_MOVE_HIL_RESPONSE_OK;
uint8_t evidence = 0u;
uint16_t token;
uint16_t suppliedArmToken;
uint8_t index;
bool operationOk = false;
seat_motor_status_t status;
if((frame == NULL) || (frame->dlc != SEAT_MOVE_CAN_DLC))
{
return;
}
bytes[0] = (uint8_t)(frame->data0 & 0xFFu);
bytes[1] = (uint8_t)((frame->data0 >> 8u) & 0xFFu);
bytes[2] = (uint8_t)((frame->data0 >> 16u) & 0xFFu);
bytes[3] = (uint8_t)((frame->data0 >> 24u) & 0xFFu);
bytes[4] = (uint8_t)(frame->data1 & 0xFFu);
bytes[5] = (uint8_t)((frame->data1 >> 8u) & 0xFFu);
bytes[6] = (uint8_t)((frame->data1 >> 16u) & 0xFFu);
bytes[7] = (uint8_t)((frame->data1 >> 24u) & 0xFFu);
for(index = 0u; index < 7u; index++)
{
checksum = (uint8_t)(checksum + bytes[index]);
}
if((bytes[0] != 0xA5u) || (checksum != bytes[7]))
{
return;
}
caseIndex = bytes[1];
operation = bytes[2];
token = (uint16_t)((uint16_t)bytes[3] |
((uint16_t)bytes[4] << 8u));
suppliedArmToken = (uint16_t)((uint16_t)bytes[5] |
((uint16_t)bytes[6] << 8u));
if((caseIndex != 1u) && (caseIndex != 2u) && (caseIndex != 3u) &&
(caseIndex != 5u) && (caseIndex != 6u))
{
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED, 0u);
return;
}
if(operation == 0u)
{
if((bytes[5] != 0x53u) || (bytes[6] != 0x46u))
{
result = SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED;
}
else
{
s_hilArmedCase = caseIndex;
s_hilArmToken = token;
s_hilArmExpiresMs = Motor_GetTimeMs() +
SEAT_MOVE_HIL_ARM_WINDOW_MS;
}
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token, result, evidence);
return;
}
if((s_hilArmedCase != caseIndex) ||
(s_hilArmToken != suppliedArmToken) ||
SeatMove_TimeReached(Motor_GetTimeMs(), s_hilArmExpiresMs))
{
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_UNAUTHORIZED, 0u);
return;
}
/* Every arm token authorizes exactly one mutating or verifying request. */
s_hilArmedCase = 0u;
s_hilArmToken = 0u;
s_hilArmExpiresMs = 0u;
if(operation == 1u)
{
switch(caseIndex)
{
case 1u:
SeatMoveApplication_StopAll(token);
operationOk =
(MOTOR_API_SUCCESS == Motor_InvalidatePosition(
SeatMove_PhysicalMode(2u)));
if(operationOk)
{
SeatMoveStore_RejectRestore();
s_hilFaultActive[caseIndex] = true;
s_storeSavePending = true;
s_storeSaveNotBeforeMs = Motor_GetTimeMs() +
SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
s_motorStatusPendingMask |= (uint8_t)(1u << 2u);
s_diagnosticPendingMask |= (uint8_t)(1u << 2u);
s_systemStatusPending = true;
}
else
{
evidence = 0xE2u;
}
break;
case 2u:
SeatMoveApplication_StopAll(token);
operationOk = SeatMoveStore_HilInjectCrcCorruption();
s_hilFaultActive[caseIndex] = operationOk;
s_systemStatusPending = true;
break;
case 3u:
SeatMoveApplication_StopAll(token);
operationOk = Motor_WaitUntilStopped(
SeatMove_PhysicalMode(0u)) &&
!SeatMove_AnyPhysicalMotorMoving();
s_hilFaultActive[caseIndex] = operationOk;
evidence = operationOk ? 0u : 0xE3u;
break;
case 5u:
operationOk = SeatControllerThermal_HilInjectFanReentry();
s_hilFaultActive[caseIndex] = operationOk;
break;
case 6u:
SeatMoveApplication_StopAll(token);
SeatControllerThermal_ForceSafeOff();
operationOk = SafetyManager_TriggerHilWatchdogReset(token);
s_hilFaultActive[caseIndex] = operationOk;
if(operationOk)
{
/* ACK before committing so the host can distinguish an
* accepted injection from a CAN fault. Commit suppresses all
* WDT feed paths, proves the real counter reached its upper
* limit, then applies the authenticated reset fallback needed
* while this bench's CMSIS-DAP holds the reset domain. */
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token,
SEAT_MOVE_HIL_RESPONSE_OK, 0u);
Cy_SysTick_DelayInUs(5000u);
SafetyManager_CommitHilWatchdogReset();
}
break;
default:
break;
}
}
else if(operation == 2u)
{
switch(caseIndex)
{
case 1u:
operationOk = SeatMove_GetStatus(2u, &status) &&
status.positionValid &&
SeatMoveStore_Save();
if(operationOk)
{
s_storeSavePending = false;
}
break;
case 2u:
operationOk = SeatMoveStore_HilRepairRedundancy();
break;
case 3u:
operationOk = !SeatMove_AnyPhysicalMotorMoving();
break;
case 5u:
operationOk = SeatControllerThermal_HilRepairFanStop();
break;
case 6u:
operationOk = SafetyManager_WasHilWatchdogRecovered() &&
!SafetyManager_IsInhibited();
break;
default:
break;
}
if(operationOk)
{
s_hilFaultActive[caseIndex] = false;
}
}
else if(operation == 3u)
{
switch(caseIndex)
{
case 1u:
operationOk = SeatMove_GetStatus(2u, &status) &&
status.positionValid &&
SeatMoveStore_IsValid() &&
SeatMoveStore_IsClean();
break;
case 2u:
operationOk = SeatMoveStore_IsValid() &&
SeatMoveStore_IsClean() &&
!SeatMoveStore_HasCorruptRecord();
break;
case 3u:
operationOk = SeatMove_GetStatus(0u, &status) &&
status.positionValid &&
!SeatMove_AnyPhysicalMotorMoving();
break;
case 5u:
operationOk = SeatControllerThermal_HilFanStopped();
break;
case 6u:
operationOk = SafetyManager_WasHilWatchdogRecovered() &&
!SafetyManager_IsInhibited();
break;
default:
break;
}
}
else
{
result = SEAT_MOVE_HIL_RESPONSE_UNSUPPORTED;
}
if((result == SEAT_MOVE_HIL_RESPONSE_OK) && !operationOk)
{
result = (operation == 3u) ?
SEAT_MOVE_HIL_RESPONSE_PRECONDITION :
SEAT_MOVE_HIL_RESPONSE_EXECUTION;
}
(void)SeatMove_SendHilSelfHealResponse(
caseIndex, operation, token, result, evidence);
}
static void SeatMove_StopMotor(uint8_t motor, uint16_t sequence)
{
motor_mode_t mode;
@@ -284,6 +284,75 @@ uint32_t SeatMoveStore_GetSequence(void)
return s_valid ? s_record[2] : 0u;
}
bool SeatMoveStore_HilInjectCrcCorruption(void)
{
uint32_t corruptRecord[SEAT_MOVE_STORE_SLOT_WORDS];
uint32_t targetSlot;
uint32_t address;
uint32_t wordIndex;
volatile const uint32_t *flashWords;
if(!s_valid || !s_clean)
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
corruptRecord[wordIndex] = s_record[wordIndex];
}
corruptRecord[2] = s_record[2] + 1u;
corruptRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
SeatMoveStore_Crc32(corruptRecord,
SEAT_MOVE_STORE_CRC_WORD_INDEX) ^ 0x00000001u;
targetSlot = s_activeSlot ^ 1u;
address = SeatMoveStore_SlotAddress(targetSlot);
flashWords = (volatile const uint32_t *)(uintptr_t)address;
Cy_FlashSectorErase(address, CY_FLASH_DRIVER_BLOCKING);
if(!Cy_WorkFlashBlankCheck(address, CY_FLASH_DRIVER_BLOCKING))
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
Cy_FlashWriteWork(address + (wordIndex * sizeof(uint32_t)),
&corruptRecord[wordIndex],
CY_FLASH_DRIVER_BLOCKING);
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
if(flashWords[wordIndex] != corruptRecord[wordIndex])
{
return false;
}
}
/* The active in-RAM record deliberately remains the older valid slot. */
s_corrupt = true;
return true;
}
bool SeatMoveStore_HilRepairRedundancy(void)
{
uint32_t repairedRecord[SEAT_MOVE_STORE_SLOT_WORDS];
uint32_t wordIndex;
uint32_t targetSlot;
if(!s_valid)
{
return false;
}
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
{
repairedRecord[wordIndex] = s_record[wordIndex];
}
repairedRecord[2] = s_record[2] + 2u;
repairedRecord[3] = 1u;
repairedRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
SeatMoveStore_Crc32(repairedRecord,
SEAT_MOVE_STORE_CRC_WORD_INDEX);
targetSlot = s_activeSlot ^ 1u;
return SeatMoveStore_WriteRecord(repairedRecord, targetSlot);
}
static uint32_t SeatMoveStore_Crc32(const uint32_t *words, uint32_t wordCount)
{
uint32_t crc = 0xFFFFFFFFu;
@@ -16,4 +16,11 @@ bool SeatMoveStore_IsClean(void);
bool SeatMoveStore_HasCorruptRecord(void);
uint32_t SeatMoveStore_GetSequence(void);
/* Token-gated bench HIL helpers. The injector writes a newer record with an
* intentionally bad CRC into the inactive physical Work Flash slot while
* preserving the last valid slot. Repair atomically replaces that slot with
* a still-newer valid record. */
bool SeatMoveStore_HilInjectCrcCorruption(void);
bool SeatMoveStore_HilRepairRedundancy(void);
#endif /* SEAT_MOVE_STORE_H */
@@ -50,6 +50,10 @@ static const cy_stc_id_filter_t s_standardIdFilters[] =
SEAT_MOVE_CAN_COMMAND_ID,
SEAT_MOVE_CAN_COMMAND_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID,
SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
@@ -30,7 +30,13 @@
#define SEAT_BACKREST_HEATER_DEFAULT_LEVEL (0u)
#define SEAT_BACKREST_HEATER_TASK_PERIOD_US (1000u)
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (10u)
/*
* Do not sample at the 10 ms PWM period. Equal periods phase-lock the ADC
* to one PWM point and can report zero current continuously for a healthy
* low-duty heater. Seven milliseconds walks through every PWM phase while
* remaining slow enough for the existing SAR ADC service.
*/
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (7u)
#define SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS (3u)
#define SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS (600000u)
@@ -29,18 +29,22 @@
#define SEAT_MOVE_M0_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M1_DEFAULT_TIMEOUT_MS (8000u)
#define SEAT_MOVE_M2_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (10000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (30000u)
#define SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT (90u)
/* The connected loaded legrest leaves its hard stop more reliably at the
* empirically verified diagnostic PWM than at maximum PWM. It is selected
* only after a supervised endpoint-departure attempt produced no Hall edge. */
#define SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT (60u)
/* A Hall target is discrete and an endpoint can physically stop one or two
* pulses short because of switch/gear backlash. If a position move is
* already this close when an endpoint/stall event stops the bridge, classify
* the requested position as reached instead of retrying and latching STALL.
/* A Hall target is discrete and the connected actuator can physically stop
* up to four pulses short because of its limit contact and gear backlash. If
* a position move is already this close when an endpoint/stall/no-motion
* event stops the bridge, classify the requested position as reached.
* The raw lastMotorEvent remains available in 0x303 for diagnosis. */
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (2u)
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (4u)
/* CAN4 wiring verified by Seat_Fan_demo and D:\wcl\can. P3.1 is shared with
* the DRV8718 DVDD enable on this board; both users require it to remain high. */
@@ -67,7 +71,7 @@
#define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u)
/* Repository image identity carried by 0x301 byte 7 high nibble. Increment
* whenever a host-visible safety or motion fix requires automatic reflash. */
#define SEAT_MOVE_FIRMWARE_REVISION (13u)
#define SEAT_MOVE_FIRMWARE_REVISION (15u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
@@ -75,6 +79,15 @@
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_DYNAMIC_STATUS_ID (0x306u)
/* The self-healing hook is accepted only by this bench-demo image and only
* after a short-lived token arm exchange. */
#define SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID (0x2F0u)
#define SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID (0x2F1u)
#define SEAT_MOVE_HIL_ARM_WINDOW_MS (30000u)
#define SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX (0u)
#define SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX (1u)
#define SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX (2u)
#define SEAT_MOVE_HIL_WDT_MARKER (0x48494C36u) /* "HIL6" */
#define SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS (100u)
#define SEAT_MOVE_DYNAMIC_STATUS_PERIOD_MS (100u)
/* Native dynamic modes are bounded independently of the host process. The
@@ -160,7 +173,9 @@
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu))
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_HIL_SELF_HEAL_RESPONSE_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif
@@ -172,6 +187,24 @@
#error "The selected debug-session backup register is not available."
#endif
#if ((SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX == \
SEAT_MOVE_DEBUG_SESSION_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX) || \
(SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX == \
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX))
#error "The HIL WDT credential requires three dedicated backup registers."
#endif
#if ((SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS == 0u) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS < \
SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS) || \
@@ -492,6 +492,7 @@ static uint32_t currentSenseLastServiceTicks = 0ul;
static bool Motor_CalibrationRangeIsPlausible(uint32_t motorIndex,
uint32_t measuredPulses);
static bool Motor_WaitForCoastToSettle(motor_mode_t mode);
CY_WEAK void Motor_CalibrationServiceHook(void)
{
@@ -2870,6 +2871,19 @@ bool Motor_IsMoving(motor_mode_t mode)
return moving;
}
bool Motor_WaitUntilStopped(motor_mode_t mode)
{
if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)))
{
return false;
}
if(Motor_IsMoving(mode))
{
(void)Motor_StopMode(mode);
}
return Motor_WaitForCoastToSettle(mode) && !Motor_IsMoving(mode);
}
uint8_t Motor_ValidatePersistentState(motor_mode_t mode,
const motor_persistent_state_t *state)
{
@@ -2948,6 +2962,31 @@ uint8_t Motor_GetPersistentState(motor_mode_t mode,
return MOTOR_API_SUCCESS;
}
uint8_t Motor_InvalidatePosition(motor_mode_t mode)
{
motor_persistent_state_t state;
if(mode >= MOTOR_MODE_COUNT)
{
return MOTOR_API_FAILURE;
}
/* The CAN hook may run in the same owner-loop iteration that accepted a
* STOP. Finish the bounded stop and Hall-coast transition before
* enforcing the persistent-state API's stopped-axis precondition. */
if(Motor_IsMoving(mode))
{
(void)Motor_StopMode(mode);
}
if((false == Motor_WaitForCoastToSettle(mode)) ||
Motor_IsMoving(mode) ||
(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &state)))
{
return MOTOR_API_FAILURE;
}
state.positionValid = false;
return Motor_ApplyPersistentState(mode, &state);
}
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100)
{
@@ -3579,6 +3618,8 @@ static uint8_t Motor_CalibrationProbeMotion(
uint32_t *travelEdges)
{
uint32_t index = (uint32_t)mode;
uint32_t verifyEdges = (mode == MOTOR_MODE_3_LINEAR_TRACK) ?
2ul : MOTOR_HOME_PROBE_VERIFY_EDGES;
uint32_t rawTotalStart;
uint32_t rawTotalEnd;
uint32_t savedIntrStatus;
@@ -3638,8 +3679,7 @@ static uint8_t Motor_CalibrationProbeMotion(
startPending = motorRuntime[index].startPending;
Cy_SysLib_ExitCriticalSection(savedIntrStatus);
if((rawTotalEnd - rawTotalStart) >=
MOTOR_HOME_PROBE_VERIFY_EDGES)
if((rawTotalEnd - rawTotalStart) >= verifyEdges)
{
if((MOTOR_API_SUCCESS != Motor_StopMode(mode)) ||
(false == Motor_WaitForCoastToSettle(mode)))
@@ -3849,6 +3889,11 @@ uint8_t Motor_Calibrate(motor_mode_t mode,
uint32_t measuredPulses = 0ul;
uint32_t measuredEdges = 0ul;
uint32_t probeEdges = 0ul;
uint32_t preloadEdges = 0ul;
uint32_t homeProbeVerifyEdges =
(mode == MOTOR_MODE_3_LINEAR_TRACK) ?
2ul : MOTOR_HOME_PROBE_VERIFY_EDGES;
uint8_t probeAttempt = 0u;
#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u)
uint32_t firstNearPulses = 0ul;
uint32_t firstNearEdges = 0ul;
@@ -3915,13 +3960,54 @@ uint8_t Motor_Calibrate(motor_mode_t mode,
firstNearEdges = measuredEdges;
#endif
if((status == MOTOR_API_SUCCESS) &&
(measuredEdges < MOTOR_HOME_PROBE_VERIFY_EDGES))
(measuredEdges < homeProbeVerifyEdges))
{
/* Do not turn a non-start into a fake Hall zero. A genuine
* already-near condition is proved by moving across several Hall
* edges toward far and seeking the same path back toward near. */
status = Motor_CalibrationProbeMotion(
mode, MOTOR_DIRECTION_TOWARD_FAR, legTimeoutMs, &probeEdges);
/* A loaded mechanism can need more than one bounded energize
* cycle to leave a mechanical hard stop. Retry the departure,
* but still require real Hall edges before accepting the home.
* This improves endpoint breakaway without weakening the open
* Hall / disconnected-motor diagnostic. The loaded M3 linkage
* is accepted after two filtered raw Hall edges: the real bench
* repeatedly produces one complete Hall pulse before its bounded
* probe stops, while an open/disconnected Hall still produces
* zero. The other axes retain the four-edge proof. */
for(probeAttempt = 0u; probeAttempt < 3u; probeAttempt++)
{
status = Motor_CalibrationProbeMotion(
mode,
MOTOR_DIRECTION_TOWARD_FAR,
legTimeoutMs,
&probeEdges);
if(status == MOTOR_API_SUCCESS)
{
break;
}
if((mode == MOTOR_MODE_3_LINEAR_TRACK) &&
(MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(
mode, SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT)))
{
status = MOTOR_API_FAILURE;
break;
}
/* A failed probe has already stopped and settled the bridge.
* Briefly re-energize toward the known hard stop before the
* next departure. On the loaded M3 linkage this alternating
* preload releases static lash which repeated starts in the
* same direction do not. Its result is deliberately ignored:
* only a later far probe with real Hall edges can pass. */
if(probeAttempt < 2u)
{
preloadEdges = 0ul;
(void)Motor_CalibrationProbeMotion(
mode,
MOTOR_DIRECTION_TOWARD_NEAR,
legTimeoutMs,
&preloadEdges);
}
}
if(status == MOTOR_API_SUCCESS)
{
status = Motor_CalibrationSeekEndpoint(
@@ -170,6 +170,9 @@ uint8_t Motor_SetSafetyOptions(motor_mode_t mode,
uint8_t Motor_SetFixedDutyPercent(motor_mode_t mode, uint8_t dutyPercent);
uint8_t Motor_StartOpenLoop(motor_mode_t mode, motor_direction_t direction);
bool Motor_IsMoving(motor_mode_t mode);
/* Complete a commanded stop plus bounded Hall-coast tracking before a
* safety decision that requires the mechanism to be physically settled. */
bool Motor_WaitUntilStopped(motor_mode_t mode);
/*
* Restore/store Hall-domain state. These calls are accepted only while the
@@ -181,6 +184,10 @@ uint8_t Motor_ApplyPersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_GetPersistentState(motor_mode_t mode,
motor_persistent_state_t *state);
/* Bench HIL support: invalidate only the stopped axis' absolute-position
* confidence while preserving its measured Hall count and confirmed range.
* Production command handling never calls this API. */
uint8_t Motor_InvalidatePosition(motor_mode_t mode);
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100);
File diff suppressed because it is too large Load Diff
@@ -62,6 +62,9 @@ from cognitive_agent.application.vehicle_profile import ( # noqa: E402
VehicleProfile,
VehicleProfileRegistry,
)
from cognitive_agent.application.zcan_seat_executor import ( # noqa: E402
ZcanBridgeSeatExecutor,
)
from cognitive_agent.infrastructure.llm.openai_responses import ( # noqa: E402
OpenAIIntentError,
OpenAIIntentInterpreter,
@@ -217,7 +220,7 @@ class SeatDemoService:
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
"hardware_execution_allowed": (
self.hardware_settings.configured
and self.vehicle_can_settings.configured
or self.vehicle_can_settings.configured
),
"hardware": hardware_info,
"vehicle_can": self.vehicle_can_settings.public_info(),
@@ -334,6 +337,29 @@ class SeatDemoService:
def _new_hardware_executor(
self, bus_factory: Any | None = None
) -> HardwareSeatExecutor:
shared_bridge_enabled = os.getenv(
"COGNITIVE_AGENT_SEAT_SHARED_ZCANPRO_BRIDGE_ENABLED", "true"
).strip().lower() in {"1", "true", "yes", "on"}
if bus_factory is None and shared_bridge_enabled:
# Import lazily so the ordinary dry-run service does not initialize
# any self-repair runtime state. Both modules then share the one
# process that is allowed to own the Windows ZLG adapter.
from cognitive_agent.application.self_repair.service import (
BRIDGE_MANAGER,
PENDING_DIR,
REQUIRED_BRIDGE_VERSION,
STATUS_PATH,
ZCANPRO_EXE,
)
if ZCANPRO_EXE.is_file():
return ZcanBridgeSeatExecutor(
self.hardware_matrix,
BRIDGE_MANAGER,
STATUS_PATH,
PENDING_DIR,
required_version=REQUIRED_BRIDGE_VERSION,
auto_home=self.hardware_settings.auto_home_enabled,
)
resolved_bus_factory = bus_factory or self.hardware_settings.bus_factory()
return HardwareSeatExecutor(
self.hardware_matrix,
@@ -554,7 +580,8 @@ class SeatDemoService:
)
parsed = merge_native_can_intent(parsed, utterance)
parsed = self._partition_native_actions(
parsed, promote_supported=hardware_request
parsed,
promote_supported=(hardware_request and self.vehicle_can_settings.enabled),
)
if hardware_request and not control_plane_request:
if parsed.get("actions"):
@@ -4,7 +4,8 @@ The original HIL demo mixed HTTP hosting, business state, firmware tooling and
ZCANPRO process control in one script. This module keeps the proven state
machine and diagnostic rules while exposing them through the repository's
existing server. Every machine-specific path is supplied through environment
configuration; importing the module never launches hardware tooling.
configuration or discovered standard installations; importing the module never
launches hardware tooling.
"""
from __future__ import unicode_literals
@@ -29,6 +30,9 @@ from .signal_diagnostic import (
from .zcan_bridge import (
ZcanBridgeManager, run_self_test as run_bridge_manager_self_test,
)
from cognitive_agent.infrastructure.firmware.t2g_seat import (
EXPECTED_FIRMWARE_REVISION,
)
REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
@@ -39,6 +43,17 @@ def _env_path(name, default):
return Path(value).expanduser() if value else Path(default)
def _env_or_installed_path(name, candidates, fallback):
value = os.getenv(name, "").strip()
if value:
return Path(value).expanduser()
for candidate in candidates:
path = Path(candidate)
if path.exists():
return path
return Path(fallback)
def _env_flag(name, default=False):
value = os.getenv(name)
if value is None:
@@ -71,16 +86,28 @@ HARDWARE_LOG_ROOT = _env_path(
REPOSITORY_ROOT / "artifacts" / "self-repair" / "hardware",
)
PROJECT_DIR = _env_path(
"SELF_REPAIR_FIRMWARE_ROOT", REPOSITORY_ROOT / "__unconfigured_firmware__")
"SELF_REPAIR_FIRMWARE_ROOT",
REPOSITORY_ROOT / "firmware" / "t2g" / "Seat_Controller_demo")
IAR_PROJECT_PATH = PROJECT_DIR / "IAR" / "Seat_Controller_demo.ewp"
FIRMWARE_OUT_PATH = PROJECT_DIR / "IAR" / "rev_d" / "Exe" / \
"Seat_Controller_demo.out"
FIRMWARE_HEX_PATH = PROJECT_DIR / "IAR" / "rev_d" / "Exe" / \
"Seat_Controller_demo.hex"
IAR_BUILD_PATH = _env_path(
"SELF_REPAIR_IAR_BUILD_PATH", REPOSITORY_ROOT / "__unconfigured_iar_build__")
CSPY_BAT_PATH = _env_path(
"SELF_REPAIR_CSPY_BAT_PATH", REPOSITORY_ROOT / "__unconfigured_cspy__")
IAR_INSTALL_ROOTS = (
Path(r"D:\Program Files\IAR Systems\Embedded Workbench 9.2"),
Path(r"C:\Program Files\IAR Systems\Embedded Workbench 9.2"),
Path(r"C:\Program Files\IAR Systems\Embedded Workbench 9.4"),
)
IAR_BUILD_PATH = _env_or_installed_path(
"SELF_REPAIR_IAR_BUILD_PATH",
tuple(root / "common" / "bin" / "iarbuild.exe"
for root in IAR_INSTALL_ROOTS),
REPOSITORY_ROOT / "__unconfigured_iar_build__")
CSPY_BAT_PATH = _env_or_installed_path(
"SELF_REPAIR_CSPY_BAT_PATH",
tuple(root / "common" / "bin" / "CSpyBat.exe"
for root in IAR_INSTALL_ROOTS),
REPOSITORY_ROOT / "__unconfigured_cspy__")
CSPY_GENERAL_PATH = _env_path(
"SELF_REPAIR_CSPY_GENERAL_PATH",
PROJECT_DIR / "IAR" / "settings" / "Seat_Controller_demo.rev_d.general.xcl",
@@ -96,13 +123,19 @@ FIRMWARE_LOG_ROOT = _env_path(
FIRMWARE_SOURCE_ROOTS = tuple(
PROJECT_DIR / name for name in ("App", "Bsp", "Config", "Driver"))
FIRMWARE_SOURCE_SUFFIXES = frozenset((".c", ".h", ".s", ".asm"))
REQUIRED_BRIDGE_VERSION = "2026-08-07-reset-home-far-target-v15"
ZCANPRO_EXE = _env_path(
"SELF_REPAIR_ZCANPRO_EXE", REPOSITORY_ROOT / "__unconfigured_zcanpro__")
ZCANPRO_SCRIPT_CONFIG = _env_path(
"SELF_REPAIR_ZCANPRO_SCRIPT_CONFIG",
REPOSITORY_ROOT / "__unconfigured_zcanpro_script_config__",
REQUIRED_BRIDGE_VERSION = "2026-08-09-real-self-heal-v32"
ZCANPRO_ROOTS = (
Path(r"C:\Program Files (x86)\ZCANPRO"),
Path(r"C:\Program Files\ZCANPRO"),
)
ZCANPRO_EXE = _env_or_installed_path(
"SELF_REPAIR_ZCANPRO_EXE",
tuple(root / "ZCANPRO.exe" for root in ZCANPRO_ROOTS),
REPOSITORY_ROOT / "__unconfigured_zcanpro__")
ZCANPRO_SCRIPT_CONFIG = _env_or_installed_path(
"SELF_REPAIR_ZCANPRO_SCRIPT_CONFIG",
tuple(root / "config" / "extend_script.ini" for root in ZCANPRO_ROOTS),
REPOSITORY_ROOT / "__unconfigured_zcanpro_script_config__")
BRIDGE_MANAGER = ZcanBridgeManager(
STATUS_PATH, BRIDGE_SCRIPT_PATH,
required_version=REQUIRED_BRIDGE_VERSION,
@@ -268,6 +301,13 @@ class ConsoleState(object):
"CAN链路有流量但缺少0x301~0x305,判定为未烧录/固件不匹配类问题" if
bridge.get("bus_traffic") else
"桥接在线但座椅协议无报文,判定为未烧录/固件未运行类问题")
elif not bridge.get("firmware_revision_ok"):
reason = "FIRMWARE_REVISION_MISMATCH"
detail = (
"Seat_Controller_demo固件revision=%s,期望revision=%s,"
"需要自动重新构建并烧录" %
(bridge.get("firmware_revision", "未知"),
EXPECTED_FIRMWARE_REVISION))
else:
return {
"ready": True, "auto_flash": False,
@@ -334,7 +374,8 @@ class ConsoleState(object):
last_bridge = self._bridge_summary(last_status, time.time())
if (last_bridge.get("online") and
(not require_protocol or
last_bridge.get("protocol_online"))):
(last_bridge.get("protocol_online") and
last_bridge.get("firmware_revision_ok")))):
return last_status, last_bridge
time.sleep(0.25)
return last_status, last_bridge or {}
@@ -385,7 +426,8 @@ class ConsoleState(object):
trigger_reason=qualification.get("reason_code"))
status, bridge = self._wait_bridge(require_protocol=True,
timeout_s=12.0)
if not bridge.get("protocol_online"):
if (not bridge.get("protocol_online") or
not bridge.get("firmware_revision_ok")):
detail = ("自动烧录已执行,但仍未收到0x301~0x305;"
"硬件诊断未启动,请检查ECU供电或烧录日志")
with self.lock:
@@ -430,6 +472,7 @@ class ConsoleState(object):
online = age is not None and age < 3.0 and version_ok
system = {} if not isinstance(status, dict) else (
status.get("system") or {})
firmware_revision = system.get("firmware_revision")
frame_epochs = {} if not isinstance(status, dict) else (
status.get("frame_epochs") or {})
recent_frame_ids = []
@@ -460,6 +503,10 @@ class ConsoleState(object):
"current_request_id": None if not isinstance(status, dict) else status.get(
"current_request_id"),
"position_valid_mask": int(system.get("position_valid_mask", 0)),
"firmware_revision": firmware_revision,
"expected_firmware_revision": EXPECTED_FIRMWARE_REVISION,
"firmware_revision_ok":
firmware_revision == EXPECTED_FIRMWARE_REVISION,
"active_mask": int(system.get("active_mask", 0)),
"faulted_mask": int(system.get("faulted_mask", 0)),
"bus": {} if not isinstance(status, dict) else (
@@ -618,6 +665,16 @@ class ConsoleState(object):
self.hardware_active = False
if not self.hardware_run_id or self.hardware_report_path is not None:
return
active_test = (self.hardware_result or {}).get("active_test") or {}
test_phase = active_test.get("phase")
if test_phase in ("PASSED", "FAILED"):
self.hardware_prepare.update({
"state": "DIAGNOSTIC_%s" % test_phase,
"detail": active_test.get("detail") or
active_test.get("title") or
"21针主动检测已结束",
"finished_epoch": now,
})
report = {
"schema": "seat-hardware-diagnostic-report-v1",
"run_id": self.hardware_run_id,
@@ -625,8 +682,7 @@ class ConsoleState(object):
"events": list(self.hardware_events),
"diagnosis": self.hardware_result,
"startup_rule_check": self.hardware_self_test,
"active_pin_test": (self.hardware_result or {}).get(
"active_test"),
"active_pin_test": active_test,
"bridge": self._bridge_summary(status, now),
}
path = HARDWARE_LOG_ROOT / self.hardware_run_id / \
@@ -839,10 +895,11 @@ class ConsoleState(object):
"firmware": self._firmware_snapshot(),
}
def start(self, case_id, execution_mode="shadow"):
def start(self, case_id, execution_mode="shadow", allow_batch=False):
with self.lock:
if execution_mode == "bench":
if self.hardware_active or self.batch.get("active"):
if (self.hardware_active or
(self.batch.get("active") and not allow_batch)):
raise ValueError("硬件诊断或批量验证正在运行,不能启动实物故障用例")
scenario = SCENARIO_BY_ID.get(case_id) or {}
if not (scenario.get("bench") or {}).get("supported", False):
@@ -850,8 +907,9 @@ class ConsoleState(object):
"该用例没有安全的实物注入手段,请使用影子仿真")
self.bridge_manager.ensure_async()
now = time.time()
status = _read_json(STATUS_PATH)
bridge = self._bridge_summary(status, now)
status, bridge = self._wait_bridge(
require_protocol=True, timeout_s=12.0)
now = time.time()
if not bridge.get("online"):
raise ValueError("ZCANPRO桥接未在线,不能向实物ECU写入故障")
if not bridge.get("protocol_online"):
@@ -888,7 +946,8 @@ class ConsoleState(object):
self.bench_request_id = None
return self.engine.start(case_id, execution_mode="shadow")
def reset(self, execution_mode="shadow"):
def reset(self, execution_mode="shadow", allow_batch=False,
force_all_axes=True, home_nonzero_axes=False):
with self.lock:
if (self.engine.execution_mode == "bench" and
self.engine.phase not in ("IDLE", PHASE_PASSED,
@@ -907,12 +966,14 @@ class ConsoleState(object):
})
return {"healing": self.engine.snapshot(),
"bench_reset": dict(self.bench_reset)}
if self.hardware_active or self.batch.get("active"):
if (self.hardware_active or
(self.batch.get("active") and not allow_batch)):
raise ValueError("硬件诊断或批量验证正在运行,不能执行四轴归零")
self.bridge_manager.ensure_async()
now = time.time()
status = _read_json(STATUS_PATH)
bridge = self._bridge_summary(status, now)
status, bridge = self._wait_bridge(
require_protocol=True, timeout_s=12.0)
now = time.time()
if not bridge.get("online") or not bridge.get("protocol_online"):
raise ValueError("CAN桥接或座椅ECU协议未在线,不能执行四轴归零")
if (status or {}).get("current_request_id"):
@@ -925,7 +986,10 @@ class ConsoleState(object):
"armed_until_epoch": now + 600.0,
"source": "seat_self_healing_console",
"action": "initialize_known_ranges",
"payload": {"force_all_axes": True},
"payload": {
"force_all_axes": bool(force_all_axes),
"home_nonzero_axes": bool(home_nonzero_axes),
},
}
zero = self._bench_zero_snapshot(status)
self.bench_reset = {
@@ -938,13 +1002,64 @@ class ConsoleState(object):
return {"healing": self.engine.snapshot(),
"bench_reset": dict(self.bench_reset)}
def _wait_bench_reset(self, timeout_s=620.0):
"""Wait for the bridge-owned four-axis homing request to finish."""
deadline = time.time() + float(timeout_s)
last = None
while time.time() < deadline:
last = self.snapshot()
reset = last.get("bench_reset") or {}
if reset.get("phase") in ("PASSED", "FAILED"):
return reset
time.sleep(0.2)
with self.lock:
self.bench_reset.update({
"phase": "FAILED",
"finished_epoch": time.time(),
"summary": "四轴归零等待超时,批量验证已停止",
})
return dict(self.bench_reset)
@staticmethod
def _wait_bench_bridge_idle(timeout_s=8.0):
"""Wait until the bridge clears its just-published request result."""
deadline = time.time() + float(timeout_s)
while time.time() < deadline:
status = _read_json(STATUS_PATH) or {}
if not status.get("current_request_id"):
return True
time.sleep(0.05)
return False
def _batch_worker(self, execution_mode):
try:
for scenario in public_scenarios():
with self.lock:
self.batch["current_case_id"] = scenario["id"]
self.latest_report_path = None
self.start(scenario["id"], execution_mode)
if execution_mode == "bench":
if not self._wait_bench_bridge_idle():
raise ValueError(
"%s前台架桥接任务未在安全窗口内结束" % scenario["id"])
reset = {}
for reset_attempt in range(2):
self.reset(
"bench", allow_batch=True,
force_all_axes=False,
home_nonzero_axes=True)
reset = self._wait_bench_reset()
if (reset.get("phase") == "PASSED" and
reset.get("all_zero")):
break
if reset_attempt == 0:
time.sleep(1.0)
if (reset.get("phase") != "PASSED" or
not reset.get("all_zero")):
raise ValueError(
"%s前四轴自动归零失败:%s" %
(scenario["id"], reset.get("summary", "未知错误")))
self.start(
scenario["id"], execution_mode, allow_batch=True)
while True:
value = self.snapshot()
phase = value["healing"]["phase"]
@@ -961,9 +1076,12 @@ class ConsoleState(object):
"duration_s": value["healing"].get("elapsed_s"),
"diagnosed_code": value["healing"].get("diagnosed_code"),
"execution_mode": execution_mode,
"snapshot_path": (None if self.latest_report_path is None else
str(self.latest_report_path.parent /
"diagnostic_snapshot_success.json")),
"report_path": (None if self.latest_report_path is None else
str(self.latest_report_path)),
"snapshot_path": (
None if self.latest_report_path is None or not passed else
str(self.latest_report_path.parent /
"diagnostic_snapshot_success.json")),
}
self.batch["results"].append(result)
self.batch["completed"] += 1
@@ -990,8 +1108,9 @@ class ConsoleState(object):
if execution_mode not in ("shadow", "bench"):
raise ValueError("未知批量验证方式")
if execution_mode == "bench":
raise ValueError(
"实物台架需逐项验证:每个用例前请人工点击重置并确认四轴归零")
if self.hardware_active:
raise ValueError("硬件诊断正在运行,不能启动实物批量验证")
self.bridge_manager.ensure_async()
self.batch = self._new_batch_state(execution_mode)
self.batch["active"] = True
self.batch["started_epoch"] = time.time()
@@ -1376,7 +1495,10 @@ class SelfRepairService(object):
return self.state.start_batch(payload.get("execution_mode", "shadow"))
def reset_healing(self, payload):
return self.state.reset(payload.get("execution_mode", "shadow"))
return self.state.reset(
payload.get("execution_mode", "shadow"),
force_all_axes=bool(payload.get("force_all_axes", True)),
home_nonzero_axes=bool(payload.get("home_nonzero_axes", False)))
def start_hardware(self):
return self.state.start_hardware()
@@ -24,6 +24,7 @@ class ZcanBridgeManager(object):
def __init__(self, status_path, script_path, heartbeat_stale_s=3.0,
poll_s=1.0, retry_cooldown_s=15.0,
busy_heartbeat_grace_s=45.0,
required_version=None, zcanpro_exe=None,
script_config_path=None, auto_start=False):
self.status_path = Path(status_path)
@@ -34,6 +35,7 @@ class ZcanBridgeManager(object):
self.heartbeat_stale_s = float(heartbeat_stale_s)
self.poll_s = float(poll_s)
self.retry_cooldown_s = float(retry_cooldown_s)
self.busy_heartbeat_grace_s = float(busy_heartbeat_grace_s)
self.required_version = required_version
self.lock = threading.RLock()
self.stop_event = threading.Event()
@@ -79,6 +81,7 @@ class ZcanBridgeManager(object):
"last_attempt_epoch": self.last_attempt_epoch,
"last_online_epoch": self.last_online_epoch,
"heartbeat_age_s": None if age is None else round(age, 2),
"busy_heartbeat_grace_s": self.busy_heartbeat_grace_s,
"zcan_window_found": self.zcan_window_found,
"retry_armed": self.retry_requested,
"zcan_exe": str(self.zcanpro_exe),
@@ -120,6 +123,20 @@ class ZcanBridgeManager(object):
return False
return True
def _bridge_busy_within_grace(self):
try:
import json
with self.status_path.open("r", encoding="utf-8") as stream:
status = json.load(stream)
heartbeat = float(status.get("heartbeat_epoch", 0) or 0)
age = max(0.0, time.time() - heartbeat)
return bool(
status.get("current_request_id") and
status.get("bridge_state") == "RUNNING" and
heartbeat > 0 and age <= self.busy_heartbeat_grace_s)
except (OSError, ValueError, TypeError):
return False
def _take_retry_request(self):
with self.lock:
if not self.retry_requested:
@@ -130,15 +147,22 @@ class ZcanBridgeManager(object):
def _monitor(self):
while not self.stop_event.is_set():
if self._heartbeat_online():
with self.lock:
# A defensive ensure request is already satisfied by a
# healthy bridge. Do not carry it into a later live task.
self.retry_requested = False
self._set_state(
"ONLINE", "CAN桥接在线,由ZCANPRO扩展脚本持续提供数据",
last_online_epoch=time.time())
elif self._bridge_busy_within_grace():
self._set_state(
"BUSY", "CAN bridge request is active; restart is deferred")
else:
with self.lock:
# A bridge that was genuinely online gets one automatic
# recovery attempt. A failed recovery is not repeated
# until an explicit Start action calls ``ensure_async``.
if self.state == "ONLINE":
if self.state in ("ONLINE", "BUSY"):
self.retry_requested = True
if self._take_retry_request():
self._attempt_start()
@@ -0,0 +1,254 @@
"""Execute normal seat command plans through the shared ZCANPRO bridge.
ZLG's Windows driver gives one process exclusive ownership of a USBCAN device.
Self-repair already keeps that ownership inside ZCANPRO, so opening the same
adapter again from the web server makes both user and admin orchestration fail.
This executor keeps one owner and exchanges bounded, armed JSON requests with
the bridge instead.
"""
from __future__ import annotations
from datetime import UTC, datetime
import json
import os
from pathlib import Path
import time
from typing import Any
import uuid
def _now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _read_json(path: Path) -> dict[str, Any] | None:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError):
return None
return value if isinstance(value, dict) else None
def _atomic_json(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
temporary = path.with_name(
f".{path.name}.{os.getpid()}.{time.time_ns()}.tmp"
)
temporary.write_text(
json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8"
)
os.replace(temporary, path)
class ZcanBridgeSeatExecutor:
"""Hardware-seat executor compatible with ``HardwareSeatExecutor``."""
def __init__(
self,
matrix: Any,
bridge_manager: Any,
status_path: Path,
pending_dir: Path,
*,
required_version: str,
auto_home: bool,
startup_timeout_s: float = 12.0,
request_timeout_s: float = 660.0,
) -> None:
self.matrix = matrix
self.bridge_manager = bridge_manager
self.status_path = Path(status_path)
self.pending_dir = Path(pending_dir)
self.required_version = required_version
self.auto_home = bool(auto_home)
self.startup_timeout_s = float(startup_timeout_s)
self.request_timeout_s = float(request_timeout_s)
def _online_status(self, *, require_protocol: bool) -> dict[str, Any] | None:
value = _read_json(self.status_path)
if value is None or value.get("bridge_version") != self.required_version:
return None
try:
fresh = time.time() - float(value.get("heartbeat_epoch", 0)) < 3.0
except (TypeError, ValueError):
fresh = False
if not fresh:
return None
if require_protocol and not (value.get("system") and value.get("frame_epochs")):
return None
return value
def _wait_online(self, *, require_protocol: bool) -> dict[str, Any]:
self.bridge_manager.ensure_async()
deadline = time.time() + self.startup_timeout_s
while time.time() < deadline:
value = self._online_status(require_protocol=require_protocol)
if value is not None:
return value
time.sleep(0.1)
qualifier = "及座椅ECU协议" if require_protocol else ""
raise RuntimeError(f"ZCANPRO桥接{qualifier}未在线")
def _payload(
self,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
commands = []
for command in command_plan["spec"]["commands"]:
encoded = self.matrix.encode(command["message"], command["signals"])
item = dict(command)
item["frame_id"] = int(encoded["frame_id"], 16)
item["data"] = list(bytes.fromhex(encoded["data_hex"]))
commands.append(item)
return {
"name": command_plan.get("metadata", {}).get(
"name", "自然语言座椅控制"
),
"commands": commands,
"operations": list(function_config["spec"]["operations"]),
"allow_auto_home": self.auto_home,
}
def _submit(self, payload: dict[str, Any]) -> dict[str, Any]:
status = self._wait_online(require_protocol=True)
idle_deadline = time.time() + self.startup_timeout_s
while status.get("current_request_id") and time.time() < idle_deadline:
time.sleep(0.1)
status = self._wait_online(require_protocol=True)
if status.get("current_request_id"):
raise RuntimeError(
"ZCANPRO桥接正在执行其他台架任务:%s" %
status.get("current_request_id")
)
request_id = "SEAT_ORCH_%d_%s" % (
int(time.time() * 1000), uuid.uuid4().hex[:6]
)
request = {
"schema": "seat-function-request-v1",
"request_id": request_id,
"created_epoch": time.time(),
"armed_until_epoch": time.time() + self.request_timeout_s + 30.0,
"source": "cabin_cognitive_orchestrator",
"action": "execute_command_plan",
"payload": payload,
}
_atomic_json(self.pending_dir / f"{request_id}.json", request)
deadline = time.time() + self.request_timeout_s
while time.time() < deadline:
status = self._online_status(require_protocol=False)
if status is None:
time.sleep(0.1)
continue
result = status.get("last_result") or {}
if result.get("request_id") == request_id:
return result
time.sleep(0.1)
raise RuntimeError("ZCANPRO桥接执行座椅请求超时,已停止等待")
@staticmethod
def _report(
function_config: dict[str, Any],
started_at: str,
bridge_result: dict[str, Any] | None,
error: Exception | None,
) -> dict[str, Any]:
request_id = function_config["metadata"]["request_id"]
passed = error is None and (bridge_result or {}).get("result") == "PASS"
detail = (bridge_result or {}).get("details") or {}
message = (
detail.get("summary")
or (bridge_result or {}).get("summary")
or (str(error) if error is not None else "ZCANPRO桥接未返回通过结果")
)
results = [
{
"ref": operation["id"],
"domain": "seat",
"status": "succeeded" if passed else "failed",
"message": message,
"telemetry": {
"transport": "zcanpro_shared_bridge",
"hardware_feedback": passed,
"simulated": False,
"bridge_result": bridge_result,
"error_type": type(error).__name__ if error is not None else None,
"stop_all_attempted": bool(detail.get("stop_all_attempted")),
},
}
for operation in function_config["spec"]["operations"]
]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": f"report-{request_id}",
"name": "T2G seat ZCANPRO bridge execution report",
"version": "1.0.0",
"request_id": request_id,
"labels": {"simulation": "false", "feedback": "physical_can"},
},
"spec": {
"mode": "hil",
"status": "succeeded" if passed else "failed",
"started_at": started_at,
"finished_at": _now(),
"results": results,
},
}
def execute(
self,
command_plan: dict[str, Any],
function_config: dict[str, Any],
actuator_state: dict[str, Any] | None = None,
) -> dict[str, Any]:
del actuator_state
started_at = _now()
try:
result = self._submit(self._payload(command_plan, function_config))
except Exception as exc: # normalize bridge/startup failures for the API
return self._report(function_config, started_at, None, exc)
return self._report(function_config, started_at, result, None)
def read_physical_state(self) -> dict[str, Any]:
status = self._wait_online(require_protocol=True)
axes = ("longitudinal", "cushion_tilt", "backrest_recline", "legrest_height")
positions: dict[str, Any] = {}
missing = []
motors = status.get("motors") or []
for motor, axis in enumerate(axes):
item = motors[motor] if motor < len(motors) else None
if not isinstance(item, dict):
missing.append(axis)
continue
positions[axis] = {
"position_pct": round(int(item.get("position", 0)) / 100),
"position_valid": bool(item.get("position_valid")),
"motion_state": "moving" if item.get("active") else "stopped",
}
thermal = status.get("thermal") or []
thermal_byte = int(thermal[0]) if thermal else 0
ventilation_raw = thermal_byte & 0x07
heating_raw = (thermal_byte >> 3) & 0x07
return {
"position": positions,
"ventilation": {
"state": "off" if ventilation_raw <= 1 else "on",
"level": max(0, ventilation_raw - 1),
},
"heating": {
"state": "off" if heating_raw <= 1 else "on",
"level": max(0, heating_raw - 1),
},
"captured_at": _now(),
"source": "physical_can",
"target": {
"device": "seat", "zone": "front_right", "instance": "passenger"
},
"complete": not missing,
"missing": missing,
}
@@ -504,7 +504,11 @@ class VehicleCanSettings:
repository_root=repository_root,
enabled=_env_bool(
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
default=_env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED"),
# The seat bench and the vehicle CAN harness are independent
# pieces of hardware. Enabling physical seat execution must
# not silently open/transmit on the unconnected vehicle
# channel; dual-channel execution is an explicit opt-in.
default=False,
),
approval_required=_env_bool(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED", default=True
@@ -7,16 +7,17 @@ import subprocess
import time
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Callable
from typing import TYPE_CHECKING, Any, Callable
from cognitive_agent.infrastructure.can.gateway import CanBus
if TYPE_CHECKING:
from cognitive_agent.infrastructure.can.gateway import CanBus
FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-dynamic-modes-v13"
FIRMWARE_PROTOCOL_FRAME_IDS = frozenset(
{0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x4FA}
)
EXPECTED_FIRMWARE_REVISION = 13
EXPECTED_FIRMWARE_REVISION = 15
EXPECTED_MOTOR_MAX_HALL = (480, 374, 917, 647)
+8 -2
View File
@@ -115,7 +115,7 @@ class SeatWebApiTests(unittest.TestCase):
def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-43", timeout=3) as response:
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-44", timeout=3) as response:
app_script = response.read().decode("utf-8")
with self.opener.open(
f"{self.base_url}/gesture-control.js?v=android-pad-6", timeout=3
@@ -150,7 +150,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn("整车孪生界面", html)
self.assertIn("<strong>阅读灯</strong>", html)
self.assertNotIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=seat-hil-43"', html)
self.assertIn('src="/app.js?v=seat-hil-44"', html)
self.assertIn('src="/gesture-control.js?v=android-pad-6"', html)
self.assertNotIn('class="vehicle-system-hud"', html)
self.assertIn('src="/console-v2.js?v=desktop-refactor-28"', html)
@@ -229,6 +229,12 @@ class SeatWebApiTests(unittest.TestCase):
self.assertNotIn('id="releaseHardwareAccess"', html)
self.assertIn('const dynamicModeOrder = ["rocking_horse", "dynamic_cradle"]', app_script)
self.assertIn('divider.className = "mode-group-divider"', app_script)
self.assertIn(
'mode === "hardware" && !state.runtime?.hardware_execution_allowed',
app_script,
)
self.assertIn("hardwareButton.disabled = !hardwareExecutionReady", app_script)
self.assertIn("车端 CAN 未启用,其他领域保持虚拟执行", app_script)
self.assertIn("function extractReferenceWindowPanes", app_script)
self.assertIn("function collectReferenceFrontLampMeshes", app_script)
self.assertIn("function emphasizeReferenceWindowPanes", app_script)
+27 -6
View File
@@ -5,6 +5,7 @@ import threading
import urllib.error
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
import pytest
@@ -134,15 +135,15 @@ class TestSelfRepairApi:
with urllib.request.urlopen(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8")
with urllib.request.urlopen(
f"{self.base_url}/self-repair.js?v=repair-15", timeout=3
f"{self.base_url}/self-repair.js?v=repair-17", timeout=3
) as response:
script = response.read().decode("utf-8")
with urllib.request.urlopen(
f"{self.base_url}/self-repair.css?v=repair-15", timeout=3
f"{self.base_url}/self-repair.css?v=repair-17", timeout=3
) as response:
styles = response.read().decode("utf-8")
with urllib.request.urlopen(
f"{self.base_url}/self-repair-seat3d.js?v=repair-15", timeout=3
f"{self.base_url}/self-repair-seat3d.js?v=repair-17", timeout=3
) as response:
seat3d = response.read().decode("utf-8")
with urllib.request.urlopen(
@@ -150,10 +151,15 @@ class TestSelfRepairApi:
) as response:
console_script = response.read().decode("utf-8")
assert 'href="/self-repair.css?v=repair-15"' in html
assert 'src="/self-repair.js?v=repair-15"' in html
assert 'src="/self-repair-seat3d.js?v=repair-15"' in html
assert 'href="/self-repair.css?v=repair-17"' in html
assert 'src="/self-repair.js?v=repair-17"' in html
assert 'src="/self-repair-seat3d.js?v=repair-17"' in html
assert "量产故障用例" in script
assert 'id="repairConfirmDialog"' in script
assert "confirmAction(" in script
assert "window.confirm" not in script
assert 'hardware.link?.adapter === "OK"' in script
assert 'PASSED: ["21针通过", "online"]' in script
assert "connectorRows" in script
assert "Polling refreshes data only" in script
assert "renderRecords();\n showView(activeView);" not in script
@@ -172,3 +178,18 @@ class TestSelfRepairApi:
assert 'window.addEventListener("self-repair-seat-reset"' in seat3d
assert '["self-calibration", "self-repair"].includes(module)' in console_script
assert 'roleSelect.disabled = ui.activeModule === "self-repair"' in console_script
def test_self_repair_bridge_uses_the_backend_runtime_directory() -> None:
source = (
Path(__file__).resolve().parents[2]
/ "tools"
/ "zcan"
/ "seat_self_repair_bridge.py"
).read_text(encoding="utf-8")
assert '"artifacts", "tmp", "self-repair", "runtime"' in source
assert 'os.path.join(BASE_DIR, "seat_function_runtime")' not in source
assert "A silent or disconnected CAN bus must not kill" in source
assert 'self.message("GUI STOP_ALL发送失败' in source
assert 'self.message("GUI thermal off发送失败' in source
+5 -1
View File
@@ -353,7 +353,11 @@
]},
{"mode_id": "zero_gravity", "expected": {"longitudinal": 100, "cushion_tilt": 100, "backrest_recline": 81, "legrest_height": 94}, "variants": [
{"id": "MODE-ZERO-GRAVITY-OPEN", "utterance": "打开零重力模式"}, {"id": "MODE-ZERO-GRAVITY-ENTER", "utterance": "副驾进入零重力"}, {"id": "MODE-ZERO-GRAVITY-ENGLISH", "utterance": "开启 zero-gravity 模式"}
]}
]},
{"id": "MODE-ROCKING-HORSE-START", "mode_id": "rocking_horse", "utterance": "开启木马模式", "expected": {}, "expected_dynamic_mode": {"mode_id": 1, "state": "running"}},
{"id": "MODE-ROCKING-HORSE-STOP", "utterance": "关闭木马模式", "expected": {}, "expected_dynamic_mode": {"mode_id": 0, "state": "idle"}},
{"id": "MODE-DYNAMIC-CRADLE-START", "mode_id": "dynamic_cradle", "utterance": "开启动态摇篮模式", "expected": {}, "expected_dynamic_mode": {"mode_id": 2, "state": "running"}},
{"id": "MODE-DYNAMIC-CRADLE-STOP", "utterance": "关闭动态摇篮模式", "expected": {}, "expected_dynamic_mode": {"mode_id": 0, "state": "idle"}}
],
"model_cases": [
{
@@ -66,7 +66,11 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
{"thermal", "motion", "combined", "mode"},
)
self.assertEqual(
{case["mode_id"] for case in self.positive_cases if case["group"] == "mode"},
{
case["mode_id"]
for case in self.positive_cases
if case["group"] == "mode" and case.get("mode_id")
},
{
"easy_entry_exit",
"office",
@@ -79,6 +83,8 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
"electric_beach_chair",
"nap",
"zero_gravity",
"rocking_horse",
"dynamic_cradle",
},
)
@@ -150,6 +156,19 @@ class SeatFrontendInputCoverageTests(unittest.TestCase):
for field, expected in expected_motion.items():
if field != "axis":
self.assertEqual(operation["motion"][field], expected)
expected_dynamic_mode = case.get("expected_dynamic_mode")
if expected_dynamic_mode:
capability = (
"seat.stop_dynamic_mode"
if expected_dynamic_mode["mode_id"] == 0
else "seat.start_dynamic_mode"
)
self.assertIn(capability, by_capability)
if expected_dynamic_mode["mode_id"]:
self.assertEqual(
by_capability[capability]["parameters"]["mode_id"],
expected_dynamic_mode["mode_id"],
)
def test_every_rejection_utterance_stops_before_execution(self) -> None:
for case in self.rejection_cases:
+12
View File
@@ -1,5 +1,6 @@
from __future__ import annotations
import os
import unittest
from pathlib import Path
from unittest.mock import patch
@@ -162,6 +163,17 @@ class NativeCanIntegrationTests(unittest.TestCase):
self.assertEqual(info["data"]["bitrate"], 2_000_000)
self.assertEqual(info["data"]["sample_point_percent"], 80)
def test_enabling_seat_hardware_does_not_implicitly_enable_vehicle_can(self) -> None:
with patch.dict(
os.environ,
{"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED": "true"},
clear=False,
):
os.environ.pop("COGNITIVE_AGENT_VEHICLE_CAN_ENABLED", None)
settings = VehicleCanSettings.from_env(Path(REPOSITORY_ROOT))
self.assertFalse(settings.enabled)
if __name__ == "__main__":
unittest.main()
+23
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import json
import time
from pathlib import Path
from cognitive_agent.application.self_repair.healing_model import (
@@ -81,3 +83,24 @@ def test_bridge_manager_does_not_arm_visible_tooling_by_default(tmp_path: Path)
assert snapshot["state"] == "IDLE"
assert snapshot["retry_armed"] is False
assert snapshot["attempts"] == 0
def test_bridge_manager_does_not_restart_a_live_busy_request(tmp_path: Path) -> None:
status_path = tmp_path / "bridge.json"
status_path.write_text(
json.dumps({
"heartbeat_epoch": time.time() - 5.0,
"bridge_state": "RUNNING",
"current_request_id": "SELF_HEAL_BENCH_1",
}),
encoding="utf-8",
)
manager = ZcanBridgeManager(
status_path,
tmp_path / "bridge.py",
heartbeat_stale_s=3.0,
busy_heartbeat_grace_s=45.0,
)
assert manager._heartbeat_online() is False
assert manager._bridge_busy_within_grace() is True
+55
View File
@@ -106,6 +106,7 @@ def expected_operation_errors(
result: dict[str, Any],
expected: dict[str, int],
expected_motion: dict[str, Any] | None = None,
expected_dynamic_mode: dict[str, Any] | None = None,
) -> list[str]:
errors: list[str] = []
seat_config = (result.get("function_configs") or {}).get("seat") or result.get(
@@ -151,6 +152,47 @@ def expected_operation_errors(
f"{capability} motion.{field}={actual_value}, "
f"expected={expected_value}"
)
if expected_dynamic_mode:
expected_mode_id = int(expected_dynamic_mode["mode_id"])
capability = (
"seat.stop_dynamic_mode"
if expected_mode_id == 0
else "seat.start_dynamic_mode"
)
operation = by_capability.get(capability)
if operation is None:
errors.append(f"expected capability was not generated: {capability}")
elif expected_mode_id and dig(operation, "parameters", "mode_id") != expected_mode_id:
errors.append(
f"{capability} mode_id={dig(operation, 'parameters', 'mode_id')}, "
f"expected={expected_mode_id}"
)
hardware_results = dig(result, "execution_report", "spec", "results") or []
telemetry = next(
(
item.get("telemetry", {})
for item in hardware_results
if item.get("domain") == "seat"
and item.get("telemetry", {}).get("dynamic_mode_id") is not None
),
None,
)
if telemetry is None:
errors.append("dynamic-mode execution did not include physical 0x306 telemetry")
else:
if telemetry.get("dynamic_mode_id") != expected_mode_id:
errors.append(
f"dynamic telemetry mode_id={telemetry.get('dynamic_mode_id')}, "
f"expected={expected_mode_id}"
)
dynamic_state = telemetry.get("dynamic_state")
accepted_states = {0} if expected_mode_id == 0 else {1, 2}
if dynamic_state not in accepted_states:
errors.append(
f"dynamic telemetry state={dynamic_state}, "
f"expected one of {sorted(accepted_states)}"
)
return errors
@@ -306,6 +348,7 @@ def run_case(
result,
case.get("expected", {}),
case.get("expected_motion"),
case.get("expected_dynamic_mode"),
)
)
seat_config = (result.get("function_configs") or {}).get(
@@ -359,6 +402,18 @@ def run_case(
record["errors"].extend(
validate_physical_state(state, case.get("expected", {}), tolerance)
)
expected_dynamic_mode = case.get("expected_dynamic_mode")
if expected_dynamic_mode:
actual_dynamic_mode = dig(
state, "actuator_state", "passenger", "dynamic_mode"
) or {}
for field in ("mode_id", "state"):
expected_value = expected_dynamic_mode[field]
if actual_dynamic_mode.get(field) != expected_value:
record["errors"].append(
f"dynamic_mode.{field}={actual_dynamic_mode.get(field)}, "
f"expected={expected_value}"
)
record["physical_state"] = state.get("physical_state")
record["status"] = "passed" if not record["errors"] else "failed"
except Exception as exc: # continue the suite after transport and parse errors
+592 -33
View File
@@ -1,7 +1,7 @@
"""ZCANPRO bridge for the standalone selectable seat-function GUI.
"""ZCANPRO bridge for the web-console self-repair hardware workflow.
Run this file in ZCANPRO's extension-script window and leave it running. The
normal Windows GUI writes armed JSON requests into ``seat_function_runtime``;
web console writes armed JSON requests into the repository self-repair runtime;
this bridge is the only process that imports and calls ``zcanpro``.
"""
@@ -18,6 +18,19 @@ import time
import zcanpro
# ZCANPRO executes extension scripts without adding the script directory or
# the firmware HIL helpers to ``sys.path``. A fresh ZCANPRO process therefore
# could only start this bridge when another script happened to have imported
# ``hil_common`` first. Resolve the repository helper explicitly so bridge
# startup is deterministic after a reboot.
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REPOSITORY_ROOT = os.path.dirname(os.path.dirname(BASE_DIR))
HIL_COMMON_DIR = os.path.join(
REPOSITORY_ROOT, "firmware", "t2g", "Seat_Controller_demo",
"Tests", "manual_hil")
if HIL_COMMON_DIR not in sys.path:
sys.path.insert(0, HIL_COMMON_DIR)
if "hil_common" in sys.modules:
_hil_common = importlib.reload(sys.modules["hil_common"])
else:
@@ -31,15 +44,15 @@ parse_303 = _hil_common.parse_303
parse_304 = _hil_common.parse_304
parse_305 = _hil_common.parse_305
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RUNTIME_DIR = os.path.join(BASE_DIR, "seat_function_runtime")
RUNTIME_DIR = os.path.join(
REPOSITORY_ROOT, "artifacts", "tmp", "self-repair", "runtime")
PENDING_DIR = os.path.join(RUNTIME_DIR, "requests", "pending")
PROCESSING_DIR = os.path.join(RUNTIME_DIR, "requests", "processing")
COMPLETED_DIR = os.path.join(RUNTIME_DIR, "requests", "completed")
STATUS_PATH = os.path.join(RUNTIME_DIR, "bridge_status.json")
EMERGENCY_PATH = os.path.join(RUNTIME_DIR, "emergency_stop.json")
LOG_ROOT = os.path.join(BASE_DIR, "logs", "05_seat_function_gui")
LOG_ROOT = os.path.join(
REPOSITORY_ROOT, "artifacts", "self-repair", "hardware")
CONFIRMED_MAX_HALL = (480, 374, 917, 647)
POSITION_TIMEOUT_MS = 30000
NORMAL_TASK_MIN_OBSERVE_S = 5.0
@@ -81,7 +94,14 @@ SELF_HEAL_HOOK_CASES = {
"normal_task": "用户调整腿托后调用刚保存的舒适位置",
"fault_reaction": "CRC校验失败,腿托调用目标被挂起",
"resume_reaction": "重建双槽后继续执行原腿托舒适位置",
"demo_motor": 3, "demo_delta": 2200, "prelude_delta": 2800,
# Keep both physical legs inside the connected seat's repeatedly
# observed M3 travel window. The CRC scenario needs a real saved move
# and a later restored move; it does not require a long-range pose.
# Save one reachable short-travel comfort pose, retreat from it, then
# use the saved pose itself as the post-repair recall target. This is
# both semantically accurate and independent of the connected seat's
# much shorter M3 internal-limit window.
"demo_motor": 3, "demo_delta": 200, "prelude_delta": 200,
"normal_observe_s": NORMAL_TASK_MIN_OBSERVE_S,
"inject": "向真实SMV8备用槽写入更新序号但CRC损坏的记录",
"repair": "保留旧有效槽并以更高序号重新原子提交SMV8冗余记录",
@@ -142,7 +162,7 @@ HOME_LEG_TIMEOUT_S = 120
SIGNAL_WINDOW_S = 2.5
HEATER_CURRENT_ACTIVE_DELTA_RAW = 500
FAN_CURRENT_ACTIVE_DELTA_RAW = 40
BRIDGE_VERSION = "2026-08-07-reset-home-far-target-v15"
BRIDGE_VERSION = "2026-08-09-real-self-heal-v32"
HARDWARE_MOTOR_PINS = {
0: ("JA1-4", "JA1-5", "JC1-9"),
1: ("JB1-6", "JB1-8", "JC1-15"),
@@ -177,9 +197,18 @@ def _atomic_json(path, value):
for attempt in range(8):
try:
os.replace(temporary, path)
return
return True
except PermissionError:
if attempt >= 7:
# The status file is a best-effort telemetry snapshot.
# Windows readers may briefly deny FILE_SHARE_DELETE;
# dropping one snapshot is safe because the 10 ms bridge
# loop republishes the retained state (and last_result).
# Raising here would terminate the live ZCAN bridge and
# lose the in-flight request result.
if os.path.normcase(os.path.abspath(path)) == \
os.path.normcase(os.path.abspath(STATUS_PATH)):
return False
raise
# The web console polls this file frequently. Windows can
# briefly deny replacement while a reader is closing it.
@@ -189,6 +218,7 @@ def _atomic_json(path, value):
os.remove(temporary)
except OSError:
pass
return False
def _read_json(path):
@@ -287,6 +317,7 @@ class SeatFunctionBridge(object):
self.latest_currents = [None, None, None, None]
self.latest_thermal_current = None
self.latest_thermal = None
self.latest_dynamic = None
self.frame_epochs = {}
self.motor_current_history = [[], [], [], []]
self.motor_hall_history = [[], [], [], []]
@@ -390,6 +421,7 @@ class SeatFunctionBridge(object):
"currents": self.latest_currents,
"thermal_current": self.latest_thermal_current,
"thermal": self.latest_thermal,
"dynamic": self.latest_dynamic,
"frame_epochs": self.frame_epochs,
"signal_window": self.signal_window(),
"last_result": self.last_result,
@@ -459,6 +491,16 @@ class SeatFunctionBridge(object):
"raw": value["heater_raw"],
"valid": value["heater_sample_valid"],
}, received_epoch)
elif can_id == 0x306 and len(data) >= 8:
self.latest_dynamic = {
"mode_id": data[0],
"state": data[1],
"phase": data[2],
"active_mask": data[3] & 0x0F,
"fault_mask": (data[3] >> 4) & 0x0F,
"cycle_index": data[4] | (data[5] << 8),
"phase_elapsed_ms": data[6] | (data[7] << 8),
}
elif can_id == 0x4FA and len(data) >= 1:
self.latest_thermal = data
output.append((can_id, data))
@@ -484,11 +526,21 @@ class SeatFunctionBridge(object):
return False
self.message("执行GUI紧急STOP_ALL %s" % request_id)
sequence = self.next_sequence()
self.transmit(0x300, command_payload(0x09, 0, 0, sequence),
"GUI EMERGENCY STOP_ALL")
# A silent or disconnected CAN bus must not kill the long-running
# bridge. The request still interrupts any active workflow, while
# each shutdown frame is attempted independently so observation can
# resume after a firmware flash or a temporary bus interruption.
try:
self.transmit(0x300, command_payload(0x09, 0, 0, sequence),
"GUI EMERGENCY STOP_ALL")
except Exception as error:
self.message("GUI STOP_ALL发送失败:%r" % error)
if request.get("thermal_off", True):
self.transmit(0x42A, [0x0A, 0, 0, 0, 0, 0, 0, 0],
"GUI emergency thermal off")
try:
self.transmit(0x42A, [0x0A, 0, 0, 0, 0, 0, 0, 0],
"GUI emergency thermal off")
except Exception as error:
self.message("GUI thermal off发送失败:%r" % error)
return True
def wait(self, predicate, timeout_s, allow_emergency=True):
@@ -550,11 +602,12 @@ class SeatFunctionBridge(object):
current = int(status.get("position", 0))
if not status.get("position_valid"):
return None
magnitude = max(600, min(2800, int(delta)))
magnitude = max(100, min(2800, int(delta)))
target = current + magnitude if current <= 5000 else current - magnitude
target = max(800, min(9200, target))
if abs(target - current) < 500:
target = 8000 if current < 5000 else 2000
target = max(100, min(9200, target))
if abs(target - current) < 100:
target = min(9200, current + 100) if current < 5000 else \
max(100, current - 100)
return {"motor": int(motor), "start": current, "target": target,
"sequence": None, "ack": None}
@@ -789,6 +842,7 @@ class SeatFunctionBridge(object):
# signal only when each direction produces Hall movement at least once.
# This remains sensitive to a disconnected/intermittent pin, for which
# one direction (or both) never produces a valid edge in either cycle.
hall_direction_ok = {
direction: any(
item["direction"] == direction and
@@ -816,6 +870,75 @@ class SeatFunctionBridge(object):
"hall_ok": hall_ok,
"hall_direction_ok": hall_direction_ok, "runs": runs}
def _depart_legrest_endpoint_for_home(self):
"""Move M3 off its hard stop with a bounded adaptive timed profile.
Absolute homing cannot prove an endpoint when the loaded legrest is
statically wedged and its first Hall edge never arrives. The active
pin diagnostic normally releases this bench with a bounded near/far
pair at 60% PWM. Loaded repetitions can need additional breakaway
torque, so retry at 75% and finally the ECU's normal 90% duty. Every
leg remains limited to 900 ms and is followed by STOP; multiple real
Hall edges are still required before proceeding.
"""
motor = 3
attempts = []
initial = self.latest_diagnostics[motor]
initial_hall = None if initial is None else int(initial["hall"])
for attempt, duty_x10 in enumerate((600, 750, 900), start=1):
for direction in (2, 1):
before = self.latest_diagnostics[motor]
before_hall = None if before is None else int(before["hall"])
sequence = self.next_sequence()
state, response = self.send_command(
command_payload(0x03, motor, direction, sequence,
900, duty_x10),
1, 3.0,
"M3 HOME breakaway attempt=%d direction=%d" %
(attempt, direction))
if state == "EMERGENCY":
return False, {
"summary": "M3端点脱离期间收到紧急停止",
"attempts": attempts,
}
self.capture(1.45)
stop_sequence = self.next_sequence()
self.send_command(
command_payload(0x08, motor, 0, stop_sequence),
1, 2.0, "M3 HOME breakaway bounded stop")
self.capture(0.35)
after = self.latest_diagnostics[motor]
after_hall = None if after is None else int(after["hall"])
hall_delta = None if before_hall is None or \
after_hall is None else abs(after_hall - before_hall)
cumulative_hall_delta = None if initial_hall is None or \
after_hall is None else abs(after_hall - initial_hall)
accepted = (state == "OK" and response is not None and
int(response.get("reject", 1)) == 0)
attempts.append({
"attempt": attempt,
"direction": direction,
"duty_percent": duty_x10 // 10,
"accepted": accepted,
"hall_before": before_hall,
"hall_after": after_hall,
"hall_delta": hall_delta,
"cumulative_hall_delta": cumulative_hall_delta,
})
if(direction == 1 and accepted and
((hall_delta is not None and hall_delta >= 3) or
(cumulative_hall_delta is not None and
cumulative_hall_delta >= 2))):
return True, {
"summary": "M3已用%d%%受控近/远端序列脱离硬端点" %
(duty_x10 // 10),
"attempts": attempts,
}
return False, {
"summary": "M3受控脱离重试后仍无真实Hall边沿",
"attempts": attempts,
}
def execute_hardware_diagnostic(self, request):
request_id = request.get("request_id")
pin_results = {
@@ -884,7 +1007,12 @@ class SeatFunctionBridge(object):
self.transmit(0x42A, [0x0A, 0, 0, 0, 0, 0, 0, 0],
"HW fan safe off")
# Heater branch: low level only, bounded to three seconds.
# Heater branch: use high level for a short, bounded interval.
# The heater PWM is 100 Hz while 0x305 is published every 100 ms;
# at low duty those periods can stay phase-aligned on the OFF
# portion and hide a healthy load. High duty breaks that sampling
# blind spot while the three-second bound keeps the diagnostic
# thermally safe.
self.capture(0.5)
heater_baseline = int((self.latest_thermal_current or {}).get(
"heater_raw", 0))
@@ -893,8 +1021,8 @@ class SeatFunctionBridge(object):
"HW heater mode")
self.transmit(0x2AA, [0x01, 0, 0x1B, 0, 0, 0, 0, 0],
"HW heater config")
self.transmit(0x29E, [0x12, 0, 0x10, 0, 0, 0, 0, 0],
"HW heater level 1")
self.transmit(0x29E, [0x12, 0, 0x30, 0, 0, 0, 0, 0],
"HW heater level 3 diagnostic pulse")
self.capture(3.0)
heater_samples = [int(sample.get("raw", 0))
for _, sample in self.heater_current_history
@@ -1005,6 +1133,283 @@ class SeatFunctionBridge(object):
expected_position))
return issues
def execute_command_plan(self, request):
"""Execute a compiler-produced normal seat plan with this CAN owner.
The web server has already validated the FunctionConfig and CommandPlan.
This boundary validates the small physical allow-list again, replaces
placeholder motion sequences, and requires ECU acknowledgement plus
the matching physical feedback before reporting PASS.
"""
payload = request.get("payload") or {}
commands = payload.get("commands") or []
operations = payload.get("operations") or []
operation_by_id = dict((item.get("id"), item) for item in operations)
if not commands or not operations:
return "BLOCKED", {"summary": "座椅命令计划为空"}
allowed_messages = {
"T2G_Seat_Move_Command_300": 0x300,
"T2G_Seat_Thermal_Control_29E": 0x29E,
"T2G_Seat_Thermal_Mode_3CE": 0x3CE,
}
for command in commands:
message = command.get("message")
if message not in allowed_messages or \
int(command.get("frame_id", -1)) != allowed_messages[message]:
return "BLOCKED", {
"summary": "桥接拒绝未授权CAN消息 %s" % message,
}
data = command.get("data") or []
if len(data) != 8 or any(int(value) < 0 or int(value) > 255
for value in data):
return "BLOCKED", {"summary": "%s的CAN数据无效" % message}
refs = str(command.get("operation_ref", "")).split(",")
if not refs or any(ref not in operation_by_id for ref in refs):
return "BLOCKED", {"summary": "%s引用了未知操作" % message}
self.capture(0.8)
if self.latest_system is None:
return "BLOCKED", {"summary": "未收到0x301系统反馈"}
motion_commands = [
command for command in commands
if command.get("message") == "T2G_Seat_Move_Command_300" and
int((command.get("signals") or {}).get("Opcode", -1)) not in (0x09, 0x0F)
]
if motion_commands and not self.stop_all_sync():
return "FAIL", {
"summary": "自然语言座椅控制前的STOP_ALL无有效ACK",
"stop_all_attempted": True,
}
results = []
for command in commands:
if self.check_emergency():
return "STOPPED", {
"summary": "座椅控制过程中收到紧急停止",
"results": results,
"stop_all_attempted": bool(motion_commands),
}
message = command["message"]
refs = str(command["operation_ref"]).split(",")
referenced = [operation_by_id[ref] for ref in refs]
signals = command.get("signals") or {}
if message == "T2G_Seat_Thermal_Mode_3CE":
data = [int(value) & 0xFF for value in command["data"]]
if (data[0] & ~0x30) or any(data[1:]):
return "BLOCKED", {"summary": "3CE包含未授权位"}
self.transmit(0x3CE, data, command.get("id", "thermal mode"))
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "热管理手动模式已下发",
})
continue
if message == "T2G_Seat_Thermal_Control_29E":
data = [int(value) & 0xFF for value in command["data"]]
if (data[0] & ~0x3F) or any(data[index] for index in (1, 3, 4, 5, 6, 7)):
return "BLOCKED", {"summary": "29E包含未授权位"}
expected = {}
for operation in referenced:
capability = operation.get("capability")
state = (operation.get("parameters") or {}).get("state")
level = int((operation.get("parameters") or {}).get("level", 1))
raw = 1 if state == "off" else min(level, 3) + 1
if capability == "seat.set_ventilation":
expected["ventilation"] = raw
elif capability == "seat.set_heating":
expected["heating"] = raw
else:
return "BLOCKED", {
"summary": "29E引用了非热管理操作 %s" % capability,
}
self.transmit(0x29E, data, command.get("id", "thermal control"))
def thermal_predicate(can_id, frame_data):
if can_id != 0x4FA or len(frame_data) < 1:
return None
ventilation = frame_data[0] & 0x07
heating = (frame_data[0] >> 3) & 0x07
if ("ventilation" in expected and
ventilation != expected["ventilation"]):
return None
if "heating" in expected and heating != expected["heating"]:
return None
return list(frame_data)
wait_state, feedback = self.wait(
thermal_predicate,
max(1.0, float((command.get("completion") or {}).get(
"timeout_ms", 3000)) / 1000.0))
if wait_state != "OK" or feedback is None:
return "FAIL", {
"summary": "0x4FA未确认热管理目标",
"results": results,
"expected": expected,
}
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "0x4FA已确认热管理目标", "feedback": feedback,
})
continue
opcode = int(signals.get("Opcode", -1))
motor = int(signals.get("Motor_Selector", -1))
direction = int(signals.get("Direction", 0))
param_a = int(signals.get("Param_A", 0))
param_b = int(signals.get("Param_B", 0))
if opcode not in (0x01, 0x03, 0x09, 0x0B, 0x0E, 0x0F) or \
motor not in range(4):
return "BLOCKED", {
"summary": "0x300操作码或电机号未授权:opcode=0x%02X motor=%d" %
(opcode, motor),
}
if opcode in (0x01, 0x03) and self.latest_system is not None and \
not (self.latest_system["position_valid_mask"] & (1 << motor)):
if not payload.get("allow_auto_home", False):
return "BLOCKED", {
"summary": "M%d位置无效且请求未授权自动归零" % motor,
}
home_result, home_details = self.execute_initialize_known_ranges({
"payload": {"force_all_axes": False},
})
if home_result != "PASS":
return home_result, {
"summary": "自然语言控制前自动归零失败:%s" %
home_details.get("summary", ""),
"auto_home": home_details,
}
completion = command.get("completion") or {}
target = int(completion.get("value", param_a))
tolerance = max(0, int(completion.get("tolerance", 100)))
current = self.latest_motors[motor] or {}
already_at_target = (
opcode in (0x01, 0x0B) and
bool(current.get("position_valid")) and
not bool(current.get("active")) and
abs(int(current.get("position", -100000)) - target) <= tolerance)
if opcode == 0x0B:
already_at_target = already_at_target and bool(current.get("near"))
if already_at_target:
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "M%d已由真实CAN确认处于目标位置,未重复驱动机械端点" % motor,
"terminal": current, "motion_skipped": True,
"skip_reason": "PHYSICAL_TARGET_ALREADY_CONFIRMED",
})
continue
sequence = self.next_sequence()
expected_ack = 4 if opcode == 0x09 else 1
send_state, acknowledgement = self.send_command(
command_payload(opcode, motor, direction, sequence,
param_a, param_b),
expected_ack, 2.5, command.get("id", "orchestration"))
if send_state == "EMERGENCY":
return "STOPPED", {"summary": "下发时收到紧急停止"}
if send_state != "OK" or acknowledgement is None or \
acknowledgement.get("reject") != 0:
self.stop_all_sync()
return "FAIL", {
"summary": "0x300命令未被ECU接受",
"command_id": command.get("id"),
"acknowledgement": acknowledgement,
"stop_all_attempted": True,
}
if opcode == 0x09:
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "STOP_ALL已确认",
"acknowledgement": acknowledgement,
})
continue
if opcode in (0x0E, 0x0F):
mode_id = 0 if opcode == 0x0F else param_a
accepted_states = set(
(command.get("completion") or {}).get("accepted_states", [1, 2]))
def dynamic_predicate(can_id, frame_data):
if can_id != 0x306 or len(frame_data) < 8:
return None
if opcode == 0x0F:
matched = frame_data[0] == 0 and frame_data[1] == 0
else:
matched = frame_data[0] == mode_id and \
frame_data[1] in accepted_states
return list(frame_data) if matched else None
dynamic_state, dynamic_feedback = self.wait(
dynamic_predicate,
max(1.0, float((command.get("completion") or {}).get(
"timeout_ms", 5000)) / 1000.0))
if dynamic_state != "OK" or dynamic_feedback is None:
self.stop_all_sync()
return "FAIL", {
"summary": "0x306未确认动态模式状态",
"mode_id": mode_id,
"stop_all_attempted": True,
}
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "0x301/0x306已确认动态模式",
"feedback": dynamic_feedback,
})
continue
timeout_s = max(
1.0, float((command.get("completion") or {}).get(
"timeout_ms", POSITION_TIMEOUT_MS)) / 1000.0)
def motor_predicate(can_id, frame_data):
if can_id != 0x302:
return None
value = parse_302(frame_data)
if value is None or value["motor"] != motor or \
value["sequence"] != sequence:
return None
if not value["active"] and value["task"] in (6, 7):
return value
return None
motor_state, terminal = self.wait(motor_predicate, timeout_s + 2.0)
passed = (motor_state == "OK" and terminal is not None and
terminal.get("task") == 6 and
terminal.get("result") in (2, 4))
if opcode in (0x01, 0x0B):
passed = (passed and terminal.get("position_valid") and
abs(int(terminal.get("position", -100000)) - target) <=
tolerance)
if not passed:
self.stop_all_sync()
return "FAIL", {
"summary": "M%d运动反馈未达到目标" % motor,
"command_id": command.get("id"),
"target": target, "tolerance": tolerance,
"terminal": terminal, "stop_all_attempted": True,
}
results.append({
"command_id": command.get("id"), "result": "PASS",
"summary": "M%d物理反馈已达到目标" % motor,
"terminal": terminal,
})
self.capture(0.3)
return "PASS", {
"summary": "ZCANPRO共享桥接已完成%d条座椅命令并验证实物反馈" %
len(commands),
"results": results,
"system": self.latest_system,
"motors": self.latest_motors,
"thermal": self.latest_thermal,
"dynamic": self.latest_dynamic,
"stop_all_attempted": bool(motion_commands),
}
def execute_mode(self, request):
payload = request.get("payload") or {}
axes = payload.get("axes") or []
@@ -1174,6 +1579,7 @@ class SeatFunctionBridge(object):
def execute_initialize_known_ranges(self, request):
payload = request.get("payload") or request
force_all_axes = bool(payload.get("force_all_axes", False))
home_nonzero_axes = bool(payload.get("home_nonzero_axes", False))
if not self.stop_all_sync():
return "FAIL", {"summary": "基线STOP_ALL无有效ACK"}
if not self.capture(1.2):
@@ -1182,13 +1588,27 @@ class SeatFunctionBridge(object):
self.latest_system["position_valid_mask"]
homed = []
for motor, expected_max in enumerate(CONFIRMED_MAX_HALL):
if not force_all_axes and (initial_mask & (1 << motor)) != 0:
initial_status = self.latest_motors[motor]
initial_diag = self.latest_diagnostics[motor]
already_zero = bool(
initial_status is not None and
initial_status.get("position_valid") and
initial_status.get("near") and
int(initial_status.get("position", 65535)) <= 100 and
initial_diag is not None and
int(initial_diag.get("hall", 65535)) <= 2)
position_valid = (initial_mask & (1 << motor)) != 0
should_home = (force_all_axes or not position_valid or
(home_nonzero_axes and not already_zero))
if not should_home:
homed.append({
"motor": motor,
"result": "ALREADY_VALID_NOT_HOMED",
"status": self.latest_motors[motor],
"diagnostic": self.latest_diagnostics[motor],
"note": "位置记录有效,未执行机械归零",
"status": initial_status,
"diagnostic": initial_diag,
"note": ("位置已有效且处于零位,未重复机械归零" if
already_zero else
"位置记录有效,未执行机械归零"),
})
continue
if self.check_emergency():
@@ -1196,8 +1616,6 @@ class SeatFunctionBridge(object):
"summary": "回最近端过程中收到紧急停止",
"axes": homed,
}
initial_status = self.latest_motors[motor]
initial_diag = self.latest_diagnostics[motor]
initial_current = self.latest_currents[motor]
initial_status = dict(initial_status) \
if initial_status is not None else None
@@ -1210,6 +1628,28 @@ class SeatFunctionBridge(object):
if initial_diag is not None:
observed_halls.append(int(initial_diag["hall"]))
current_peak = initial_current
endpoint_departure = None
if motor == 3 and (initial_status is None or
not initial_status.get("position_valid") or
initial_status.get("near") or
int(initial_status.get("position", 0)) <= 300):
departure_ok, endpoint_departure = \
self._depart_legrest_endpoint_for_home()
if not departure_ok:
self.stop_all_sync()
homed.append({
"motor": motor,
"result": "FAIL",
"endpoint_departure": endpoint_departure,
"initial_status": initial_status,
"initial_diagnostic": initial_diag,
"initial_current": initial_current,
})
return "FAIL", {
"summary": endpoint_departure.get(
"summary", "M3端点脱离失败"),
"axes": homed,
}
home_started = time.monotonic()
sequence = self.next_sequence()
state, response = self.send_command(
@@ -1299,6 +1739,7 @@ class SeatFunctionBridge(object):
"initial_status": initial_status,
"initial_diagnostic": initial_diag,
"initial_current": initial_current,
"endpoint_departure": endpoint_departure,
"peak_current": current_peak,
"home_elapsed_s": round(time.monotonic() - home_started, 3),
"observed_hall_values": distinct_halls,
@@ -1371,6 +1812,7 @@ class SeatFunctionBridge(object):
"initial_position_valid_mask": initial_mask,
"final_position_valid_mask": final_mask,
"force_all_axes": force_all_axes,
"home_nonzero_axes": home_nonzero_axes,
"actually_homed_axes": actually_homed,
"axes": homed,
"range_issues": range_issues,
@@ -1517,10 +1959,17 @@ class SeatFunctionBridge(object):
arm_payload = self._self_heal_hook_payload(
case_index, 0, arm_token)
self.drain()
self.transmit(SELF_HEAL_HOOK_REQUEST_ID, arm_payload,
"ARM " + note)
arm_ack = self._wait_self_heal_hook_response(
case_index, 0, arm_token, 3.0)
arm_ack = None
# A controlled watchdog case deliberately makes CAN disappear between
# INJECT and REPAIR. Repeat the idempotent ARM exchange while the ECU
# reboots instead of classifying that bounded outage as CAN failure.
arm_deadline = time.monotonic() + 12.0
while arm_ack is None and time.monotonic() < arm_deadline:
self.transmit(SELF_HEAL_HOOK_REQUEST_ID, arm_payload,
"ARM " + note)
arm_ack = self._wait_self_heal_hook_response(
case_index, 0, arm_token,
min(2.0, max(0.1, arm_deadline - time.monotonic())))
if arm_ack is None or arm_ack["result"] != 0:
return None
if after_arm is not None and not after_arm():
@@ -1579,6 +2028,17 @@ class SeatFunctionBridge(object):
self._publish_healing_progress(
request_id, case_id, "BASELINE", "正常任务持续运行",
prelude_telemetry["bench_reaction"], prelude_telemetry)
departure_ok, departure = \
self._depart_legrest_endpoint_for_home()
base["normal_task_endpoint_departure"] = departure
if not departure_ok:
self._publish_healing_progress(
request_id, case_id, "FAILED", "正常任务未建立",
"CRC用例的故障前腿托无法真实离开机械端点,未注入故障",
dict(base, request_feedback_match=False))
return "FAIL", {
"summary": departure.get(
"summary", "CRC用例腿托端点脱离失败")}
prelude = self._demo_axis_task(
profile["demo_motor"], profile.get("prelude_delta", 900))
if (prelude is None or
@@ -1590,11 +2050,49 @@ class SeatFunctionBridge(object):
"CRC用例的故障前腿托调整未完成,未注入故障",
dict(base, request_feedback_match=False))
return "FAIL", {"summary": "CRC用例的正常腿托调整未完成"}
self.capture(1.0)
if "demo_endpoints" in profile:
# The ECU marks SMV8 dirty before motion and only commits the
# settled clean record after a 3000 ms quiet period. Injecting
# CRC corruption earlier is correctly rejected by the firmware
# because there is not yet a saved comfort position to protect.
self.capture(3.5)
saved_target = int(prelude["target"])
self.capture(0.35)
current_status = self.latest_motors[profile["demo_motor"]] or {}
retreat_start = int(current_status.get(
"position", saved_target))
retreat_target = max(100, retreat_start - 200)
retreat = {
"motor": int(profile["demo_motor"]),
"start": retreat_start,
"target": retreat_target,
"sequence": None,
"ack": None,
}
if (not current_status.get("position_valid") or
not self._start_demo_axis_task(
retreat, "NORMAL TASK leave saved comfort pose") or
not self._wait_demo_axis_task(retreat)):
self._publish_healing_progress(
request_id, case_id, "FAILED", "正常任务未建立",
"CRC用例无法从已保存舒适位安全回退,未注入故障",
dict(base, request_feedback_match=False))
return "FAIL", {"summary": "CRC用例舒适位回退未完成"}
# Retreat is another persisted position transaction. Wait
# through its clean SMV8 commit before deliberately writing a
# higher-sequence corrupt inactive slot; otherwise the ECU
# correctly rejects the HIL injection while the store is dirty.
self.capture(3.5)
demo_task = {
"motor": int(profile["demo_motor"]),
"start": int(retreat["target"]),
"target": saved_target,
"sequence": None,
"ack": None,
}
if demo_task is None and "demo_endpoints" in profile:
demo_task = self._demo_axis_far_endpoint_task(
profile["demo_motor"], profile["demo_endpoints"])
else:
elif demo_task is None:
demo_task = self._demo_axis_task(
profile["demo_motor"], profile.get("demo_delta", 2000))
if demo_task is None:
@@ -1662,6 +2160,19 @@ class SeatFunctionBridge(object):
self._publish_healing_progress(
request_id, case_id, "BASELINE", "正常任务持续运行",
fan_running["bench_reaction"], fan_running)
self.latest_thermal = None
self.transmit(0x2AA, [0x01, 0, 0x1B, 0, 0, 0, 0, 0],
"FI-05 normal fan configuration")
self.transmit(0x29E, [0x25, 0, 0x02, 0, 0, 0, 0, 0],
"FI-05 normal fan gear 2")
if self._wait_thermal_feedback(0x0B, 3.0) is None:
return False
if not self.capture(profile.get("normal_observe_s", 5.5)):
return False
self.latest_thermal = None
self.transmit(0x42A, [0x0A, 0, 0, 0, 0, 0, 0, 0],
"FI-05 user requested fan off")
return self._wait_thermal_feedback(0x09, 3.0) is not None
return True
try:
injection_ack = self._run_self_heal_hook(
@@ -1721,6 +2232,16 @@ class SeatFunctionBridge(object):
"未收到台架故障钩子成功应答,AI拒绝执行修复", fault)
return "FAIL", {
"summary": "0x2F0故障注入没有收到有效0x2F1成功应答"}
if case_id == "FI-FIELD-05":
reentry_feedback = self._wait_thermal_feedback(0x0B, 3.0)
fault["fan_reentry_feedback"] = reentry_feedback
if reentry_feedback is None:
self._publish_healing_progress(
request_id, case_id, "FAILED", "风扇异常重启未建立",
"0x2F1已确认注入,但实物0x4FA未出现风扇2挡重入",
fault)
return "FAIL", {
"summary": "风扇重入故障没有形成实物反馈"}
self._publish_healing_progress(
request_id, case_id, "INJECTED", "实物故障已写入",
profile["fault_reaction"], fault)
@@ -1738,6 +2259,15 @@ class SeatFunctionBridge(object):
profile["fault_code"])
if not self.capture(1.2):
return "STOPPED", {"summary": "诊断展示阶段收到紧急停止"}
if case_id == "FI-FIELD-06":
# The real watchdog upper limit is 3200 ticks. The generic
# 1.8 s + 1.2 s display window ends just before expiry, which
# can race the repair request against the pending reset. Keep
# observing through the actual reboot; ARM below already
# retries idempotently while CAN is temporarily absent.
if not self.capture(2.0):
return "STOPPED", {
"summary": "WDT复位观察阶段收到紧急停止"}
repairing = dict(fault)
repairing["repair_action"] = profile["repair"]
@@ -1809,6 +2339,33 @@ class SeatFunctionBridge(object):
profile["index"], 3, 120.0,
"SELF HEAL VERIFY HOOK %s" % case_id)
protection_ok = verify_ack is not None and verify_ack["result"] == 0
fan_cycle_evidence = []
if protection_ok and case_id == "FI-FIELD-05":
for cycle in range(1, 4):
self.latest_thermal = None
self.transmit(
0x2AA, [0x01, 0, 0x1B, 0, 0, 0, 0, 0],
"FI-05 verify cycle %d config" % cycle)
self.transmit(
0x29E, [0x25, 0, 0x01, 0, 0, 0, 0, 0],
"FI-05 verify cycle %d fan on" % cycle)
on_feedback = self._wait_thermal_feedback(0x0A, 3.0)
if on_feedback is not None:
self.capture(0.8)
self.latest_thermal = None
self.transmit(
0x42A, [0x0A, 0, 0, 0, 0, 0, 0, 0],
"FI-05 verify cycle %d fan off" % cycle)
off_feedback = self._wait_thermal_feedback(0x09, 3.0)
cycle_ok = on_feedback is not None and \
off_feedback is not None
fan_cycle_evidence.append({
"cycle": cycle, "pass": cycle_ok,
"on_feedback": on_feedback,
"off_feedback": off_feedback,
})
protection_ok = protection_ok and cycle_ok
verifying["fan_stop_cycles"] = fan_cycle_evidence
self.message(
"SAFETY VERIFY %s: %s" %
("PASS" if protection_ok else "FAIL",
@@ -2145,6 +2702,8 @@ class SeatFunctionBridge(object):
if not valid:
result = "BLOCKED"
details = {"summary": "请求已过期或schema无效,未驱动任何输出"}
elif request.get("action") == "execute_command_plan":
result, details = self.execute_command_plan(request)
elif request.get("action") == "execute_mode":
result, details = self.execute_mode(request)
elif request.get("action") == "execute_thermal":