feat: validate real-bench seat orchestration and self-repair
This commit is contained in:
+23
-8
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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":
|
||||
|
||||
Reference in New Issue
Block a user