feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
+9
View File
@@ -10,4 +10,13 @@ tmp/*.log
.codex_tmp/ .codex_tmp/
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/ firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/ firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/
artifacts/hil/*_server_*.log
firmware/t2g/Seat_Controller_demo/IAR/settings/
firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.eww
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.Seat_Controller_demo_build_cache
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_deps
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_log
firmware/t2g/Seat_Controller_demo/IAR/rev_d/BrowseInfo/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/List/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/Obj/
firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem
+3 -1
View File
@@ -77,6 +77,8 @@ artifacts/runs/<request_id>/
## 可执行领域 ## 可执行领域
座椅与空调等车身设备使用同一 ZLG CAN 盒双通道联动时,参见 [T2G 座椅与车身双通道 CAN 联动](docs/workflows/t2g-cabin-dual-channel-can.md)。
当前已接入四个相互隔离的可执行领域: 当前已接入四个相互隔离的可执行领域:
- 座椅:所有热管理、位置移动和姿态预设能力仅面向副驾;未指定座位时默认副驾,显式主驾请求拒绝执行。 - 座椅:所有热管理、位置移动和姿态预设能力仅面向副驾;未指定座位时默认副驾,显式主驾请求拒绝执行。
@@ -134,4 +136,4 @@ PYTHONPATH=src python -m cognitive_agent.auth generate-secret
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。 完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
已验证的 T2G 加热、通风和四电机能力已完整纳入 `firmware/t2g`,包括 IAR 工程及仓库内 `common/tviibh8m` 构建依赖。M0/M1/M2/M3 分别对应水平、坐盆、靠背和腿托;真实硬件请求先被动核对新版周期反馈与 480/374/917/647 Hall 量程,未检测到匹配固件时默认通过 IAR C-SPY 自动烧写并复验,再发送 CAN 并等待真实回读。当前前端、能力注册、编译、安全门禁和状态反馈只允许副驾 `passenger/front_right`;固件中的 `Driver_Seat_*` 仅是板端既有协议字段名。构建、ZLG bridge、接线、安全门禁、前端测试集和已知量程归零见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md) 与 [前端能力测试集](docs/testing/t2g-seat-frontend-test-suite.md)。 已验证的 T2G 加热、通风、四电机和原生动态模式能力已完整纳入 `firmware/t2g`,包括 IAR 工程及仓库内 `common/tviibh8m` 构建依赖。M0/M1/M2/M3 分别对应水平、坐盆、靠背和腿托;revision 13通过`0x306`反馈带斜向耦合补偿的木马模式与四轴全程联动动态摇篮的阶段/循环状态。木马模式围绕中间姿态在`[270,250,460,400]`和`[370,130,540,460]`之间连续俯仰往复:坐盆峰峰行程120 Hall、约18 mm垂直分量,水平峰峰行程100 Hall、90%巡航Duty,用额外水平行程抵消坐盆斜向运动约16.4 mm的反向水平分量;参数集中在`seat_dynamic_mode_config.h`并由N7长期库镜像。动态模式收到一次启动命令后由ECU连续循环,收到停止/全停命令或累计运行10分钟时停止;目标到位式阶段在底层堵转/无运动保护继续生效的前提下使用独立保护窗口。真实硬件请求先被动核对新版周期反馈与 480/374/917/647 Hall 量程,未检测到匹配固件时默认通过 IAR C-SPY 自动烧写并复验,再发送 CAN 并等待真实回读。当前前端、能力注册、编译、安全门禁和状态反馈只允许副驾 `passenger/front_right`;固件中的 `Driver_Seat_*` 仅是板端既有协议字段名。构建、ZLG bridge、接线、安全门禁、前端测试集和已知量程归零见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md) 与 [前端能力测试集](docs/testing/t2g-seat-frontend-test-suite.md)。
+133 -32
View File
@@ -2641,7 +2641,7 @@ function updateSeatFeedbackModeLabel() {
} }
function selectExecutionMode(mode) { function selectExecutionMode(mode) {
if (mode === "hardware" && !state.runtime?.hardware?.configured) return; if (mode === "hardware" && !(state.runtime?.hardware?.configured && state.runtime?.vehicle_can?.configured)) return;
if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return; if (mode === "simulated_hardware" && !state.runtime?.simulated_hardware?.enabled) return;
state.executionMode = mode; state.executionMode = mode;
$$('[data-execution-mode]').forEach((button) => { $$('[data-execution-mode]').forEach((button) => {
@@ -2663,7 +2663,7 @@ function selectExecutionMode(mode) {
? "真实台架安全门禁" ? "真实台架安全门禁"
: simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界"; : simulatedHardware ? "模拟台架隔离边界" : "虚拟执行边界";
$("#safetyText").textContent = hardware $("#safetyText").textContent = hardware
? "副驾座椅映射到统一实体执行器;绝对位置会实时检查板端标定,小幅相对移动可走受限定时/Hall 回读,异常会尝试 STOP_ALL。" ? "同一 CAN 盒由单进程持有:通道 0 控制副驾座椅并校验板端反馈,通道 1 以 ISO CAN FD 500 kbit/s / 2 Mbit/s 控制空调等车身设备并校验状态帧。"
: simulatedHardware : simulatedHardware
? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。" ? "故障只注入执行报告;物理 CAN、ZLG 适配器和 IAR 固件路径保持关闭。"
: "四域 CAN 均为 dry-run,真实发送被强制关闭。"; : "四域 CAN 均为 dry-run,真实发送被强制关闭。";
@@ -2807,6 +2807,11 @@ function modeChipLabel(mode) {
: name; : name;
} }
function modeSecondsLabel(durationMs) {
const seconds = Number(durationMs) / 1000;
return Number.isInteger(seconds) ? String(seconds) : seconds.toFixed(1);
}
function populateModeChips() { function populateModeChips() {
const group = $(".mode-chips"); const group = $(".mode-chips");
if (!group) return; if (!group) return;
@@ -2825,7 +2830,41 @@ function populateModeChips() {
} }
button.dataset.modeProfileId = mode.id; button.dataset.modeProfileId = mode.id;
button.dataset.modeCommand = `进入${mode.name}`; button.dataset.modeCommand = `进入${mode.name}`;
button.title = mode.description || mode.name; const timeline = mode.timeline;
let cycleBadge = $(".mode-cycle-badge", button);
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
if (!cycleBadge) {
cycleBadge = document.createElement("small");
cycleBadge.className = "mode-cycle-badge";
button.append(cycleBadge);
}
const runtimeMinutes = Number(timeline.max_runtime_ms || 0) / 60000;
const runtimeLabel = Number.isInteger(runtimeMinutes)
? String(runtimeMinutes)
: runtimeMinutes.toFixed(1);
const cycleDurationMs = Number(timeline.cycle_duration_ms || 0);
const cycleLabel = cycleDurationMs > 0
? `${modeSecondsLabel(cycleDurationMs)}s循环`
: "目标到位循环";
cycleBadge.textContent = runtimeMinutes > 0
? `${cycleLabel} · 最长${runtimeLabel}min`
: cycleLabel;
button.dataset.modeBehavior = "cyclic";
} else if (cycleBadge) {
cycleBadge.remove();
}
const phaseSummary = Array.isArray(timeline?.phases)
? timeline.phases.map((phase) => {
if (Number(phase.duration_ms || 0) > 0) {
return `${phase.name} ${modeSecondsLabel(phase.duration_ms)}s`;
}
const timeout = Number(phase.timeout_ms || 0);
return timeout > 0
? `${phase.name} 目标到位(保护上限${modeSecondsLabel(timeout)}s)`
: `${phase.name} 目标到位`;
}).join(" → ")
: "";
button.title = [mode.description || mode.name, phaseSummary].filter(Boolean).join("\n");
}); });
refreshIcons(); refreshIcons();
} }
@@ -2864,6 +2903,8 @@ async function loadRuntime() {
populateVehicleSelector(); populateVehicleSelector();
populateModeChips(); populateModeChips();
const hardware = state.runtime.hardware || {}; const hardware = state.runtime.hardware || {};
const vehicleCan = state.runtime.vehicle_can || {};
const linkedHardwareReady = Boolean(hardware.configured && vehicleCan.configured);
const badge = $("#modelBadge"); const badge = $("#modelBadge");
const gptButton = $('[data-provider="openai"]'); const gptButton = $('[data-provider="openai"]');
if (state.runtime.openai_available) { if (state.runtime.openai_available) {
@@ -2882,7 +2923,7 @@ async function loadRuntime() {
} }
$("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`; $("#matrixCount").textContent = `${state.runtime.matrix.messages} / ${state.runtime.matrix.signals}`;
const hardwareButton = $("#hardwareMode"); const hardwareButton = $("#hardwareMode");
hardwareButton.disabled = !hardware.configured; hardwareButton.disabled = !linkedHardwareReady;
const simulation = state.runtime.simulated_hardware || {}; const simulation = state.runtime.simulated_hardware || {};
$("#simulationMode").disabled = !simulation.enabled; $("#simulationMode").disabled = !simulation.enabled;
$("#simulationMode").title = simulation.enabled $("#simulationMode").title = simulation.enabled
@@ -2897,23 +2938,23 @@ async function loadRuntime() {
const accessText = hardware.access_policy === "local_approval_idle_lease" const accessText = hardware.access_policy === "local_approval_idle_lease"
? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止` ? ` · 本地审批 · 空闲 ${hardware.access_inactivity_timeout_seconds || 180} 秒自动停止`
: ""; : "";
$("#hardwareNote").textContent = hardware.configured $("#hardwareNote").textContent = linkedHardwareReady
? `${hardware.backend} · ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · ${approvalText}${accessText}${concurrencyText}` ? `${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}`
: `真实执行已锁定:${profileText};请完成服务端启用、适配器、固件镜像${hardware.approval_required ? "和令牌" : ""}配置`; : `联动真实执行已锁定:${profileText};请完成座椅固件、双通道 ZLG 适配器、车身 CAN FD${hardware.approval_required ? "和令牌" : ""}配置`;
$("#hardwareAliasNote").textContent = "副驾座椅 → t2g_primary_seat(统一电机、加热和通风执行器)"; $("#hardwareAliasNote").textContent = "CH0 → 副驾座椅控制器 · CH1 → 空调/车窗/门锁/灯光/遮阳帘(ISO CAN FD)";
$("#autoFlashNote").textContent = hardware.auto_flash_enabled $("#autoFlashNote").textContent = hardware.auto_flash_enabled
? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。" ? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。"
: "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。"; : "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。";
$("#hardwareBadge").className = `status-badge ${hardware.configured ? "" : "danger"}`.trim(); $("#hardwareBadge").className = `status-badge ${linkedHardwareReady ? "" : "danger"}`.trim();
$("#hardwareBadge").innerHTML = hardware.configured $("#hardwareBadge").innerHTML = linkedHardwareReady
? '<i data-lucide="shield-check"></i>硬件已配置' ? '<i data-lucide="shield-check"></i>双通道已配置'
: '<i data-lucide="shield-off"></i>硬件锁定'; : '<i data-lucide="shield-off"></i>硬件锁定';
$("#hardwareCoverage").textContent = hardware.configured $("#hardwareCoverage").textContent = linkedHardwareReady
? `可选 · ${approvalText}${accessText}${concurrencyText}` ? `CH0 座椅 + CH${vehicleCan.channel} 车身 · ${approvalText}${accessText}${concurrencyText}`
: "已阻断"; : "已阻断";
$("#hardwareCoverage").className = hardware.configured ? "available" : "blocked"; $("#hardwareCoverage").className = linkedHardwareReady ? "available" : "blocked";
const modeAvailable = state.executionMode === "hardware" const modeAvailable = state.executionMode === "hardware"
? hardware.configured ? linkedHardwareReady
: state.executionMode === "simulated_hardware" : state.executionMode === "simulated_hardware"
? simulation.enabled ? simulation.enabled
: true; : true;
@@ -3136,6 +3177,20 @@ function mergeSeatActuatorState(previousState, patchState) {
mergedSeat.position = mergedPosition; mergedSeat.position = mergedPosition;
return; return;
} }
if (feature === "dynamic_mode") {
const item = $(`.seat-card[data-seat="${seat}"] [data-dynamic-mode]`);
if (!item) return;
const running = value.state === "running";
item.classList.toggle("running", running);
const runtimeMinutes = Number(value.max_runtime_ms || 600000) / 60000;
const runtimeLabel = Number.isInteger(runtimeMinutes)
? String(runtimeMinutes)
: runtimeMinutes.toFixed(1);
$("b", item).textContent = running
? `${value.name} · 循环中 · 最长${runtimeLabel}分钟`
: "未运行";
return;
}
mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value) mergedSeat[feature] = value && typeof value === "object" && !Array.isArray(value)
? { ...(previousSeat[feature] || {}), ...value } ? { ...(previousSeat[feature] || {}), ...value }
: value; : value;
@@ -3317,6 +3372,10 @@ const CONFIG_DOMAIN_META = {
climate: { label: "空调", icon: "fan", className: "climate" }, climate: { label: "空调", icon: "fan", className: "climate" },
ambient_light: { label: "氛围灯", icon: "lamp-ceiling", className: "ambient" }, ambient_light: { label: "氛围灯", icon: "lamp-ceiling", className: "ambient" },
roof: { label: "智能天幕", icon: "panels-top-left", className: "roof" }, roof: { label: "智能天幕", icon: "panels-top-left", className: "roof" },
door: { label: "车门锁", icon: "door-open", className: "generic" },
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
outlet: { label: "电动风口", icon: "wind", className: "climate" },
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
}; };
const CONFIG_CAPABILITY_LABELS = { const CONFIG_CAPABILITY_LABELS = {
@@ -3328,6 +3387,8 @@ const CONFIG_CAPABILITY_LABELS = {
"seat.set_backrest_recline": "调节靠背角度", "seat.set_backrest_recline": "调节靠背角度",
"seat.set_legrest_height": "调节腿托角度", "seat.set_legrest_height": "调节腿托角度",
"seat.set_legrest_extension": "调节腿托伸缩", "seat.set_legrest_extension": "调节腿托伸缩",
"seat.start_dynamic_mode": "启动动态座椅模式",
"seat.stop_dynamic_mode": "停止动态座椅模式",
"climate.set_temperature": "设置空调温度", "climate.set_temperature": "设置空调温度",
"climate.set_fan_level": "设置空调风量", "climate.set_fan_level": "设置空调风量",
"climate.set_power": "设置空调电源", "climate.set_power": "设置空调电源",
@@ -3335,6 +3396,15 @@ const CONFIG_CAPABILITY_LABELS = {
"climate.set_cycle_mode": "设置内外循环", "climate.set_cycle_mode": "设置内外循环",
"climate.set_outlet_mode": "设置出风模式", "climate.set_outlet_mode": "设置出风模式",
"climate.set_front_defrost": "设置前风挡除霜", "climate.set_front_defrost": "设置前风挡除霜",
"climate.set_control_mode": "设置空调控制模式",
"door.set_rear_lock": "设置后排门锁",
"window.set_motion": "控制车窗动作",
"window.set_position": "设置车窗开度",
"outlet.set_position_percent": "设置电动风口位置",
"outlet.set_calibrated_position": "设置电动风口标定位置",
"interior_light.set_indoor_power": "设置室内灯",
"interior_light.set_wall_power": "设置壁灯",
"interior_light.set_reading_power": "设置阅读灯",
"ambient_light.set_power": "设置氛围灯电源", "ambient_light.set_power": "设置氛围灯电源",
"ambient_light.set_illumination": "设置氛围灯颜色与亮度", "ambient_light.set_illumination": "设置氛围灯颜色与亮度",
"ambient_light.set_color": "设置氛围灯颜色", "ambient_light.set_color": "设置氛围灯颜色",
@@ -3342,6 +3412,7 @@ const CONFIG_CAPABILITY_LABELS = {
"ambient_light.set_mode": "设置氛围灯模式", "ambient_light.set_mode": "设置氛围灯模式",
"roof.set_transmittance": "设置天幕透光率", "roof.set_transmittance": "设置天幕透光率",
"roof.set_shade_position": "设置遮阳帘位置", "roof.set_shade_position": "设置遮阳帘位置",
"roof.set_shade_motion": "控制遮阳帘动作",
"roof.set_automatic_dimming": "设置自动调光", "roof.set_automatic_dimming": "设置自动调光",
}; };
@@ -3361,6 +3432,7 @@ const CONFIG_PARAMETER_LABELS = {
cycle_mode: "循环模式", cycle_mode: "循环模式",
outlet_mode: "出风模式", outlet_mode: "出风模式",
mode: "工作模式", mode: "工作模式",
mode_id: "固件模式编号",
}; };
function makeConfigElement(tagName, className, textValue) { function makeConfigElement(tagName, className, textValue) {
@@ -3427,8 +3499,13 @@ function renderConfigCards(result) {
const container = $("#configCards"); const container = $("#configCards");
container.replaceChildren(); container.replaceChildren();
const entries = configEntries(result); const entries = configEntries(result);
const operations = entries.flatMap(([domain, config]) => ( const operations = entries.flatMap(([configKey, config]) => (
(config?.spec?.operations || []).map((operation) => ({ domain, config, operation })) (config?.spec?.operations || []).map((operation) => ({
configKey,
domain: config?.spec?.domain || configKey,
config,
operation,
}))
)); ));
if (!operations.length) { if (!operations.length) {
const empty = makeConfigElement("div", "config-empty"); const empty = makeConfigElement("div", "config-empty");
@@ -3457,7 +3534,7 @@ function renderConfigCards(result) {
sceneSummary.append(sceneCopy, sceneStats); sceneSummary.append(sceneCopy, sceneStats);
container.append(sceneSummary); container.append(sceneSummary);
operations.forEach(({ domain, config, operation }, index) => { operations.forEach(({ configKey, domain, config, operation }, index) => {
const meta = CONFIG_DOMAIN_META[domain] || { label: domain, icon: "box", className: "generic" }; const meta = CONFIG_DOMAIN_META[domain] || { label: domain, icon: "box", className: "generic" };
const card = makeConfigElement("article", `config-card config-${meta.className}`); const card = makeConfigElement("article", `config-card config-${meta.className}`);
const header = makeConfigElement("header", "config-card-header"); const header = makeConfigElement("header", "config-card-header");
@@ -3499,7 +3576,7 @@ function renderConfigCards(result) {
}); });
parametersBlock.append(parameterGrid); parametersBlock.append(parameterGrid);
const plan = planForDomain(result, domain); const plan = planForDomain(result, configKey);
const allCommands = plan?.spec?.commands || []; const allCommands = plan?.spec?.commands || [];
const matchedCommands = allCommands.filter((command) => !command.operation_ref || command.operation_ref === operation.id); const matchedCommands = allCommands.filter((command) => !command.operation_ref || command.operation_ref === operation.id);
const command = matchedCommands[0] || null; const command = matchedCommands[0] || null;
@@ -3528,10 +3605,16 @@ function renderConfigCards(result) {
hardwareBlock.append(signalList); hardwareBlock.append(signalList);
const footer = makeConfigElement("footer", "config-card-footer"); const footer = makeConfigElement("footer", "config-card-footer");
const executionScope = config.metadata?.labels?.execution_scope;
const executionScopeLabel = result.execution_mode === "hardware" && domain === "seat"
? "座椅 CAN · CH0"
: executionScope === "vehicle_can_channel_1"
? "车身 CAN · CH1"
: executionScope === "virtual_only" ? "仅虚拟执行" : "已通过配置校验";
footer.append( footer.append(
makeConfigElement("span", "", `超时 ${operation.timeout_ms ?? "--"} ms`), makeConfigElement("span", "", `超时 ${operation.timeout_ms ?? "--"} ms`),
makeConfigElement("span", "", operation.failure_policy === "continue" ? "失败后继续" : "失败即中止"), makeConfigElement("span", "", operation.failure_policy === "continue" ? "失败后继续" : "失败即中止"),
makeConfigElement("span", "", config.metadata?.labels?.execution_scope === "virtual_only" ? "仅虚拟执行" : "已通过配置校验"), makeConfigElement("span", "", executionScopeLabel),
); );
card.append(header, objectBlock, parametersBlock, hardwareBlock, footer); card.append(header, objectBlock, parametersBlock, hardwareBlock, footer);
@@ -3605,7 +3688,8 @@ function renderExecution(result) {
return; return;
} }
const reportSucceeded = report.spec.status === "succeeded"; const reportSucceeded = report.spec.status === "succeeded";
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : "failed"}`; const partiallySucceeded = result.status === "partially_succeeded";
summary.className = `execution-summary ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
const presetSave = Boolean(result.preset_config); const presetSave = Boolean(result.preset_config);
const modeSave = Boolean(result.mode_profile); const modeSave = Boolean(result.mode_profile);
const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware"; const simulatedHil = report.metadata?.labels?.seat_feedback === "simulated_hardware";
@@ -3614,6 +3698,10 @@ function renderExecution(result) {
const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length; const motionCount = report.spec.results.filter((item) => item.telemetry?.motion_axis).length;
const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified); const firmwareResult = report.spec.results.find((item) => item.telemetry?.firmware_verified);
const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware"); const operationResults = report.spec.results.filter((item) => item.ref !== "seat-controller-firmware");
const succeededOperations = operationResults.filter((item) => item.status === "succeeded");
const failedOperations = operationResults.filter((item) => item.status === "failed");
const succeededDomains = [...new Set(succeededOperations.map((item) => item.domain))];
const failedDomains = [...new Set(failedOperations.map((item) => item.domain))];
let executionDetail = modeSave let executionDetail = modeSave
? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令` ? `长期模式 ${result.mode_profile.name} 已写入配置库,未生成 CAN 命令`
: presetSave : presetSave
@@ -3623,13 +3711,16 @@ function renderExecution(result) {
: physical : physical
? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤` ? `${executedDomains.join(" / ")} 已下发真实 CAN,并以 IAR 固件实际反馈确认 ${operationResults.filter((item) => item.status === "succeeded").length} 个步骤`
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`; : `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
if (partiallySucceeded) {
executionDetail = `部分执行完成:${succeededDomains.join(" / ")} 成功 ${succeededOperations.length} 项;${failedDomains.join(" / ")} 未完成 ${failedOperations.length} 项,失败设备未阻断其他设备`;
}
if (motionCount) executionDetail += `,含 ${motionCount} 个座椅位置电机反馈`; if (motionCount) executionDetail += `,含 ${motionCount} 个座椅位置电机反馈`;
if (firmwareResult?.telemetry?.programmed_this_request) executionDetail += ";本次已自动烧录并校验统一固件"; if (firmwareResult?.telemetry?.programmed_this_request) executionDetail += ";本次已自动烧录并校验统一固件";
else if (firmwareResult) executionDetail += ";板上统一固件协议签名已校验"; else if (firmwareResult) executionDetail += ";板上统一固件协议签名已校验";
if (result.status === "partially_succeeded") { if (result.deferred_requests?.length) {
executionDetail += ";未接入域请求已保留为 deferred request"; executionDetail += ";未接入域请求已保留为 deferred request";
} }
if (!reportSucceeded) { if (!reportSucceeded && !partiallySucceeded) {
const failure = report.spec.results.find((item) => item.status === "failed"); const failure = report.spec.results.find((item) => item.status === "failed");
const failureTelemetry = failure?.telemetry || {}; const failureTelemetry = failure?.telemetry || {};
executionDetail = simulatedHil executionDetail = simulatedHil
@@ -3655,13 +3746,18 @@ function renderExecution(result) {
: presetSave ? "姿态预设已保存" : presetSave ? "姿态预设已保存"
: simulatedHil ? "模拟台架执行成功" : simulatedHil ? "模拟台架执行成功"
: physical ? "真实台架执行成功" : "虚拟执行成功" : physical ? "真实台架执行成功" : "虚拟执行成功"
: simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败"; : partiallySucceeded ? (physical ? "真实台架部分执行成功" : "部分执行成功")
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`; : simulatedHil ? "模拟故障已触发" : physical ? "真实台架执行失败" : "虚拟执行失败";
const executionIcon = reportSucceeded
? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big")
: partiallySucceeded ? "circle-alert" : "circle-x";
summary.innerHTML = `<i data-lucide="${executionIcon}"></i><div><strong>${executionTitle}</strong><span>${executionDetail}</span></div>`;
$("#resultNavMeta").textContent = reportSucceeded $("#resultNavMeta").textContent = reportSucceeded
? `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}` ? `${report.spec.results.length} 项完成 · ${simulatedHil ? "SIM-HIL" : physical ? "HIL" : "DRY-RUN"}`
: "执行失败 · 查看回读"; : partiallySucceeded ? `${succeededOperations.length} 项成功 / ${failedOperations.length} 项失败 · 查看明细`
$("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : "FAILED"; : "执行失败 · 查看回读";
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : "failed"}`; $("#resultPanelState").textContent = reportSucceeded ? "SUCCEEDED" : partiallySucceeded ? "PARTIAL" : "FAILED";
$("#resultPanelState").className = `workspace-panel-state ${reportSucceeded ? "succeeded" : partiallySucceeded ? "partial" : "failed"}`;
report.spec.results.forEach((item) => { report.spec.results.forEach((item) => {
const row = document.createElement("div"); const row = document.createElement("div");
row.className = "result-item"; row.className = "result-item";
@@ -3706,7 +3802,8 @@ function renderResult(result) {
} else if (result.function_configs?.seat) { } else if (result.function_configs?.seat) {
state.seatFeedbackSource = "simulated"; state.seatFeedbackSource = "simulated";
} }
const executionSucceeded = ["succeeded", "partially_succeeded"].includes(result.status); const partiallySucceeded = result.status === "partially_succeeded";
const executionSucceeded = result.status === "succeeded" || partiallySucceeded;
const domains = Object.keys(result.function_configs || {}); const domains = Object.keys(result.function_configs || {});
const domainLabels = { const domainLabels = {
seat: "座椅", seat: "座椅",
@@ -3714,7 +3811,9 @@ function renderResult(result) {
roof: "天幕", roof: "天幕",
ambient_light: "氛围灯", ambient_light: "氛围灯",
}; };
$("#resultTitle").textContent = executionSucceeded $("#resultTitle").textContent = partiallySucceeded
? `${domains.length} 域场景部分完成`
: executionSucceeded
? result.mode_profile ? result.mode_profile
? "长期模式配置已保存" ? "长期模式配置已保存"
: result.preset_config : result.preset_config
@@ -3727,7 +3826,7 @@ function renderResult(result) {
$("#requestMeta").textContent = `${result.request_id} / context:${conversationLabel} / turn:${result.conversation_turn} / ${result.provider_used || result.provider_requested}`; $("#requestMeta").textContent = `${result.request_id} / context:${conversationLabel} / turn:${result.conversation_turn} / ${result.provider_used || result.provider_requested}`;
$("#requestMeta").textContent += ` / ${result.execution_report?.spec?.mode || result.execution_mode || "dry_run"}`; $("#requestMeta").textContent += ` / ${result.execution_report?.spec?.mode || result.execution_mode || "dry_run"}`;
setRunStatus( setRunStatus(
executionSucceeded ? "succeeded" : "failed", partiallySucceeded ? "partial" : executionSucceeded ? "succeeded" : "failed",
result.status.toUpperCase(), result.status.toUpperCase(),
); );
renderPipeline(result.trace_events || []); renderPipeline(result.trace_events || []);
@@ -3818,7 +3917,9 @@ async function submitRequest(event) {
const completed = ["succeeded", "partially_succeeded"].includes(payload.status); const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware"; const simulated = payload.execution_report?.metadata?.labels?.seat_feedback === "simulated_hardware";
showToast(completed showToast(completed
? (simulated ? (payload.status === "partially_succeeded"
? "已执行可用设备;部分设备未完成,详见执行结果"
: simulated
? "模拟台架执行完成" ? "模拟台架执行完成"
: payload.execution_report?.spec?.mode === "hil" : payload.execution_report?.spec?.mode === "hil"
? "真实台架执行与反馈确认已完成" ? "真实台架执行与反馈确认已完成"
+8 -2
View File
@@ -9,13 +9,13 @@
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" href="/icons/app-icon-192.png" /> <link rel="icon" href="/icons/app-icon-192.png" />
<title>智舱随行 · 一句话与手势控制</title> <title>智舱随行 · 一句话与手势控制</title>
<link rel="stylesheet" href="/styles.css?v=android-pad-2" /> <link rel="stylesheet" href="/styles.css?v=android-pad-3" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script> <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/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.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="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-1" defer></script> <script src="/network.js?v=android-pad-1" defer></script>
<script src="/app.js?v=seat-hil-9" defer></script> <script src="/app.js?v=seat-hil-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-1"></script> <script type="module" src="/gesture-control.js?v=android-pad-1"></script>
<script src="/pwa.js?v=android-pad-1" defer></script> <script src="/pwa.js?v=android-pad-1" defer></script>
</head> </head>
@@ -155,6 +155,9 @@
<div class="example-group"> <div class="example-group">
<span class="field-label">示例请求</span> <span class="field-label">示例请求</span>
<button type="button" class="example-command featured-example" data-example="副驾座椅通风3档,同时打开空调,温度设为24度,风量3档">
<i data-lucide="workflow"></i><span>副驾通风 + 空调 24℃ · 双通道联动</span><i data-lucide="chevron-right"></i>
</button>
<button type="button" class="example-command" data-example="空调打开,车内温度调到24度,风量3档"> <button type="button" class="example-command" data-example="空调打开,车内温度调到24度,风量3档">
<i data-lucide="fan"></i><span>空调 24℃ · 风量 3 档</span><i data-lucide="chevron-right"></i> <i data-lucide="fan"></i><span>空调 24℃ · 风量 3 档</span><i data-lucide="chevron-right"></i>
</button> </button>
@@ -378,6 +381,9 @@
<div class="seat-grid"> <div class="seat-grid">
<article class="seat-card" data-seat="passenger"> <article class="seat-card" data-seat="passenger">
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div> <div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div>
<div class="dynamic-mode-state" data-dynamic-mode>
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
</div>
<div class="thermal-states"> <div class="thermal-states">
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span> <span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span> <span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
+3 -3
View File
@@ -1,12 +1,12 @@
const CACHE_VERSION = "cognitive-agent-pad-v9"; const CACHE_VERSION = "cognitive-agent-pad-v13";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
"/", "/",
"/index.html", "/index.html",
"/styles.css?v=android-pad-2", "/styles.css?v=android-pad-3",
"/network.js?v=android-pad-1", "/network.js?v=android-pad-1",
"/app.js?v=seat-hil-9", "/app.js?v=seat-hil-13",
"/gesture-control.js?v=android-pad-1", "/gesture-control.js?v=android-pad-1",
"/pwa.js?v=android-pad-1", "/pwa.js?v=android-pad-1",
"/manifest.webmanifest", "/manifest.webmanifest",
+53
View File
@@ -1325,6 +1325,11 @@ textarea:focus {
background: var(--teal-soft); background: var(--teal-soft);
} }
.run-status.partial {
color: var(--amber);
background: var(--amber-soft);
}
.run-status.failed { .run-status.failed {
color: var(--red); color: var(--red);
background: var(--red-soft); background: var(--red-soft);
@@ -2733,6 +2738,11 @@ pre {
border-left-color: var(--teal); border-left-color: var(--teal);
} }
.execution-summary.partial {
background: var(--amber-soft);
border-left-color: var(--amber);
}
.execution-summary.failed { .execution-summary.failed {
background: var(--red-soft); background: var(--red-soft);
border-left-color: var(--red); border-left-color: var(--red);
@@ -3892,6 +3902,11 @@ pre {
background: var(--teal-soft); background: var(--teal-soft);
} }
.workspace-panel-state.partial {
color: var(--amber);
background: var(--amber-soft);
}
.workspace-panel-state.failed { .workspace-panel-state.failed {
color: var(--red); color: var(--red);
background: var(--red-soft); background: var(--red-soft);
@@ -4357,6 +4372,44 @@ pre {
} }
} }
.mode-cycle-badge {
margin-left: 2px;
padding: 2px 6px;
border-radius: 999px;
background: rgba(89, 214, 255, 0.12);
color: #8be3ff;
font-size: 10px;
letter-spacing: 0.02em;
}
.dynamic-mode-state {
display: flex;
align-items: center;
gap: 8px;
margin: 10px 0;
padding: 9px 11px;
border: 1px solid rgba(126, 160, 194, 0.2);
border-radius: 10px;
color: #90a7bc;
background: rgba(13, 27, 41, 0.55);
}
.dynamic-mode-state b {
margin-left: auto;
color: #b9cad8;
font-size: 12px;
}
.dynamic-mode-state.running {
border-color: rgba(89, 214, 255, 0.48);
color: #8be3ff;
box-shadow: inset 0 0 20px rgba(89, 214, 255, 0.06);
}
.dynamic-mode-state.running b {
color: #e0f8ff;
}
.hardware-access-field { .hardware-access-field {
padding: 12px; padding: 12px;
border: 1px solid rgba(38, 102, 159, 0.24); border: 1px solid rgba(38, 102, 159, 0.24);
@@ -0,0 +1,87 @@
{
"schema_version": "1.0.0",
"validated_at": "2026-08-04T09:55:52+08:00",
"platform": "t2g_cabin_bench",
"adapter": {
"model": "USBCANFD-200U",
"device_type": 41,
"device_index": 0,
"driver_self_test": {
"passed": true,
"pointer_bits": 32,
"missing_exports": [],
"classic_frame_size": 16,
"can_fd_frame_size": 72,
"can_fd_tx_size": 76,
"can_fd_rx_size": 80
}
},
"channels": {
"seat_can": {
"channel": 0,
"format": "classic_can_11_bit",
"bitrate": 500000,
"diagnostic_self_reception": {
"frame_id": "0x7AA",
"payload_hex": "43 48 30 53 45 41 54 30",
"passed": true
},
"external_passive_observation_seconds": 6,
"external_frames_observed": 0,
"physical_feedback_verified": false
},
"body_can": {
"channel": 1,
"format": "iso_can_fd_11_bit",
"arbitration_bitrate": 500000,
"data_bitrate": 2000000,
"bitrate_switch": true,
"diagnostic_self_reception": {
"frame_id": "0x7AA",
"payload_hex": "43 48 31 2D 42 4F 44 59 2D 43 41 4E 46 44 21 21",
"payload_size": 16,
"passed": true
},
"external_passive_observation_seconds": 5,
"external_frames_observed": 0,
"physical_feedback_verified": false
}
},
"linked_request": {
"utterance": "关闭副驾座椅通风,同时关闭空调",
"dry_run": {
"status": "succeeded",
"function_configs": ["seat", "native_climate"],
"command_plans": ["seat", "native_climate"]
},
"hardware_attempts": [
{
"request_id": "req-20260804015505-713de5",
"status": "execution_failed",
"body_can": "0x1DE transmit rejected; channel_error=0x00000004",
"seat_can": "passive firmware signature not observed within 1200 ms; no seat control frame sent"
},
{
"request_id": "req-20260804015552-674fb4",
"status": "execution_failed",
"body_can": "0x1DE transmit rejected; channel_error=0x00000000",
"seat_can": "passive firmware signature not observed within 3500 ms; no seat control frame sent"
}
]
},
"verdict": {
"adapter_dual_channel_transport": "passed",
"classic_can_transport": "passed",
"iso_can_fd_brs_transport": "passed",
"one_sentence_compilation_and_routing": "passed",
"seat_ecu_closed_loop": "not_verified_no_external_frames",
"body_ecu_closed_loop": "not_verified_no_bus_ack_or_0x404_feedback",
"overall_hardware_requirement": "blocked_by_external_bus_connectivity_or_power"
},
"safety_cleanup": {
"seat_control_acknowledged": false,
"body_control_acknowledged": false,
"actuator_state_changed_by_verified_command": false,
"zcanpro_restarted": true
}
}
@@ -0,0 +1,63 @@
{
"validated_at": "2026-08-04",
"firmware_revision": 10,
"firmware_image_sha256": "85f9d67ff1efb36c37f38bb7584dffdbb6ecbee33bfc70ab491bad0d57da63b2",
"change": "remove delayed M1 clearance/catch-up and keep all four axes moving from the phase start",
"max_hall": [499, 374, 917, 647],
"known_range_home": {
"hall_before": [379, 255, 417, 416],
"hall_after": [0, 0, 0, 0],
"all_complete_ok": true,
"all_position_valid": true
},
"step_1_to_step_2": {
"start_hall": [391, 140, 406, 405],
"target_hall": [246, 281, 701, 494],
"observed_hall": [246, 281, 701, 494],
"four_axis_start_spread_s": 0.070,
"four_axis_arrival_spread_s": 0.342,
"cruise_checkpoint": {
"elapsed_s": 3.090,
"hall": [354, 202, 510, 468],
"normalized_progress": {
"longitudinal": 0.255,
"cushion_tilt": 0.440,
"backrest_recline": 0.353,
"legrest_height": 0.708
}
},
"result": "PASS"
},
"step_2_to_step_3": {
"start_hall": [246, 281, 701, 494],
"target_hall": [391, 140, 405, 404],
"observed_hall": [391, 140, 405, 404],
"four_axis_start_spread_s": 0.050,
"four_axis_arrival_spread_s": 0.339,
"cruise_checkpoint": {
"elapsed_s": 2.591,
"hall": [283, 226, 616, 435],
"normalized_progress": {
"longitudinal": 0.255,
"cushion_tilt": 0.390,
"backrest_recline": 0.287,
"legrest_height": 0.656
}
},
"result": "PASS"
},
"continuous_rollover": {
"next_cycle_all_four_axes_active": true,
"next_cycle_start_spread_s": 0.051,
"result": "PASS"
},
"stop_verification": {
"ack": 1,
"reject": 0,
"final_0x306": "00 00 00 00 00 00 00 00",
"active_mask": 0,
"fault_mask": 0,
"result": "PASS"
},
"result": "PASS"
}
@@ -0,0 +1,88 @@
{
"validated_at": "2026-08-04",
"firmware_revision": 9,
"firmware_image_sha256": "216f3f6b2d7c441f2050e99c2b9d54153a69b46466235232dec1b7ab7644b7c4",
"mode": "dynamic_cradle",
"coordinate_precondition": {
"motor": 1,
"operation": "HOME_KNOWN_RANGE",
"ack": 1,
"reject": 0,
"hall_before": 169,
"hall_after": 0,
"max_hall_preserved": 374,
"near": true,
"position_valid": true,
"result": "PASS"
},
"full_cycle": {
"entry_step_1": {
"target_hall": [391, 140, 405, 404],
"observed_hall": [392, 141, 405, 403]
},
"step_1_to_step_2": {
"target_hall": [246, 281, 701, 494],
"observed_hall": [246, 281, 701, 494],
"duration_s": 17.329,
"four_axis_start_spread_s": 0.077,
"four_axis_arrival_spread_s": 0.338,
"result": "PASS"
},
"step_2_to_step_3": {
"target_hall": [391, 140, 405, 404],
"observed_hall": [391, 140, 405, 405],
"duration_s": 15.155,
"four_axis_start_spread_s": 0.079,
"four_axis_arrival_spread_s": 0.495,
"result": "PASS"
},
"cycle_index_after_return": 1,
"fault_mask": 0,
"result": "PASS"
},
"continuous_rollover": {
"cycle_0_forward_start_spread_s": 0.052,
"cycle_0_return_start_spread_s": 0.053,
"cycle_1_forward_start_spread_s": 0.082,
"phase_transitions_s": [0.370, 17.888, 32.781],
"result": "PASS"
},
"step_3_to_step_1_semantics": "Step 3 and Step 1 have identical Hall targets, so their boundary has zero physical displacement. The ECU rolls the cycle index atomically and synchronously launches the next Step 1-to-Step 2 leg.",
"stop_verification": {
"stop_dynamic_sent": true,
"final_0x306": "00 00 00 00 00 00 00 00",
"result": "PASS"
},
"frontend_natural_language_hil": {
"start": {
"utterance": "开启动态摇篮模式",
"request_id": "req-20260804071706-45a1df",
"result": "SUCCEEDED",
"firmware_signature": "seat-controller-smv8-dynamic-modes-v9",
"command": "0x300 0E 10 06 00 02 00 00 00",
"ui_state": "动态摇篮模式 · 循环中 · 最长10分钟"
},
"active_physical_can_observation": {
"source": "physical_can",
"complete": true,
"moving_axes": {
"longitudinal_pct": 59,
"backrest_recline_pct": 75
},
"all_axes_position_valid": true,
"faults": false
},
"stop": {
"utterance": "关闭动态摇篮模式",
"request_id": "req-20260804072028-55179f",
"result": "SUCCEEDED",
"command": "0x300 0F 10 07 00 00 00 00 00",
"ui_state": "未运行",
"all_axes_stopped": true,
"all_axes_position_valid": true,
"faults": false
},
"hardware_access_after_test": "automatically released by the 180-second local idle policy",
"result": "PASS"
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 11,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 349,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 21,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 21,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 163,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 22,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 22,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 534,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 23,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 23,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 456,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 24,
"opcode": 11,
"firmware_revision": 11
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 24,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 37,
"opcode": 14,
"firmware_revision": 11
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 262550.8121217,
"start_hall": [
320,
191,
500,
431
],
"first_active": {
"2": 262550.8883221,
"3": 262550.9146271,
"0": 262550.9401055,
"1": 262550.9661217
},
"arrival": {
"3": 262552.7830827,
"1": 262552.8335206,
"2": 262553.0696745,
"0": 262553.1213695
},
"cruise_checkpoint": {
"elapsed_s": 0.6343701000441797,
"hall": [
312,
201,
481,
417
],
"normalized_progress": [
0.26666666666666666,
0.3448275862068966,
0.475,
0.45161290322580644
]
},
"end_hall": [
290,
219,
460,
400
],
"start_spread_s": 0.07779960002517328,
"arrival_spread_s": 0.3382868000189774,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
220,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 262553.4043395,
"start_hall": [
290,
219,
460,
400
],
"first_active": {
"2": 262553.4973208,
"3": 262553.5228996,
"0": 262553.5485228,
"1": 262553.5743734
},
"arrival": {
"3": 262557.0745088,
"1": 262557.1252219,
"2": 262557.2577929,
"0": 262557.3081852
},
"cruise_checkpoint": {
"elapsed_s": 1.079516199999489,
"hall": [
306,
199,
489,
422
],
"normalized_progress": [
0.26666666666666666,
0.3389830508474576,
0.3625,
0.36666666666666664
]
},
"end_hall": [
350,
160,
540,
460
],
"start_spread_s": 0.0770526000414975,
"arrival_spread_s": 0.23367639997741207,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
160,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 262557.4375185,
"start_hall": [
350,
160,
540,
460
],
"first_active": {
"3": 262557.4893286,
"0": 262557.5161744,
"1": 262557.5424914,
"2": 262557.5696487
},
"arrival": {
"3": 262561.8903825,
"1": 262561.9425291,
"2": 262562.0729115,
"0": 262562.1245799
},
"cruise_checkpoint": {
"elapsed_s": 1.337460899958387,
"hall": [
335,
185,
498,
430
],
"normalized_progress": [
0.25,
0.4166666666666667,
0.525,
0.5
]
},
"end_hall": [
290,
220,
460,
400
],
"start_spread_s": 0.08032010000897571,
"arrival_spread_s": 0.23419739998644218,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
220,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 262562.35135919997,
"start_hall": [
290,
220,
460,
400
],
"first_active": {
"3": 262562.3911222,
"0": 262562.4169718,
"1": 262562.4437546,
"2": 262562.4700679
},
"arrival": {
"3": 262566.0670384,
"1": 262566.1195433,
"2": 262566.1444,
"0": 262566.2991242
},
"cruise_checkpoint": {
"elapsed_s": 1.1310953000211157,
"hall": [
305,
198,
497,
423
],
"normalized_progress": [
0.25,
0.36666666666666664,
0.4625,
0.38333333333333336
]
},
"end_hall": [
350,
160,
540,
459
],
"start_spread_s": 0.07894570002099499,
"arrival_spread_s": 0.23208580003120005,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
160,
540,
460
]
}
],
"repeat_first_active": {
"0": 262566.4901705,
"1": 262566.5153454,
"2": 262566.542698,
"3": 262566.5681911
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 38,
"opcode": 15,
"firmware_revision": 11
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 12,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 342,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 1,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 1,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 166,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 2,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 2,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 534,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 3,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 3,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 454,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 4,
"opcode": 11,
"firmware_revision": 12
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 4,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 17,
"opcode": 14,
"firmware_revision": 12
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 264077.9579478,
"start_hall": [
320,
191,
500,
430
],
"first_active": {
"0": 264078.038142,
"1": 264078.0663315,
"2": 264078.0774349,
"3": 264078.1169224
},
"arrival": {
"3": 264079.9439302,
"1": 264079.9959276,
"2": 264080.2325822,
"0": 264080.2840808
},
"cruise_checkpoint": {
"elapsed_s": 0.6441632999922149,
"hall": [
312,
214,
481,
416
],
"normalized_progress": [
0.26666666666666666,
0.3898305084745763,
0.475,
0.4666666666666667
]
},
"end_hall": [
290,
250,
460,
400
],
"start_spread_s": 0.07878040004288778,
"arrival_spread_s": 0.34015060000820085,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
250,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 264080.62360189995,
"start_hall": [
290,
250,
460,
400
],
"first_active": {
"0": 264080.6481176,
"1": 264080.6733371,
"2": 264080.7001602,
"3": 264080.7257557
},
"arrival": {
"1": 264084.2944588,
"3": 264084.344339,
"0": 264084.475855,
"2": 264084.5271018
},
"cruise_checkpoint": {
"elapsed_s": 1.0275715000461787,
"hall": [
305,
206,
492,
421
],
"normalized_progress": [
0.25,
0.36666666666666664,
0.4,
0.35
]
},
"end_hall": [
350,
132,
540,
460
],
"start_spread_s": 0.07763810001779348,
"arrival_spread_s": 0.2326430000248365,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
130,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 264084.65867120004,
"start_hall": [
350,
132,
540,
460
],
"first_active": {
"2": 264084.7388101,
"1": 264084.7410872,
"0": 264084.7599586,
"3": 264084.7926978
},
"arrival": {
"3": 264089.0549796,
"1": 264089.108405,
"0": 264089.3941196,
"2": 264089.4443601
},
"cruise_checkpoint": {
"elapsed_s": 1.391282099939417,
"hall": [
335,
187,
497,
429
],
"normalized_progress": [
0.25,
0.4661016949152542,
0.5375,
0.5166666666666667
]
},
"end_hall": [
290,
250,
460,
400
],
"start_spread_s": 0.05388769996352494,
"arrival_spread_s": 0.38938050001161173,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
290,
250,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 264089.7385309,
"start_hall": [
290,
250,
460,
400
],
"first_active": {
"1": 264089.857611,
"2": 264089.8838599,
"3": 264089.910015,
"0": 264089.938548
},
"arrival": {
"1": 264093.3849972,
"3": 264093.4384044,
"2": 264093.5165479,
"0": 264093.5667763
},
"cruise_checkpoint": {
"elapsed_s": 1.011584700027015,
"hall": [
305,
207,
492,
420
],
"normalized_progress": [
0.25,
0.35833333333333334,
0.4,
0.3333333333333333
]
},
"end_hall": [
350,
132,
540,
460
],
"start_spread_s": 0.08093699999153614,
"arrival_spread_s": 0.1817791000357829,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
350,
130,
540,
460
]
}
],
"repeat_first_active": {
"0": 264093.8496089,
"2": 264093.8578157,
"3": 264093.8843526,
"1": 264093.9379308
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 18,
"opcode": 15,
"firmware_revision": 12
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
@@ -0,0 +1,343 @@
{
"mode": "rocking_horse",
"firmware_revision": 13,
"max_hall": [
499,
374,
917,
647
],
"homes": [
{
"motor": 0,
"hall_before": 326,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 1,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 0,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 1,
"position_x100": 0
}
},
{
"motor": 1,
"hall_before": 138,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 2,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 1,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 2,
"position_x100": 0
}
},
{
"motor": 2,
"hall_before": 532,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 3,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 2,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 3,
"position_x100": 0
}
},
{
"motor": 3,
"hall_before": 451,
"hall_after": 0,
"ack": {
"ack": 1,
"reject": 0,
"sequence": 4,
"opcode": 11,
"firmware_revision": 13
},
"terminal": {
"motor": 3,
"task": 6,
"result": 2,
"active": false,
"position_valid": true,
"sequence": 4,
"position_x100": 0
}
}
],
"start_ack": {
"ack": 1,
"reject": 0,
"sequence": 17,
"opcode": 14,
"firmware_revision": 13
},
"segments": [
{
"cycle_index": 0,
"phase": 2,
"phase_start": 265437.2473564,
"start_hall": [
320,
190,
500,
430
],
"first_active": {
"2": 265437.2955111,
"3": 265437.3198522,
"0": 265437.346692,
"1": 265437.3733125
},
"arrival": {
"1": 265438.9329059,
"3": 265438.9846889,
"0": 265439.2201775,
"2": 265439.2731793
},
"cruise_checkpoint": {
"elapsed_s": 0.5042391999741085,
"hall": [
306,
209,
486,
419
],
"normalized_progress": [
0.28,
0.31666666666666665,
0.35,
0.36666666666666664
]
},
"end_hall": [
270,
250,
460,
400
],
"start_spread_s": 0.07780140003887936,
"arrival_spread_s": 0.34027340001193807,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
270,
250,
460,
400
]
},
{
"cycle_index": 0,
"phase": 3,
"phase_start": 265439.64164610003,
"start_hall": [
270,
250,
460,
400
],
"first_active": {
"2": 265439.6948618,
"3": 265439.7208077,
"0": 265439.7458537,
"1": 265439.7726268
},
"arrival": {
"1": 265442.9056772,
"3": 265442.9558483,
"2": 265443.0339587,
"0": 265443.0861803
},
"cruise_checkpoint": {
"elapsed_s": 0.8318679999792948,
"hall": [
295,
216,
484,
417
],
"normalized_progress": [
0.25,
0.2833333333333333,
0.3,
0.2833333333333333
]
},
"end_hall": [
370,
130,
540,
460
],
"start_spread_s": 0.07776499999454245,
"arrival_spread_s": 0.18050309998216107,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
370,
130,
540,
460
]
},
{
"cycle_index": 1,
"phase": 2,
"phase_start": 265443.25305889995,
"start_hall": [
370,
130,
540,
460
],
"first_active": {
"0": 265443.2954135,
"1": 265443.3206536,
"2": 265443.3348354,
"3": 265443.3728105
},
"arrival": {
"3": 265446.8221541,
"1": 265446.8749014,
"2": 265447.0045205,
"0": 265447.159461
},
"cruise_checkpoint": {
"elapsed_s": 1.084834800043609,
"hall": [
343,
175,
507,
436
],
"normalized_progress": [
0.27,
0.375,
0.4125,
0.4
]
},
"end_hall": [
270,
250,
460,
401
],
"start_spread_s": 0.07739699998637661,
"arrival_spread_s": 0.3373069000081159,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
270,
250,
460,
400
]
},
{
"cycle_index": 1,
"phase": 3,
"phase_start": 265447.3484032,
"start_hall": [
270,
250,
460,
401
],
"first_active": {
"1": 265447.3961179,
"0": 265447.400082,
"2": 265447.4222059,
"3": 265447.4483388
},
"arrival": {
"1": 265450.6434356,
"3": 265450.693563,
"2": 265450.7765666,
"0": 265450.8258244
},
"cruise_checkpoint": {
"elapsed_s": 0.9676259999978356,
"hall": [
297,
209,
490,
421
],
"normalized_progress": [
0.27,
0.3416666666666667,
0.375,
0.3389830508474576
]
},
"end_hall": [
370,
130,
540,
460
],
"start_spread_s": 0.05222090001916513,
"arrival_spread_s": 0.18238880002172664,
"all_axes_started": true,
"all_axes_arrived": true,
"target": [
370,
130,
540,
460
]
}
],
"repeat_first_active": {
"3": 265450.9977942,
"0": 265451.025777,
"2": 265451.0380102,
"1": 265451.0497914
},
"stop_ack": {
"ack": 1,
"reject": 0,
"sequence": 18,
"opcode": 15,
"firmware_revision": 13
},
"idle": {
"mode_id": 0,
"state": 0,
"phase": 0,
"active_mask": 0,
"fault_mask": 0,
"cycle_index": 0,
"phase_elapsed_ms": 0
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,764 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T14:53:29.226676Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801145329",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-QUEEN",
"group": "mode",
"utterance": "副驾执行女王副驾行驶模式",
"started_at": "2026-08-01T14:53:29.226724Z",
"status": "failed",
"errors": [
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145329-c441d5",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 6,
"motor_result_name": "TIMEOUT",
"position_valid": true,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 10 30 00 20 1C 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 30 00 01 01 F0 4B",
"timestamp": 26444.37267
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 67 28 30 00 10 27 00",
"timestamp": 26450.902006
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 90",
"timestamp": 26444.360963
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 05 F3 01 F3 01 00 A0",
"timestamp": 26450.903153
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 65,
"valid_sample_count": 65,
"max_current_raw": 214,
"max_current_delta_raw": 213,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 214,
"current_baseline_raw": 1,
"current_delta_raw": 213,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B D6 00 01 00 F4 01",
"timestamp": 26449.53215
}
},
"latest": {
"current_raw": 212,
"current_baseline_raw": 1,
"current_delta_raw": 211,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B D4 00 01 00 F4 01",
"timestamp": 26450.886864
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:36.269029Z",
"duration_ms": 7042
},
{
"id": "MODE-BABY-NAP",
"group": "mode",
"utterance": "副驾执行宝宝午睡模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:36.269096Z",
"finished_at": "2026-08-01T14:53:36.269098Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-CRUISE-SOFA",
"group": "mode",
"utterance": "副驾执行巡航沙发模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:36.269107Z",
"finished_at": "2026-08-01T14:53:36.269108Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-MEDITATION",
"group": "mode",
"utterance": "副驾执行冥想空间模式",
"started_at": "2026-08-01T14:53:36.269121Z",
"status": "failed",
"errors": [
"T2G motor 1 STALL: task_state=7, Hall=374->374"
],
"http_status": 201,
"request_id": "req-20260801145336-cea6e3",
"failure_telemetry": {
"motor_selector": 1,
"motion_axis": "cushion_tilt",
"task_state": 7,
"motor_result": 5,
"motor_result_name": "STALL",
"position_valid": true,
"hall_before": 374,
"hall_after": 374,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 56,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 11 34 00 30 11 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 34 00 01 02 F0 49",
"timestamp": 26453.41257
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 57 68 34 00 10 27 00",
"timestamp": 26462.801669
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "01 00 76 01 76 01 00 B0",
"timestamp": 26451.441787
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "01 14 76 01 76 01 00 C1",
"timestamp": 26462.802815
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 113,
"valid_sample_count": 113,
"max_current_raw": 1353,
"max_current_delta_raw": 1351,
"high_current_seen": true,
"low_hall_seen": true,
"stall_overlap_seen": true,
"peak": {
"current_raw": 1353,
"current_baseline_raw": 2,
"current_delta_raw": 1351,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": true,
"hall_activity_low": false,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "01 07 49 05 02 00 F4 01",
"timestamp": 26461.7585
}
},
"latest": {
"current_raw": 1199,
"current_baseline_raw": 2,
"current_delta_raw": 1197,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": true,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "01 0F AF 04 02 00 F4 01",
"timestamp": 26462.798972
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:48.119388Z",
"duration_ms": 11850
},
{
"id": "MODE-LEISURE",
"group": "mode",
"utterance": "副驾执行休闲躺椅模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:48.119450Z",
"finished_at": "2026-08-01T14:53:48.119452Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 30,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-CALF",
"group": "mode",
"utterance": "副驾执行小腿解压悬浮模式",
"started_at": "2026-08-01T14:53:48.119461Z",
"status": "failed",
"errors": [
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145348-bae676",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 6,
"motor_result_name": "TIMEOUT",
"position_valid": true,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "01 10 37 00 EC 13 30 75",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "21 00 37 00 01 01 F0 41",
"timestamp": 26463.239675
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 67 28 37 00 10 27 00",
"timestamp": 26469.752618
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 10",
"timestamp": 26463.237716
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 05 F3 01 F3 01 00 20",
"timestamp": 26469.754985
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 65,
"valid_sample_count": 65,
"max_current_raw": 253,
"max_current_delta_raw": 250,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 253,
"current_baseline_raw": 3,
"current_delta_raw": 250,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 03 FD 00 03 00 F4 01",
"timestamp": 26463.272395
}
},
"latest": {
"current_raw": 207,
"current_baseline_raw": 3,
"current_delta_raw": 204,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B CF 00 03 00 F4 01",
"timestamp": 26469.728393
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:53:55.135955Z",
"duration_ms": 7017
},
{
"id": "MODE-BEACH",
"group": "mode",
"utterance": "副驾执行电感沙滩椅模式",
"status": "safety_blocked",
"started_at": "2026-08-01T14:53:55.136004Z",
"finished_at": "2026-08-01T14:53:55.136006Z",
"duration_ms": 0,
"errors": [],
"safety_block": {
"axis": "cushion_tilt",
"target_pct": 0,
"minimum_safe_target_pct": 35,
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
"evidence": {
"hall_count": 122,
"pulse_count": 0,
"peak_current_delta_raw": 1205,
"stall_threshold_raw": 500
}
}
},
{
"id": "MODE-NAP",
"group": "mode",
"utterance": "副驾执行小憩模式",
"started_at": "2026-08-01T14:53:55.136014Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145355-391520",
"matched_modes": [
"nap"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3A 00 10 27 00",
"timestamp": 26476.732654
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 3B 00 10 27 00",
"timestamp": 26476.759174
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 3C 00 A7 1F 00",
"timestamp": 26476.785245
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 3D 00 B5 24 00",
"timestamp": 26476.810211
}
}
},
"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": 26476.820464
}
},
"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": 26476.820464
}
},
"captured_at": "2026-08-01T14:54:01.990826Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:02.155719Z",
"duration_ms": 7020
},
{
"id": "MODE-ZERO-GRAVITY",
"group": "mode",
"utterance": "副驾执行零重力模式",
"started_at": "2026-08-01T14:54:02.155796Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145402-f6ff8b",
"matched_modes": [
"zero_gravity"
],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3F 00 10 27 00",
"timestamp": 26483.63127
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 40 00 10 27 00",
"timestamp": 26483.657826
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 41 00 A7 1F 00",
"timestamp": 26483.682735
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 42 00 B5 24 00",
"timestamp": 26483.708846
}
}
},
"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": 26483.717983
}
},
"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": 26483.717983
}
},
"captured_at": "2026-08-01T14:54:08.888287Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:08.963843Z",
"duration_ms": 6808
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T14:54:08.963900Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145408-c833b5",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 42 00 B5 24 00",
"timestamp": 26484.485071
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 3F 00 10 27 00",
"timestamp": 26484.511148
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 40 00 10 27 00",
"timestamp": 26484.537248
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 41 00 A7 1F 00",
"timestamp": 26484.562609
}
}
},
"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": 26484.572862
}
},
"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": 26484.572862
}
},
"captured_at": "2026-08-01T14:54:09.742991Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:54:09.821001Z",
"duration_ms": 857
},
"finished_at": "2026-08-01T14:54:09.821038Z",
"summary": {
"total": 9,
"passed": 2,
"failed": 3,
"safety_blocked": 4,
"executed": 5,
"final_state_passed": true
}
}
@@ -0,0 +1,240 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T14:55:16.839705Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801145516",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T14:55:16.839746Z",
"status": "failed",
"errors": [
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801145516-496358",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 11,
"motor_result_name": "CALIBRATION_FAILED",
"position_valid": false,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "0B 10 44 00 52 4B 78 00",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "51 00 44 00 0B 41 F0 4F",
"timestamp": 26552.027376
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 B7 00 44 00 FF FF 00",
"timestamp": 26555.266451
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 C0",
"timestamp": 26552.016761
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 88 F3 01 F3 01 00 00",
"timestamp": 26555.267587
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 36,
"valid_sample_count": 36,
"max_current_raw": 223,
"max_current_delta_raw": 220,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 223,
"current_baseline_raw": 3,
"current_delta_raw": 220,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": true,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 13 DF 00 03 00 F4 01",
"timestamp": 26552.116307
}
},
"latest": {
"current_raw": 10,
"current_baseline_raw": 23,
"current_delta_raw": 0,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B 0A 00 17 00 F4 01",
"timestamp": 26555.246382
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T14:55:20.627987Z",
"duration_ms": 3788
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T14:55:20.628029Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801145520-65fe81",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 45 00 A7 1F 00",
"timestamp": 26556.1968
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 45 00 B5 24 00",
"timestamp": 26556.222629
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 00 45 00 FF FF 00",
"timestamp": 26556.248442
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 28 45 00 10 27 00",
"timestamp": 26556.274646
}
}
},
"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": 26556.283681
}
},
"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": 26556.283681
}
},
"captured_at": "2026-08-01T14:55:21.452124Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T14:55:21.531805Z",
"duration_ms": 904
},
"finished_at": "2026-08-01T14:55:21.531835Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,62 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:32:33.035150Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801153233",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T15:32:33.035187Z",
"status": "failed",
"errors": [
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
],
"http_status": 201,
"request_id": "req-20260801153233-4d8760",
"failure_telemetry": {
"error_type": "CanGatewayError",
"stop_all_attempted": false,
"simulated": false
},
"finished_at": "2026-08-01T15:32:33.233128Z",
"duration_ms": 198
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:32:33.233166Z",
"status": "failed",
"errors": [
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
],
"http_status": 201,
"request_id": "req-20260801153233-dc02d8",
"failure_telemetry": {
"error_type": "CanGatewayError",
"stop_all_attempted": false,
"simulated": false
},
"finished_at": "2026-08-01T15:32:33.390312Z",
"duration_ms": 157
},
"finished_at": "2026-08-01T15:32:33.390330Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": false
}
}
@@ -0,0 +1,240 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:38:36.967401Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801153836",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-M0-HOME",
"group": "motion",
"utterance": "把副驾座椅水平位置归零",
"started_at": "2026-08-01T15:38:36.967442Z",
"status": "failed",
"errors": [
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
],
"http_status": 201,
"request_id": "req-20260801153836-3cd030",
"failure_telemetry": {
"motor_selector": 0,
"motion_axis": "longitudinal",
"task_state": 7,
"motor_result": 11,
"motor_result_name": "CALIBRATION_FAILED",
"position_valid": false,
"hall_before": 499,
"hall_after": 499,
"hall_delta": 0,
"endpoint_recovery_max_near_hall": 72,
"command_frame": {
"frame_id": "0x300",
"payload_size": 8,
"data_hex": "0B 10 02 00 52 4B 78 00",
"timestamp": null
},
"ack_frame": {
"frame_id": "0x301",
"payload_size": 8,
"data_hex": "51 00 02 00 0B 41 E0 55",
"timestamp": 29165.165292
},
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 B7 00 02 00 FF FF 00",
"timestamp": 29168.401573
},
"diagnostic_before_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 00 F3 01 F3 01 00 20",
"timestamp": 29165.154003
},
"diagnostic_frame": {
"frame_id": "0x303",
"payload_size": 8,
"data_hex": "00 88 F3 01 F3 01 00 60",
"timestamp": 29168.402701
},
"diagnostic_read_error": null,
"current_diagnostic": {
"sample_count": 36,
"valid_sample_count": 36,
"max_current_raw": 238,
"max_current_delta_raw": 233,
"high_current_seen": false,
"low_hall_seen": true,
"stall_overlap_seen": false,
"peak": {
"current_raw": 236,
"current_baseline_raw": 3,
"current_delta_raw": 233,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": false,
"current_calibrating": true,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 13 EC 00 03 00 F4 01",
"timestamp": 29165.251397
}
},
"latest": {
"current_raw": 12,
"current_baseline_raw": 36,
"current_delta_raw": 0,
"stall_current_delta_threshold_raw": 500,
"current_sample_valid": true,
"current_baseline_valid": true,
"high_current": false,
"hall_activity_low": true,
"current_calibrating": false,
"frame": {
"frame_id": "0x304",
"payload_size": 8,
"data_hex": "00 0B 0C 00 24 00 F4 01",
"timestamp": 29168.379476
}
}
},
"error_type": "HardwareExecutionError",
"stop_all_attempted": true,
"simulated": false
},
"finished_at": "2026-08-01T15:38:53.697878Z",
"duration_ms": 16730
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:38:53.697934Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801153853-1d639f",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 30 28 03 00 10 27 00",
"timestamp": 29169.295418
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 30 08 03 00 A7 1F 00",
"timestamp": 29169.322276
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 30 08 03 00 B5 24 00",
"timestamp": 29169.347323
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 3,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 30 00 03 00 FF FF 00",
"timestamp": 29169.374233
}
}
},
"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": 29169.380975
}
},
"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": 29169.380975
}
},
"captured_at": "2026-08-01T15:38:54.491848Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:38:54.576238Z",
"duration_ms": 878
},
"finished_at": "2026-08-01T15:38:54.576286Z",
"summary": {
"total": 1,
"passed": 0,
"failed": 1,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,363 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T15:43:36.160681Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801154336",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "THERM-H-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅加热",
"started_at": "2026-08-01T15:43:36.160725Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154336-0eefe8",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29463.233038
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29463.259912
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29463.284973
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29463.311881
}
}
},
"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": 29463.319822
}
},
"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": 29463.319822
}
},
"captured_at": "2026-08-01T15:43:48.424601Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:48.512170Z",
"duration_ms": 12351
},
{
"id": "THERM-V-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅通风",
"started_at": "2026-08-01T15:43:48.512241Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154348-d8018c",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29464.079538
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29464.105253
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29464.131102
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29464.157968
}
}
},
"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": 29464.165819
}
},
"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": 29464.165819
}
},
"captured_at": "2026-08-01T15:43:49.270734Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:49.336080Z",
"duration_ms": 824
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T15:43:49.336131Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801154349-f42e7f",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 28 00 00 10 27 00",
"timestamp": 29464.904964
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 08 00 00 A7 1F 00",
"timestamp": 29464.931901
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 08 00 00 B5 24 00",
"timestamp": 29464.95768
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": false,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 65535,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 00 00 00 FF FF 00",
"timestamp": 29464.98386
}
}
},
"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": 29464.990659
}
},
"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": 29464.990659
}
},
"captured_at": "2026-08-01T15:43:50.095435Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T15:43:50.176751Z",
"duration_ms": 841
},
"finished_at": "2026-08-01T15:43:50.176781Z",
"summary": {
"total": 2,
"passed": 2,
"failed": 0,
"safety_blocked": 0,
"executed": 2,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:39:16.430915Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173916",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:39:16.430950Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173916-90206d",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 6D 01 00 00 00",
"timestamp": 36459.051975
}
},
"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 6E 01 00 00 00",
"timestamp": 36459.077734
}
},
"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 6F 01 00 00 00",
"timestamp": 36459.104324
}
},
"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 6C 01 00 00 00",
"timestamp": 36459.129781
}
}
},
"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": 36459.136472
}
},
"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": 36459.136472
}
},
"captured_at": "2026-08-01T17:40:24.101369Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:40:24.299645Z",
"duration_ms": 67869
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:40:24.299777Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801174024-2bef93",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 6C 01 00 00 00",
"timestamp": 36460.001759
}
},
"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 6D 01 00 00 00",
"timestamp": 36460.028327
}
},
"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 6E 01 00 00 00",
"timestamp": 36460.055218
}
},
"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 6F 01 00 00 00",
"timestamp": 36460.08066
}
}
},
"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": 36460.087354
}
},
"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": 36460.087354
}
},
"captured_at": "2026-08-01T17:40:25.052281Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:40:25.130245Z",
"duration_ms": 830
},
"finished_at": "2026-08-01T17:40:25.130276Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,364 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T20:30:03.347380Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801203003",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODEL-FUZZY-CRAMPED-COMFORT",
"group": "model",
"utterance": "前舱空间太挤了,把座椅调整到舒服的位置",
"started_at": "2026-08-01T20:30:43.772650Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203043-6dba03",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 35,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 3501,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 09 00 AD 0D 00",
"timestamp": 46720.941274
}
},
"legrest_height": {
"position_pct": 10,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 1005,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 0A 00 ED 03 00",
"timestamp": 46720.967852
}
},
"longitudinal": {
"position_pct": 80,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 8016,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 08 07 00 50 1F 00",
"timestamp": 46720.993263
}
},
"cushion_tilt": {
"position_pct": 30,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 2995,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 08 08 00 B3 0B 00",
"timestamp": 46721.019784
}
}
},
"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": 46721.030217
}
},
"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": 46721.030217
}
},
"captured_at": "2026-08-01T20:31:25.795596Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:31:25.863040Z",
"duration_ms": 42090
}
],
"position_memory_sync": {
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
"group": "preflight",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T20:30:03.347411Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203003-f00e7a",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 03 00 00 00 00",
"timestamp": 46678.830874
}
},
"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 04 00 00 00 00",
"timestamp": 46678.856952
}
},
"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 05 00 00 00 00",
"timestamp": 46678.882795
}
},
"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 02 00 00 00 00",
"timestamp": 46678.908649
}
}
},
"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": 46678.916532
}
},
"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": 46678.916532
}
},
"captured_at": "2026-08-01T20:30:43.683001Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:30:43.772568Z",
"duration_ms": 40425
},
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T20:31:25.863092Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801203125-a4af68",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 0C 00 00 00 00",
"timestamp": 46759.956435
}
},
"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 0D 00 00 00 00",
"timestamp": 46759.982688
}
},
"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 0E 00 00 00 00",
"timestamp": 46760.008938
}
},
"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 0F 00 00 00 00",
"timestamp": 46760.041009
}
}
},
"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": 46760.053878
}
},
"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": 46760.053878
}
},
"captured_at": "2026-08-01T20:32:04.818553Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T20:32:04.895597Z",
"duration_ms": 39033
},
"finished_at": "2026-08-01T20:32:04.895635Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"position_memory_sync_passed": true,
"final_state_passed": true
}
}
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:16:40.264559Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801171640",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:16:40.264614Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801171640-4a5f1e",
"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 19 01 00 00 00",
"timestamp": 35105.608647
}
},
"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 1A 01 00 00 00",
"timestamp": 35105.634423
}
},
"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 17 01 00 00 00",
"timestamp": 35105.661016
}
},
"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 18 01 00 00 00",
"timestamp": 35105.68652
}
}
},
"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": 35105.692065
}
},
"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": 35105.692065
}
},
"captured_at": "2026-08-01T17:17:50.683023Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:17:50.762718Z",
"duration_ms": 70498
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:17:50.762772Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801171750-2f0f2f",
"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 19 01 00 00 00",
"timestamp": 35106.475011
}
},
"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 1A 01 00 00 00",
"timestamp": 35106.500476
}
},
"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 17 01 00 00 00",
"timestamp": 35106.526322
}
},
"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 18 01 00 00 00",
"timestamp": 35106.552849
}
}
},
"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": 35106.558466
}
},
"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": 35106.558466
}
},
"captured_at": "2026-08-01T17:17:51.549417Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:17:51.636183Z",
"duration_ms": 873
},
"finished_at": "2026-08-01T17:17:51.636217Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,363 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T16:40:47.172484Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801164047",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "THERM-H-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅加热",
"started_at": "2026-08-01T16:40:47.172525Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164047-8cddb4",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32897.543083
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32897.568506
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32897.595022
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32897.620753
}
}
},
"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": 32897.62524
}
},
"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": 32897.62524
}
},
"captured_at": "2026-08-01T16:41:02.659736Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:02.738362Z",
"duration_ms": 15566
},
{
"id": "THERM-V-OFF",
"group": "thermal",
"utterance": "关闭副驾座椅通风",
"started_at": "2026-08-01T16:41:02.738421Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164102-401927",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32898.36913
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32898.394558
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32898.421367
}
},
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32898.446823
}
}
},
"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": 32898.453497
}
},
"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": 32898.453497
}
},
"captured_at": "2026-08-01T16:41:03.487981Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:03.570272Z",
"duration_ms": 832
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T16:41:03.570317Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801164103-4efef7",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 1,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 00 18 00 00 00 00 00",
"timestamp": 32899.183035
}
},
"backrest_recline": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 2,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 00 18 00 00 00 00 00",
"timestamp": 32899.209498
}
},
"legrest_height": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 3,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 00 18 00 00 00 00 00",
"timestamp": 32899.23527
}
},
"longitudinal": {
"position_pct": 0,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 0,
"motor_result": 0,
"motor_selector": 0,
"value_x100": 0,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 00 18 00 00 00 00 00",
"timestamp": 32899.260679
}
}
},
"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": 32899.267388
}
},
"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": 32899.267388
}
},
"captured_at": "2026-08-01T16:41:04.301854Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T16:41:04.424088Z",
"duration_ms": 854
},
"finished_at": "2026-08-01T16:41:04.424118Z",
"summary": {
"total": 2,
"passed": 2,
"failed": 0,
"safety_blocked": 0,
"executed": 2,
"final_state_passed": true
}
}
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:32:24.627065Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173224",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:32:24.627106Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173224-cedf76",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 30 01 00 00 00",
"timestamp": 36047.100786
}
},
"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 31 01 00 00 00",
"timestamp": 36047.126273
}
},
"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 32 01 00 00 00",
"timestamp": 36047.152829
}
},
"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 33 01 00 00 00",
"timestamp": 36047.178304
}
}
},
"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": 36047.185019
}
},
"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": 36047.185019
}
},
"captured_at": "2026-08-01T17:33:32.157834Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:33:32.231664Z",
"duration_ms": 67605
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:33:32.231720Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173332-3d22e2",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 30 01 00 00 00",
"timestamp": 36047.928676
}
},
"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 31 01 00 00 00",
"timestamp": 36047.954191
}
},
"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 32 01 00 00 00",
"timestamp": 36047.980699
}
},
"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 33 01 00 00 00",
"timestamp": 36048.006522
}
}
},
"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": 36048.013163
}
},
"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": 36048.013163
}
},
"captured_at": "2026-08-01T17:33:32.986103Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:33:33.067889Z",
"duration_ms": 836
},
"finished_at": "2026-08-01T17:33:33.067920Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:26:35.994926Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801172635",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T17:26:35.994963Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801172635-cf368d",
"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 23 01 00 00 00",
"timestamp": 35698.280073
}
},
"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 24 01 00 00 00",
"timestamp": 35698.306668
}
},
"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 21 01 00 00 00",
"timestamp": 35698.332116
}
},
"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 22 01 00 00 00",
"timestamp": 35698.357889
}
}
},
"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": 35698.36569
}
},
"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": 35698.36569
}
},
"captured_at": "2026-08-01T17:27:43.345246Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:27:43.418173Z",
"duration_ms": 67423
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:27:43.418231Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801172743-1f95ae",
"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 23 01 00 00 00",
"timestamp": 35699.098847
}
},
"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 24 01 00 00 00",
"timestamp": 35699.124304
}
},
"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 21 01 00 00 00",
"timestamp": 35699.150901
}
},
"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 22 01 00 00 00",
"timestamp": 35699.176715
}
}
},
"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": 35699.184476
}
},
"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": 35699.184476
}
},
"captured_at": "2026-08-01T17:27:44.164137Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:27:44.276096Z",
"duration_ms": 858
},
"finished_at": "2026-08-01T17:27:44.276129Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
{
"suite": "t2g-seat-frontend-full-hil",
"started_at": "2026-08-01T17:32:02.775803Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801173202",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-ZERO-GRAVITY",
"group": "mode",
"utterance": "打开零重力模式",
"started_at": "2026-08-01T17:32:02.775839Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173202-68d1da",
"matched_modes": [
"zero_gravity"
],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 2C 01 10 27 00",
"timestamp": 35964.438236
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 2D 01 A7 1F 00",
"timestamp": 35964.464297
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 2E 01 B5 24 00",
"timestamp": 35964.490843
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 2B 01 10 27 00",
"timestamp": 35964.51698
}
}
},
"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": 35964.526089
}
},
"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": 35964.526089
}
},
"captured_at": "2026-08-01T17:32:09.500643Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:32:09.582995Z",
"duration_ms": 6807
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风",
"started_at": "2026-08-01T17:32:09.583051Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801173209-29898b",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 2B 01 10 27 00",
"timestamp": 35965.246437
}
},
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 2C 01 10 27 00",
"timestamp": 35965.27405
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 2D 01 A7 1F 00",
"timestamp": 35965.3002
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 2E 01 B5 24 00",
"timestamp": 35965.326337
}
}
},
"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": 35965.334306
}
},
"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": 35965.334306
}
},
"captured_at": "2026-08-01T17:32:10.308851Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T17:32:10.379863Z",
"duration_ms": 797
},
"finished_at": "2026-08-01T17:32:10.379894Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,250 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T18:19:38.370275Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801181938",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MOTION-ALL-ZERO",
"group": "motion",
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T18:19:38.370320Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801181938-22b770",
"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 2A 01 00 00 00",
"timestamp": 38880.446998
}
},
"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 2B 01 00 00 00",
"timestamp": 38880.472823
}
},
"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 28 01 00 00 00",
"timestamp": 38880.498667
}
},
"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 29 01 00 00 00",
"timestamp": 38880.524482
}
}
},
"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": 38880.532693
}
},
"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": 38880.532693
}
},
"captured_at": "2026-08-01T18:20:45.450814Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T18:20:45.533084Z",
"duration_ms": 67163
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T18:20:45.533139Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801182045-0ea6d9",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 30 01 00 00 00",
"timestamp": 38903.567521
}
},
"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 2D 01 00 00 00",
"timestamp": 38903.593343
}
},
"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 2E 01 00 00 00",
"timestamp": 38903.619184
}
},
"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 2F 01 00 00 00",
"timestamp": 38903.646408
}
}
},
"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": 38903.653213
}
},
"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": 38903.653213
}
},
"captured_at": "2026-08-01T18:21:08.570962Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T18:21:08.651237Z",
"duration_ms": 23118
},
"finished_at": "2026-08-01T18:21:08.651275Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,252 @@
{
"suite": "t2g-seat-frontend-comprehensive-hil",
"started_at": "2026-08-01T18:23:33.171605Z",
"base_url": "http://127.0.0.1:8000",
"vehicle_id": "denza_n7",
"conversation_id": "conv-hil-full-20260801182333",
"runtime": {
"hardware_execution_allowed": true,
"firmware_profiles": [
"combined"
]
},
"results": [
{
"id": "MODE-ZERO-GRAVITY-OPEN",
"group": "mode",
"utterance": "打开零重力模式",
"started_at": "2026-08-01T18:23:33.171647Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801182333-ad87f6",
"matched_modes": [
"zero_gravity"
],
"state_http_status": 200,
"physical_state": {
"position": {
"cushion_tilt": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 1,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "01 26 28 33 01 10 27 00",
"timestamp": 39114.700574
}
},
"backrest_recline": {
"position_pct": 81,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 2,
"value_x100": 8103,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "02 26 08 34 01 A7 1F 00",
"timestamp": 39114.725999
}
},
"legrest_height": {
"position_pct": 94,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 3,
"value_x100": 9397,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "03 26 08 35 01 B5 24 00",
"timestamp": 39114.754159
}
},
"longitudinal": {
"position_pct": 100,
"position_valid": true,
"motion_state": "stopped",
"fault": false,
"task_state": 6,
"motor_result": 2,
"motor_selector": 0,
"value_x100": 10000,
"feedback_frame": {
"frame_id": "0x302",
"payload_size": 8,
"data_hex": "00 26 28 32 01 10 27 00",
"timestamp": 39114.780784
}
}
},
"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": 39114.790091
}
},
"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": 39114.790091
}
},
"captured_at": "2026-08-01T18:24:39.703780Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T18:24:39.790515Z",
"duration_ms": 66619
}
],
"final_state": {
"id": "FINAL-THERMAL-OFF",
"group": "cleanup",
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
"started_at": "2026-08-01T18:24:39.790569Z",
"status": "passed",
"errors": [],
"http_status": 201,
"request_id": "req-20260801182439-328aa0",
"matched_modes": [],
"state_http_status": 200,
"physical_state": {
"position": {
"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 38 01 00 00 00",
"timestamp": 39185.409902
}
},
"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 39 01 00 00 00",
"timestamp": 39185.435684
}
},
"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 3A 01 00 00 00",
"timestamp": 39185.461565
}
},
"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 37 01 00 00 00",
"timestamp": 39185.487663
}
}
},
"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": 39185.496704
}
},
"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": 39185.496704
}
},
"captured_at": "2026-08-01T18:25:50.408971Z",
"source": "physical_can",
"target": {
"device": "seat",
"zone": "front_right",
"instance": "passenger"
},
"complete": true,
"missing": []
},
"finished_at": "2026-08-01T18:25:50.490639Z",
"duration_ms": 70700
},
"finished_at": "2026-08-01T18:25:50.490676Z",
"summary": {
"total": 1,
"passed": 1,
"failed": 0,
"safety_blocked": 0,
"executed": 1,
"final_state_passed": true
}
}
@@ -0,0 +1,37 @@
environment: t2g_cabin_bench
platform: t2g_cabin_bench
execution:
mode: hil
hardware_enabled_env: COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED
approval_token_required_env: COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED
approval_token_env: COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN
seat_firmware_probe_timeout_ms: 3500
can_adapter:
backend: zlgcan
device: USBCANFD-200U
device_type: 41
device_index: 0
ownership: one_bridge_process_opens_device_once
buses:
seat_can:
channel_env: COGNITIVE_AGENT_SEAT_CAN_CHANNEL
default_channel: 0
frame_format: classic_can_11_bit
bitrate: 500000
termination_enabled_env: COGNITIVE_AGENT_SEAT_CAN_TERMINATION_ENABLED
termination_enabled_default: true
body_can:
enabled_env: COGNITIVE_AGENT_VEHICLE_CAN_ENABLED
channel_env: COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL
default_channel: 1
frame_format: can_fd_11_bit
arbitration_bitrate: 500000
data_bitrate: 2000000
iso: true
bitrate_switch: true
termination_enabled_env: COGNITIVE_AGENT_VEHICLE_CAN_TERMINATION_ENABLED
termination_enabled_default: false
safety:
channels_must_be_distinct: true
raw_can_passthrough: false
feedback_timeout_ms_env: COGNITIVE_AGENT_VEHICLE_CAN_FEEDBACK_TIMEOUT_MS
@@ -20,7 +20,7 @@ firmware:
topology: one_board_unified_seat_controller topology: one_board_unified_seat_controller
repository_project: firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp repository_project: firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp
repository_vendor_dependencies: [firmware/t2g/common, firmware/t2g/tviibh8m] repository_vendor_dependencies: [firmware/t2g/common, firmware/t2g/tviibh8m]
verification: passive_301_302_303_304_305_4FA_and_max_hall verification: passive_301_302_303_304_305_306_4FA_revision_7_and_max_hall
expected_max_hall: [480, 374, 917, 647] expected_max_hall: [480, 374, 917, 647]
auto_flash_enabled_env: COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED auto_flash_enabled_env: COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED
auto_flash_default_when_hardware_enabled: true auto_flash_default_when_hardware_enabled: true
@@ -0,0 +1,82 @@
id: dynamic_cradle
name: 动态摇篮模式
aliases:
- 动态摇篮
- 摇篮模式
- cradle
- dynamic cradle
description: >-
腾势 N7 固件原生位置闭环动态模式。Step 1先进入起始姿态;Step 2四轴同步启动并
以目标会合方式到达向前收拢姿态;Step 3四轴同步启动并会合返回起点,随后持续循环。
icon: waves
scope:
vehicle_ids:
- denza_n7
timeline:
repeat: until_stopped_or_timeout
max_runtime_ms: 600000
entry:
id: step1
name: 起始位置
transition: all_targets_reached
timeout_ms: 30000
targets:
longitudinal: {value: 172, unit: mm, hall_pulse: 391}
cushion_tilt: {value: 37.5, unit: percent, hall_pulse: 140}
backrest_recline: {value: 30, unit: degree_from_upright, hall_pulse: 405}
legrest_height: {value: 45, unit: degree, hall_pulse: 404}
phases:
- id: forward
name: Step 2 向前收拢
completion: all_targets_reached
timeout_ms: 30000
transition: synchronized_target_rendezvous
targets:
longitudinal: {value: 110, unit: mm, hall_pulse: 246}
cushion_tilt: {value: 75, unit: percent, hall_pulse: 281}
backrest_recline: {value: 52, unit: degree_from_upright, hall_pulse: 701}
legrest_height: {value: 55, unit: degree, hall_pulse: 494}
effects:
body_motion: {metaphor: cradle_rise, easing: smooth}
motion:
synchronized_start: true
synchronized_arrival: true
continuous_four_axis_cruise: true
cruise_duty_percent:
longitudinal: 50
cushion_tilt: 50
backrest_recline: 90
legrest_height: 50
final_approach_hall_pulses: 12
final_duty_percent: 50
- id: return
name: Step 3 向后回复
completion: all_targets_reached
timeout_ms: 30000
transition: synchronized_target_rendezvous
targets:
longitudinal: {value: 172, unit: mm, hall_pulse: 391}
cushion_tilt: {value: 37.5, unit: percent, hall_pulse: 140}
backrest_recline: {value: 30, unit: degree_from_upright, hall_pulse: 405}
legrest_height: {value: 45, unit: degree, hall_pulse: 404}
effects:
body_motion: {metaphor: cradle_sink, easing: smooth}
motion:
synchronized_start: true
synchronized_arrival: true
continuous_four_axis_cruise: true
cruise_duty_percent:
longitudinal: 50
cushion_tilt: 50
backrest_recline: 90
legrest_height: 50
final_approach_hall_pulses: 12
final_duty_percent: 50
operations:
- domain: seat
capability: seat.start_dynamic_mode
target_scope: passenger
operation: set
parameters:
mode_id: 2
reason: 启动 IAR revision 13 四轴全程联动位置闭环动态摇篮,循环至停止指令或十分钟上限。
@@ -0,0 +1,103 @@
id: rocking_horse
name: 木马模式
aliases:
- 木马模式
- 木马
- 摇摇马
- 骑木马
- rocking horse
description: >-
腾势 N7 固件原生动态模式。座椅围绕中间姿态连续向前俯摆、向后仰摆,
坐盆使用增强上下行程,四电机在每个半周期同步启动并以位置会合方式到达,
形成可感知升降、无屏息、无末段追赶的木马节奏。
icon: waves
scope:
vehicle_ids:
- denza_n7
seat_tuning:
coordinate_space: hall_pulse
firmware_header: firmware/t2g/Seat_Controller_demo/Config/seat_dynamic_mode_config.h
calibrated_max_hall: [480, 374, 917, 647]
actual_bench_max_hall: [499, 374, 917, 647]
experience_tuning:
cushion_peak_to_peak_hall_pulses: 120
cushion_peak_to_peak_vertical_mm: 18.0
cushion_amplitude_gain_vs_revision_11: 2.0
diagonal_coupling_compensation:
cushion_opposing_horizontal_mm: 16.4
longitudinal_peak_to_peak_hall_pulses: 100
longitudinal_peak_to_peak_mm: 44.8
estimated_net_horizontal_mm: 28.4
customization_rules:
- 所有目标必须位于对应轴已标定 Hall 量程内
- 前后端点应围绕中心姿态分布,避免接近机械端点
- 四轴占空比不得低于台架已验证的可靠起动值
- 修改本文件后必须同步修改固件配置头并重新执行 IAR/HIL
timeline:
repeat: until_stopped_or_timeout
max_runtime_ms: 600000
entry:
id: center
name: 木马中心姿态
transition: all_targets_reached
timeout_ms: 30000
targets:
longitudinal: {value: 143, unit: mm, hall_pulse: 320}
cushion_tilt: {value: 50.8, unit: percent, hall_pulse: 190}
backrest_recline: {value: 37.1, unit: degree_from_upright, hall_pulse: 500}
legrest_height: {value: 47.9, unit: degree, hall_pulse: 430}
phases:
- id: rock_forward
name: 向前俯摆
completion: all_targets_reached
timeout_ms: 15000
transition: synchronized_target_rendezvous
targets:
longitudinal: {value: 121, unit: mm, hall_pulse: 270}
cushion_tilt: {value: 66.8, unit: percent, hall_pulse: 250}
backrest_recline: {value: 34.1, unit: degree_from_upright, hall_pulse: 460}
legrest_height: {value: 44.5, unit: degree, hall_pulse: 400}
effects:
body_motion: {metaphor: rocking_horse_nose_down, easing: smooth}
motion:
synchronized_start: true
synchronized_arrival: true
continuous_four_axis_cruise: true
cruise_duty_percent:
longitudinal: 90
cushion_tilt: 90
backrest_recline: 90
legrest_height: 50
final_approach_hall_pulses: 8
final_duty_percent: 50
- id: rock_backward
name: 向后仰摆
completion: all_targets_reached
timeout_ms: 15000
transition: synchronized_target_rendezvous
targets:
longitudinal: {value: 166, unit: mm, hall_pulse: 370}
cushion_tilt: {value: 34.8, unit: percent, hall_pulse: 130}
backrest_recline: {value: 40.0, unit: degree_from_upright, hall_pulse: 540}
legrest_height: {value: 51.2, unit: degree, hall_pulse: 460}
effects:
body_motion: {metaphor: rocking_horse_nose_up, easing: smooth}
motion:
synchronized_start: true
synchronized_arrival: true
continuous_four_axis_cruise: true
cruise_duty_percent:
longitudinal: 90
cushion_tilt: 90
backrest_recline: 90
legrest_height: 50
final_approach_hall_pulses: 8
final_duty_percent: 50
operations:
- domain: seat
capability: seat.start_dynamic_mode
target_scope: passenger
operation: set
parameters:
mode_id: 1
reason: 启动 IAR revision 13 带坐盆斜向耦合水平补偿的原生木马模式,循环至停止指令或十分钟上限。
+21
View File
@@ -0,0 +1,21 @@
apiVersion: cognitive-agent/v1alpha1
kind: CapabilityCatalog
metadata:
id: door-native-can-capabilities
name: Door native CAN capabilities
version: 1.0.0
spec:
domain: door
capabilities:
- name: door.set_rear_lock
description: Lock or unlock the second-row door lock through the verified vehicle CAN command.
target_devices: [rear_door_lock]
target_scopes: [rear]
parameters:
- name: state
type: string
required: true
allowed_values: [lock, unlock]
description: Absolute second-row lock request.
risk_level: medium
confirmation_policy: on_ambiguity
@@ -0,0 +1,35 @@
apiVersion: cognitive-agent/v1alpha1
kind: CapabilityCatalog
metadata:
id: interior-light-native-can-capabilities
name: Interior light native CAN capabilities
version: 1.0.0
spec:
domain: interior_light
capabilities:
- name: interior_light.set_indoor_power
description: Turn the general indoor light on or off through vehicle CAN.
target_devices: [indoor_light]
target_scopes: [vehicle]
parameters: &power_parameters
- name: state
type: string
required: true
allowed_values: ["on", "off"]
description: Absolute lamp power state.
risk_level: low
confirmation_policy: on_ambiguity
- name: interior_light.set_wall_power
description: Turn the wall lamp on or off through vehicle CAN.
target_devices: [wall_lamp]
target_scopes: [vehicle]
parameters: *power_parameters
risk_level: low
confirmation_policy: on_ambiguity
- name: interior_light.set_reading_power
description: Turn all or one supported reading-light zone on or off through vehicle CAN FD.
target_devices: [reading_light]
target_scopes: [all, front_left, front_right, middle_left, middle_right, middle_left_rotary, middle_right_rotary, rear_left, rear_right]
parameters: *power_parameters
risk_level: low
confirmation_policy: on_ambiguity
@@ -0,0 +1,27 @@
apiVersion: cognitive-agent/v1alpha1
kind: CapabilityCatalog
metadata:
id: outlet-native-can-capabilities
name: Air outlet native CAN capabilities
version: 1.0.0
spec:
domain: outlet
capabilities:
- name: outlet.set_position_percent
description: Set both axes of one calibrated front air outlet using normalized percentages through vehicle CAN FD.
target_devices: [motorized_air_outlet]
target_scopes: [driver_left, driver_right, passenger_left, passenger_right]
parameters:
- {name: horizontal_pct, type: integer, unit: percent, minimum: 0, maximum: 100, required: true, description: Left-to-right target.}
- {name: vertical_pct, type: integer, unit: percent, minimum: 0, maximum: 100, required: true, description: Down-to-up target.}
risk_level: medium
confirmation_policy: on_ambiguity
- name: outlet.set_calibrated_position
description: Set both raw calibrated axes of one front air outlet.
target_devices: [motorized_air_outlet]
target_scopes: [driver_left, driver_right, passenger_left, passenger_right]
parameters:
- {name: horizontal_value, type: integer, minimum: -128, maximum: 126, required: true, description: Calibrated left-to-right coordinate.}
- {name: vertical_value, type: integer, minimum: -128, maximum: 126, required: true, description: Calibrated down-to-up coordinate.}
risk_level: medium
confirmation_policy: on_ambiguity
@@ -0,0 +1,35 @@
apiVersion: cognitive-agent/v1alpha1
kind: CapabilityCatalog
metadata:
id: window-native-can-capabilities
name: Window native CAN capabilities
version: 1.0.0
spec:
domain: window
capabilities:
- name: window.set_motion
description: Open, close, or stop one or more windows through vehicle CAN FD.
target_devices: [window]
target_scopes: [all, front, rear, front_left, front_right, rear_left, rear_right]
parameters:
- name: motion
type: string
required: true
allowed_values: [open, close, stop]
description: Explicit window motion command.
risk_level: high
confirmation_policy: on_ambiguity
- name: window.set_position
description: Set one window to an absolute opening percentage using position feedback and a stop command.
target_devices: [window]
target_scopes: [front_left, front_right, rear_left, rear_right]
parameters:
- name: position_pct
type: integer
unit: percent
minimum: 0
maximum: 100
required: true
description: Absolute window opening percentage.
risk_level: high
confirmation_policy: on_ambiguity
@@ -66,6 +66,13 @@
"operator": {"type": "string", "enum": ["eq", "neq", "gte", "lte", "within"]}, "operator": {"type": "string", "enum": ["eq", "neq", "gte", "lte", "within"]},
"value": {"type": ["number", "integer", "boolean", "string"]}, "value": {"type": ["number", "integer", "boolean", "string"]},
"tolerance": {"type": "number", "minimum": 0}, "tolerance": {"type": "number", "minimum": 0},
"state_signal": {"type": "string", "minLength": 1},
"accepted_states": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"type": "integer", "minimum": 0, "maximum": 255}
},
"timeout_ms": {"type": "integer", "minimum": 1, "maximum": 3600000} "timeout_ms": {"type": "integer", "minimum": 1, "maximum": 3600000}
} }
}, },
@@ -94,6 +101,13 @@
"operator": {"type": "string", "enum": ["eq", "neq", "gte", "lte", "within"]}, "operator": {"type": "string", "enum": ["eq", "neq", "gte", "lte", "within"]},
"value": {"type": ["number", "integer", "boolean", "string"]}, "value": {"type": ["number", "integer", "boolean", "string"]},
"tolerance": {"type": "number", "minimum": 0}, "tolerance": {"type": "number", "minimum": 0},
"state_signal": {"type": "string", "minLength": 1},
"accepted_states": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": {"type": "integer", "minimum": 0, "maximum": 255}
},
"timeout_ms": {"type": "integer", "minimum": 1, "maximum": 3600000} "timeout_ms": {"type": "integer", "minimum": 1, "maximum": 3600000}
} }
} }
@@ -0,0 +1,53 @@
# T2G 座椅与车身双通道 CAN 联动
## 集成基线
- 座椅控制沿用当前仓库的 IAR `Seat_Controller_demo` 协议与闭环回读。
- 车身控制从 `https://git.kendeji.fun/dev-team/project-code` 的 `CAN_DEV` 分支导入,固定基线为提交 `6e7ed04a468dfcde7efda9e30bc15144bf02faf9`。
- 不允许自然语言直接提供 CAN ID 或原始载荷;只有能力目录中命名、经过范围校验的动作可以编译到车身 CAN。
## 适配器和通道
一个 `USBCANFD-200U` 只由一个 32 位桥接进程打开一次,再向应用暴露两个通道视图:
| 通道 | 用途 | 帧格式 | 仲裁速率 | 数据速率 |
|---|---|---|---:|---:|
| CH0 | T2G 副驾座椅控制器 | 11-bit Classic CAN | 500 kbit/s | — |
| CH1 | 空调、车窗、门锁、灯光、遮阳帘和电动风口 | 11-bit ISO CAN FD;按报文选择 BRS | 500 kbit/s | 2 Mbit/s |
车身通道使用 60 MHz CAN 时钟。仲裁段为 `tseg1=14, tseg2=3, sjw=2, smp=0, brp=5`,20 TQ、80% 采样点;数据段为 `tseg1=10, tseg2=2, sjw=2, smp=0, brp=1`,15 TQ、80% 采样点。ZLG 运行时使用驱动内置的 `500000`/`2000000`、80% 参数项,并显式选择 Normal、ISO CAN FD。
## 服务端配置
```powershell
$env:COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED = "true"
$env:COGNITIVE_AGENT_VEHICLE_CAN_ENABLED = "true"
$env:COGNITIVE_AGENT_SEAT_CAN_BACKEND = "zlgcan"
$env:COGNITIVE_AGENT_VEHICLE_CAN_BACKEND = "zlgcan"
$env:COGNITIVE_AGENT_SEAT_CAN_CHANNEL = "0"
$env:COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL = "1"
$env:COGNITIVE_AGENT_VEHICLE_CAN_ARBITRATION_BITRATE = "500000"
$env:COGNITIVE_AGENT_VEHICLE_CAN_DATA_BITRATE = "2000000"
$env:COGNITIVE_AGENT_SEAT_CAN_TERMINATION_ENABLED = "true"
$env:COGNITIVE_AGENT_VEHICLE_CAN_TERMINATION_ENABLED = "false"
$env:COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES = "combined"
$env:COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN = "<bench-only-token>"
```
真实执行会拒绝以下情况:两个逻辑总线配置为同一通道、CAN FD 时序不等于已验证值、ZLG 32 位运行时缺失、座椅固件未通过被动签名检查、车身动作不在可信能力目录内,或需要反馈的车身指令未收到对应状态帧。
## 一句话联动示例
`副驾座椅通风3档,同时打开空调,温度设为24度,风量3档`
该请求生成独立的 `seat` 与 `native_climate` FunctionConfig/CommandPlan。真实执行时,两组执行器在独立线程中使用 CH0/CH1 通道视图;底层桥接仍统一串行化 SDK 调用,并将长时间接收切成不超过 20 ms 的时间片,以便两个通道交替收发。
## 验证要求
1. 先运行 `zlgcan_bridge.py --self-test`,确认经典 CAN 与 CAN FD API 导出及 ABI 结构尺寸。
2. 在 ZCANPRO 确认 CH0/CH1 接线、终端电阻和总线状态正常,然后关闭其在线通道,避免与服务争用设备。
3. 用 dry-run 核对两份计划:座椅报文位于 `seat_can`,车身报文位于 `body_can`;空调控制 ID 为 `0x1DE`。
4. 以 hardware 模式执行低风险联动句,要求座椅板端反馈成功,且车身通道观测到对应状态反馈(空调为 `0x404`)。仅发送成功而无状态帧不能记为通过。
5. 验证完成后关闭座椅热舒适功能、发送座椅 `STOP_ALL`,并按测试前状态恢复车身设备。
相关机器可读配置位于 `platforms/t2g_cabin_bench/` 与 `config/environments/t2g_cabin_bench.example.yaml`。
+16 -2
View File
@@ -42,13 +42,13 @@ powershell -ExecutionPolicy Bypass -File .\tools\build_seat_firmware.ps1
- 热管理命令:`0x29E/0x3CE`;档位反馈:`0x4FA`。 - 热管理命令:`0x29E/0x3CE`;档位反馈:`0x4FA`。
- 移动命令:`0x300`;系统 ACK:`0x301`;归一化位置:`0x302`;Hall/量程:`0x303`。 - 移动命令:`0x300`;系统 ACK:`0x301`;归一化位置:`0x302`;Hall/量程:`0x303`。
- 电流诊断:`0x304` 四轴轮询,`0x305` 通风/加热;这些值用于台架可见性,当前保守阈值不能替代外部限流或量产保护。 - 电流诊断:`0x304` 四轴轮询,`0x305` 通风/加热;动态模式:`0x306` 以100 ms周期报告模式、阶段、循环和活动轴。这些诊断值用于台架可见性,当前保守阈值不能替代外部限流或量产保护。
- Opcode 1 的 `ParamA` 为 1–10000 绝对位置;后端显式发送 30 s 超时覆盖值。 - Opcode 1 的 `ParamA` 为 1–10000 绝对位置;后端显式发送 30 s 超时覆盖值。
- 前端目标 `0%`/“归零”不是普通绝对位置,而是 `HOME_KNOWN_RANGE (0x0B)`;只有收到 `COMPLETE/OK`、Near、Hall≤2 和 PositionValid 才算成功。 - 前端目标 `0%`/“归零”不是普通绝对位置,而是 `HOME_KNOWN_RANGE (0x0B)`;只有收到 `COMPLETE/OK`、Near、Hall≤2 和 PositionValid 才算成功。
- Opcode 3 的 `ParamA` 为持续时间,`ParamB` 为驱动等级×10(默认 90%)。 - Opcode 3 的 `ParamA` 为持续时间,`ParamB` 为驱动等级×10(默认 90%)。
- `STOP_ALL` 的专用 ACK 为 4;完成/故障 TaskState 分别为 6/7。 - `STOP_ALL` 的专用 ACK 为 4;完成/故障 TaskState 分别为 6/7。
新版固件把 `0x304/0x305` 用于电流诊断,不能继续发送旧版身份请求。后端在任何控制帧之前被动观察 `0x301–0x305`、`0x4FA`、四个 MotorID、固件 revision 6 和各轴标称 MaxHall(允许 5% 台架差异);真实硬件执行启用后,未观察到完整签名会自动使用仓库内 IAR 镜像烧写,重新被动验证成功后继续原请求。烧写或复验失败时禁止发送座椅控制帧。 新版固件把 `0x304/0x305` 用于电流诊断,并用`0x306`报告原生动态模式,不能继续发送旧版身份请求。后端在任何控制帧之前被动观察 `0x301–0x306`、`0x4FA`、四个 MotorID、固件 revision 13 和各轴标称 MaxHall(允许 5% 台架差异);真实硬件执行启用后,未观察到完整签名会自动使用仓库内 IAR 镜像烧写,重新被动验证成功后继续原请求。烧写或复验失败时禁止发送座椅控制帧。
## 启用配置 ## 启用配置
@@ -99,6 +99,20 @@ COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S=120
2026-08-01 长序列中记录到的 M0/M2 无 Hall、M1 高电流无 Hall 故障,后续确认由水平、坐盆和靠背 Hall 地线断开引起,并非软件标定或堵转阈值问题。2026-08-02 重新接好地线并用手工 HIL GUI 验证后,仓库 revision 6 自动烧写成功,四轴均从机械零位以 `PositionValid=1` 启动;自动前端 HIL 的 33 条热功能、单轴、四轴归零、组合和预设模式用例全部通过。因此测试清单中的临时 `safety_blocked` 已撤销。若以后再次出现同类现象,仍应以命令期间 `0x303` Hall 与 `0x304` 电流联合诊断,不能通过提高 Duty 或阈值掩盖线路故障。 2026-08-01 长序列中记录到的 M0/M2 无 Hall、M1 高电流无 Hall 故障,后续确认由水平、坐盆和靠背 Hall 地线断开引起,并非软件标定或堵转阈值问题。2026-08-02 重新接好地线并用手工 HIL GUI 验证后,仓库 revision 6 自动烧写成功,四轴均从机械零位以 `PositionValid=1` 启动;自动前端 HIL 的 33 条热功能、单轴、四轴归零、组合和预设模式用例全部通过。因此测试清单中的临时 `safety_blocked` 已撤销。若以后再次出现同类现象,仍应以命令期间 `0x303` Hall 与 `0x304` 电流联合诊断,不能通过提高 Duty 或阈值掩盖线路故障。
2026-08-03 revision 7加入`START_DYNAMIC_MODE(0x0E)`、`STOP_DYNAMIC_MODE(0x0F)`和`0x306`;revision 8验证了连续循环、循环中停止、堵转/无运动保护和ECU启动累计600 s自动停止的公共动态模式运行时。旧ModeID 1轨迹现已从固件、长期库和前端能力中移除,revision 11复用ModeID 1实现木马模式。
2026-08-04 revision 8复测时,公共动态运行时曾单次连续运行600.024 s并自动返回全零IDLE且无FAULT;循环中`STOP_DYNAMIC`也通过。同期动态摇篮被实台M1坐盆轴的真实STALL阻断,独立单轴复测同样在Hall约169失速;后续已恢复机构运动并保留安全全停,未通过降低目标或屏蔽堵转保护改判。
同日revision 9把摇篮改为位置闭环会合,不再使用固定30 s阶段。M1以`0x0B`从Hall 169连续回近端0且保持MaxHall=374后,Step 1→2实测17.329 s、启动/到位离散0.077/0.338 s;Step 2→3实测15.155 s、离散0.079/0.495 s;目标精确、CycleIndex进入1、下一轮四轴启动离散0.082 s、停止全零,PASS。证据见`artifacts/hil/dynamic_cradle_rev9_20260804.json`。Step 3和Step 1坐标相同,其边界是零位移原子换轮;可观测同步动作是紧随其后的下一轮Step 1→2四轴启动。
同日revision 10移除revision 9中“M1先走10 Hall、等待其他轴预备到位后再单独追赶”的临时避让。正向/返回四轴从阶段开始即全程联动;水平/靠背达到约25%行程时,坐盆已分别完成44.0%/39.0%,启动离散0.070/0.050 s,到位离散0.342/0.339 s,下一轮启动离散0.051 s,无FAULT并可正常停止。证据见`artifacts/hil/dynamic_cradle_rev10_continuous_cruise_20260804.json`。
同日revision 11移除旧ModeID 1轨迹并加入木马模式。入口为`[320,190,500,430]`,连续端点为前俯`[290,220,460,400]`与后仰`[350,160,540,460]`;稳态完整循环约9.0 s。实测两个稳态半周期四轴启动离散0.080/0.079 s、到位离散0.234/0.232 s;水平轴到25%时坐盆已达41.7%/36.7%,目标误差不超过1 Hall,自动续环与循环中停止均PASS。证据见`artifacts/hil/rocking_horse_rev11_20260804.json`。
同日revision 12把木马坐盆端点由`220↔160`扩大为`250↔130` Hall,并把M1巡航占空比由50%提升为90%。峰峰上下行程由60增至120 Hall,折算垂直分量约由9 mm增至18 mm;稳态前俯/后仰启动离散0.054/0.081 s,到位离散0.389/0.182 s,水平轴到25%时坐盆已完成46.6%/35.8%,完整循环约9.2 s。目标误差不超过2 Hall、无FAULT、自动续环与停止全零均PASS。证据见`artifacts/hil/rocking_horse_rev12_cushion_amplitude_20260804.json`。
同日revision 13加入坐盆斜向运动的水平前馈补偿:木马M0端点由`290↔350`扩大为`270↔370` Hall,峰峰行程100 Hall,巡航Duty由50%提升为90%。按台架M0 MaxHall=499换算水平峰峰行程约43.1 mm,扣除M1斜向行程估算的16.4 mm反向水平分量后,净水平摆幅约26.7 mm。稳态前俯/后仰启动离散0.077/0.052 s,到位离散0.337/0.182 s;巡航检查点四轴均持续推进,目标误差不超过1 Hall、无FAULT、自动续环与停止全零均PASS。证据见`artifacts/hil/rocking_horse_rev13_horizontal_compensation_20260804.json`。
2026-08-02 时长/超量程规则完成后,扩充清单的真实回归结果为 161/161 通过、0 失败、0 `safety_blocked`,最终清理和独立物理复核均通过。报告位于 `artifacts/hil/seat_frontend_hil_20260802_full_timed_overflow.json`;复核时加热/通风为 0 档,M0/M1/M2/M3 均为 0%,全部 `Position_Valid=1`。 2026-08-02 时长/超量程规则完成后,扩充清单的真实回归结果为 161/161 通过、0 失败、0 `safety_blocked`,最终清理和独立物理复核均通过。报告位于 `artifacts/hil/seat_frontend_hil_20260802_full_timed_overflow.json`;复核时加热/通风为 0 档,M0/M1/M2/M3 均为 0%,全部 `Position_Valid=1`。
2026-08-02 增加 GPT-5.6-sol 模糊意图与位置记忆同步后,完整回归为 162/162 通过;“前舱空间太挤了,把座椅调整到舒服的位置”明确记录 `provider_used=openai:gpt-5.6-sol`,物理目标为 M0/M1/M2/M3=`80/30/35/10%`。报告位于 `artifacts/hil/seat_frontend_hil_20260802_full_gpt56_position_sync.json`;最终独立复核仍为热管理关闭、四轴 0% 且全部 `Position_Valid=1`。 2026-08-02 增加 GPT-5.6-sol 模糊意图与位置记忆同步后,完整回归为 162/162 通过;“前舱空间太挤了,把座椅调整到舒服的位置”明确记录 `provider_used=openai:gpt-5.6-sol`,物理目标为 M0/M1/M2/M3=`80/30/35/10%`。报告位于 `artifacts/hil/seat_frontend_hil_20260802_full_gpt56_position_sync.json`;最终独立复核仍为热管理关闭、四轴 0% 且全部 `Position_Valid=1`。
+15 -3
View File
@@ -71,14 +71,14 @@ spec:
risk_level: low risk_level: low
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: climate.set_outlet_mode - name: climate.set_outlet_mode
description: Direct a zone outlet toward the face or feet. description: Select a whole-cabin vehicle CAN airflow mode; face and feet remain valid zone-level virtual controls.
target_devices: [hvac] target_devices: [hvac]
target_scopes: [driver, passenger, rear_left, rear_right] target_scopes: [driver, passenger, rear_left, rear_right, vehicle]
parameters: parameters:
- name: mode - name: mode
type: string type: string
required: true required: true
allowed_values: [face, feet] allowed_values: [face, face_feet, feet, feet_defrost, defrost, face_feet_defrost, face_defrost]
description: Absolute outlet direction. description: Absolute outlet direction.
risk_level: low risk_level: low
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
@@ -94,3 +94,15 @@ spec:
description: Absolute front defrost state. description: Absolute front defrost state.
risk_level: medium risk_level: medium
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: climate.set_control_mode
description: Select manual or automatic HVAC control through the verified vehicle CAN command.
target_devices: [hvac]
target_scopes: [vehicle]
parameters:
- name: mode
type: string
required: true
allowed_values: [manual, auto]
description: Absolute HVAC control strategy.
risk_level: low
confirmation_policy: on_ambiguity
@@ -63,7 +63,30 @@
"type": "object", "type": "object",
"additionalProperties": false, "additionalProperties": false,
"required": ["mode"], "required": ["mode"],
"properties": {"mode": {"enum": ["face", "feet"]}} "properties": {
"mode": {
"enum": [
"face",
"face_feet",
"feet",
"feet_defrost",
"defrost",
"face_feet_defrost",
"face_defrost"
]
}
}
}
}
},
{
"properties": {
"capability": {"const": "climate.set_control_mode"},
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["mode"],
"properties": {"mode": {"enum": ["manual", "auto"]}}
} }
} }
} }
+12
View File
@@ -7,6 +7,18 @@ metadata:
spec: spec:
domain: roof domain: roof
capabilities: capabilities:
- name: roof.set_shade_motion
description: Open, close, half-open, or stop the panoramic roof shade through vehicle CAN.
target_devices: [roof_shade]
target_scopes: [vehicle]
parameters:
- name: motion
type: string
required: true
allowed_values: [open, close, half, stop]
description: Explicit shade motion command.
risk_level: high
confirmation_policy: on_ambiguity
- name: roof.set_shade_position - name: roof.set_shade_position
description: Set the rear panoramic roof shade to an absolute position, 0 closed and 100 open. description: Set the rear panoramic roof shade to an absolute position, 0 closed and 100 open.
target_devices: [roof_shade] target_devices: [roof_shade]
@@ -23,6 +23,17 @@
} }
} }
}, },
{
"properties": {
"capability": {"const": "roof.set_shade_motion"},
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["motion"],
"properties": {"motion": {"enum": ["open", "close", "half", "stop"]}}
}
}
},
{ {
"properties": { "properties": {
"capability": {"const": "roof.set_transmittance"}, "capability": {"const": "roof.set_transmittance"},
@@ -89,6 +89,18 @@ messages:
- {name: Current_Raw, start_bit: 16, bit_length: 16} - {name: Current_Raw, start_bit: 16, bit_length: 16}
- {name: Current_Baseline_Raw, start_bit: 32, bit_length: 16} - {name: Current_Baseline_Raw, start_bit: 32, bit_length: 16}
- {name: Stall_Current_Delta_Threshold_Raw, start_bit: 48, bit_length: 16} - {name: Stall_Current_Delta_Threshold_Raw, start_bit: 48, bit_length: 16}
- name: T2G_Seat_Dynamic_Mode_Status_306
frame_id: 0x306
role: feedback
minimum_payload_bytes: 8
signals:
- {name: Dynamic_Mode_ID, start_bit: 0, bit_length: 8}
- {name: Dynamic_State, start_bit: 8, bit_length: 8}
- {name: Dynamic_Phase, start_bit: 16, bit_length: 8}
- {name: Dynamic_Active_Mask, start_bit: 24, bit_length: 4}
- {name: Dynamic_Fault_Mask, start_bit: 28, bit_length: 4}
- {name: Dynamic_Cycle_Index, start_bit: 32, bit_length: 16}
- {name: Dynamic_Phase_Elapsed_Ms, start_bit: 48, bit_length: 16}
- name: T2G_Seat_Thermal_Current_Diagnostic_305 - name: T2G_Seat_Thermal_Current_Diagnostic_305
frame_id: 0x305 frame_id: 0x305
role: feedback role: feedback
+21 -1
View File
@@ -3,7 +3,7 @@ kind: SignalMapping
metadata: metadata:
id: seat-t2g-bench-hardware-mapping id: seat-t2g-bench-hardware-mapping
name: Verified T2G seat bench mapping name: Verified T2G seat bench mapping
version: 3.0.0 version: 3.1.0
description: Front-passenger-only mapping implemented by the unified Seat_Controller_demo IAR firmware. description: Front-passenger-only mapping implemented by the unified Seat_Controller_demo IAR firmware.
spec: spec:
domain: seat domain: seat
@@ -80,3 +80,23 @@ spec:
feedback_signal: Value_X100 feedback_signal: Value_X100
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
unit: percent unit: percent
- capability: seat.start_dynamic_mode
parameter: mode_id
target_zone: front_right
physical_target_alias: t2g_primary_seat
compiler_handler: compile_t2g_dynamic_mode
bus: seat_can
message: T2G_Seat_Move_Command_300
signal: Param_A
feedback_message: T2G_Seat_Dynamic_Mode_Status_306
feedback_signal: Dynamic_Mode_ID
unit: enum
- capability: seat.stop_dynamic_mode
target_zone: front_right
physical_target_alias: t2g_primary_seat
compiler_handler: compile_t2g_dynamic_mode
bus: seat_can
message: T2G_Seat_Move_Command_300
feedback_message: T2G_Seat_Dynamic_Mode_Status_306
feedback_signal: Dynamic_State
unit: enum
+39 -3
View File
@@ -3,7 +3,7 @@ kind: CapabilityCatalog
metadata: metadata:
id: seat-capabilities id: seat-capabilities
name: Seat capabilities name: Seat capabilities
version: 1.3.0 version: 1.4.0
spec: spec:
domain: seat domain: seat
capabilities: capabilities:
@@ -27,11 +27,13 @@ spec:
- name: level - name: level
type: integer type: integer
required: false required: false
description: Desired 1-3 comfort level; simulated until a level command signal is supplied. description: Desired 1-5 comfort level; levels 4-5 require the vehicle native-CAN backend.
allowed_values: allowed_values:
- 1 - 1
- 2 - 2
- 3 - 3
- 4
- 5
risk_level: medium risk_level: medium
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: seat.set_heating - name: seat.set_heating
@@ -53,11 +55,13 @@ spec:
- name: level - name: level
type: integer type: integer
required: false required: false
description: Desired 1-3 comfort level; simulated until a level command signal is supplied. description: Desired 1-5 comfort level; levels 4-5 require the vehicle native-CAN backend.
allowed_values: allowed_values:
- 1 - 1
- 2 - 2
- 3 - 3
- 4
- 5
risk_level: medium risk_level: medium
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: seat.set_longitudinal_position - name: seat.set_longitudinal_position
@@ -198,6 +202,38 @@ spec:
description: Absolute normalized leg-rest extension target. description: Absolute normalized leg-rest extension target.
risk_level: high risk_level: high
confirmation_policy: on_ambiguity confirmation_policy: on_ambiguity
- name: seat.start_dynamic_mode
description: >-
Start a firmware-owned cyclic four-axis trajectory on the front-passenger seat.
Mode 1 is the short-travel rocking-horse pitch cycle; mode 2 is the long-travel
dynamic cradle. Both use four-axis target rendezvous, repeat until explicitly
stopped, and stop automatically when the firmware-enforced ten-minute runtime
limit expires. A target phase faults safely if it cannot finish within its limit.
target_devices:
- seat_motion_controller
target_scopes:
- passenger
parameters:
- name: mode_id
type: integer
required: true
description: Native dynamic mode identifier implemented by Seat_Controller_demo.
allowed_values:
- 1
- 2
risk_level: high
confirmation_policy: on_ambiguity
- name: seat.stop_dynamic_mode
description: >-
Stop whichever firmware-owned dynamic trajectory is currently running on the
front-passenger seat. Use for natural-language stop, close, cancel, or end requests.
target_devices:
- seat_motion_controller
target_scopes:
- passenger
parameters: []
risk_level: low
confirmation_policy: never
- name: seat.save_posture_preset - name: seat.save_posture_preset
description: >- description: >-
Save the current five-axis posture of the front-passenger seat as a conversation-owned named preset. Save the current five-axis posture of the front-passenger seat as a conversation-owned named preset.
@@ -22,7 +22,7 @@
"required": ["state"], "required": ["state"],
"properties": { "properties": {
"state": {"type": "string", "enum": ["on", "off"]}, "state": {"type": "string", "enum": ["on", "off"]},
"level": {"type": "integer", "minimum": 1, "maximum": 3} "level": {"type": "integer", "minimum": 1, "maximum": 5}
} }
} }
} }
@@ -37,7 +37,7 @@
"required": ["state"], "required": ["state"],
"properties": { "properties": {
"state": {"type": "string", "enum": ["on", "off"]}, "state": {"type": "string", "enum": ["on", "off"]},
"level": {"type": "integer", "minimum": 1, "maximum": 3} "level": {"type": "integer", "minimum": 1, "maximum": 5}
} }
} }
} }
@@ -65,6 +65,31 @@
} }
} }
} }
},
{
"type": "object",
"properties": {
"capability": {"const": "seat.start_dynamic_mode"},
"parameters": {
"type": "object",
"additionalProperties": false,
"required": ["mode_id"],
"properties": {
"mode_id": {"type": "integer", "enum": [1, 2]}
}
}
}
},
{
"type": "object",
"properties": {
"capability": {"const": "seat.stop_dynamic_mode"},
"parameters": {
"type": "object",
"additionalProperties": false,
"maxProperties": 0
}
}
} }
] ]
} }
+44 -1
View File
@@ -24,7 +24,7 @@ MOTION_STATUS_MESSAGES = {
class SeatCompiler: class SeatCompiler:
version = "1.1.0" version = "1.2.0"
def __init__(self, mapping_path: Path): def __init__(self, mapping_path: Path):
self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8")) self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8"))
@@ -42,6 +42,49 @@ class SeatCompiler:
motion_index = 0 motion_index = 0
for operation in function_config["spec"]["operations"]: for operation in function_config["spec"]["operations"]:
if operation["capability"] == "seat.start_dynamic_mode":
mode_id = int(operation["parameters"]["mode_id"])
if mode_id not in {1, 2}:
raise ValueError("dynamic seat mode_id must be 1 or 2")
motion_commands.append(
{
"id": f"dynamic_{operation['id']}",
"operation_ref": operation["id"],
"bus": "seat_can",
"message": "Seat_Dynamic_Mode_Virtual",
"signals": {"Dynamic_Mode_ID": mode_id},
"send_offset_ms": motion_index * 50,
"completion": {
"message": "Seat_Dynamic_Mode_Status_Virtual",
"signal": "Dynamic_Mode_ID",
"operator": "eq",
"value": mode_id,
"timeout_ms": int(operation.get("timeout_ms", 5000)),
},
}
)
motion_index += 1
continue
if operation["capability"] == "seat.stop_dynamic_mode":
motion_commands.append(
{
"id": f"dynamic_stop_{operation['id']}",
"operation_ref": operation["id"],
"bus": "seat_can",
"message": "Seat_Dynamic_Mode_Stop_Virtual",
"signals": {"Dynamic_State": 0},
"send_offset_ms": motion_index * 50,
"completion": {
"message": "Seat_Dynamic_Mode_Status_Virtual",
"signal": "Dynamic_State",
"operator": "eq",
"value": 0,
"timeout_ms": int(operation.get("timeout_ms", 5000)),
},
}
)
motion_index += 1
continue
mapping = self._mapping_for(operation["capability"], operation["target"]["zone"]) mapping = self._mapping_for(operation["capability"], operation["target"]["zone"])
handler = mapping["compiler_handler"] handler = mapping["compiler_handler"]
if handler == "compile_binary_control": if handler == "compile_binary_control":
+65
View File
@@ -58,6 +58,15 @@ class VirtualSeatExecutor:
operations = [operation_by_id[item] for item in operation_ids] operations = [operation_by_id[item] for item in operation_ids]
if command["message"] == "Seat_Thermal_Control_42A": if command["message"] == "Seat_Thermal_Control_42A":
results.extend(self._execute_thermal(command, operations)) results.extend(self._execute_thermal(command, operations))
elif command["message"] in {
"Seat_Dynamic_Mode_Virtual",
"Seat_Dynamic_Mode_Stop_Virtual",
}:
if len(operations) != 1:
raise ValueError(
"A dynamic-mode command must reference exactly one operation"
)
results.append(self._execute_dynamic_mode(command, operations[0]))
else: else:
if len(operations) != 1: if len(operations) != 1:
raise ValueError("A motion command must reference exactly one operation") raise ValueError("A motion command must reference exactly one operation")
@@ -134,6 +143,62 @@ class VirtualSeatExecutor:
) )
return results return results
@staticmethod
def _execute_dynamic_mode(
command: dict[str, Any], operation: dict[str, Any]
) -> dict[str, Any]:
if operation["capability"] == "seat.stop_dynamic_mode":
return {
"ref": operation["id"],
"domain": "seat",
"status": "succeeded",
"message": "虚拟控制器已停止动态座椅模式",
"telemetry": {
"dynamic_mode_id": 0,
"dynamic_mode_name": "",
"dynamic_state": "idle",
"repeat": "none",
"max_runtime_ms": 600_000,
"command_frame": {
"frame_id": "virtual",
"data_hex": "dynamic-stop",
},
"feedback_frame": {
"frame_id": "virtual",
"data_hex": "dynamic-idle",
},
"simulated": True,
},
}
mode_id = int(operation["parameters"]["mode_id"])
cycle_ms = None
mode_name = "木马模式" if mode_id == 1 else "动态摇篮模式"
return {
"ref": operation["id"],
"domain": "seat",
"status": "succeeded",
"message": (
f"虚拟控制器已启动{mode_name},持续循环至停止指令或十分钟上限"
),
"telemetry": {
"dynamic_mode_id": mode_id,
"dynamic_mode_name": mode_name,
"dynamic_state": "running",
"repeat": "until_stopped_or_timeout",
"max_runtime_ms": 600_000,
"cycle_duration_ms": cycle_ms,
"command_frame": {
"frame_id": "virtual",
"data_hex": f"dynamic-mode-{mode_id}",
},
"feedback_frame": {
"frame_id": "virtual",
"data_hex": f"dynamic-running-{mode_id}",
},
"simulated": True,
},
}
def _execute_motion( def _execute_motion(
self, self,
command: dict[str, Any], command: dict[str, Any],
+69
View File
@@ -20,6 +20,9 @@ MOTION_CAPABILITIES = {
"legrest_height": "seat.set_legrest_height", "legrest_height": "seat.set_legrest_height",
"legrest_extension": "seat.set_legrest_extension", "legrest_extension": "seat.set_legrest_extension",
} }
DYNAMIC_MODE_CAPABILITY = "seat.start_dynamic_mode"
DYNAMIC_MODE_STOP_CAPABILITY = "seat.stop_dynamic_mode"
DYNAMIC_MODE_MAX_RUNTIME_MS = 600_000
def _now() -> str: def _now() -> str:
@@ -31,6 +34,36 @@ def build_intent_ir(
) -> dict[str, Any]: ) -> dict[str, Any]:
requests = [] requests = []
for action in parsed["actions"]: for action in parsed["actions"]:
if action.get("action_type") == "dynamic_mode_stop":
requests.append(
{
"domain": "seat",
"goal": f"Stop the active dynamic seat mode on {action['target']}",
"target": TARGETS[action["target"]],
"parameters": {"capability": DYNAMIC_MODE_STOP_CAPABILITY},
"source_span": utterance,
}
)
continue
if action.get("action_type") == "dynamic_mode":
requests.append(
{
"domain": "seat",
"goal": (
f"Start bounded cyclic dynamic seat mode {action['mode_name']} "
f"on {action['target']}"
),
"target": TARGETS[action["target"]],
"parameters": {
"capability": DYNAMIC_MODE_CAPABILITY,
"mode_id": action["mode_id"],
"repeat": "until_stopped_or_timeout",
"max_runtime_ms": DYNAMIC_MODE_MAX_RUNTIME_MS,
},
"source_span": utterance,
}
)
continue
if action.get("action_type") == "position": if action.get("action_type") == "position":
parameters = { parameters = {
"axis": action["axis"], "axis": action["axis"],
@@ -158,6 +191,42 @@ def build_function_config(
"The supplied matrix is partial; this configuration is eligible for virtual execution only." "The supplied matrix is partial; this configuration is eligible for virtual execution only."
] ]
for index, action in enumerate(parsed["actions"], start=1): for index, action in enumerate(parsed["actions"], start=1):
if action.get("action_type") == "dynamic_mode_stop":
operation_id = f"{action['target']}_dynamic_mode_stop"
operations.append(
{
"id": operation_id,
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target": TARGETS[action["target"]],
"parameters": {},
"timeout_ms": 5000,
"failure_policy": "abort",
}
)
assumptions.append(
f"Operation {operation_id} stops the firmware-owned loop and "
"requires 0x306 to return IDLE."
)
continue
if action.get("action_type") == "dynamic_mode":
operation_id = f"{action['target']}_dynamic_mode_{action['mode_id']}"
operations.append(
{
"id": operation_id,
"capability": DYNAMIC_MODE_CAPABILITY,
"target": TARGETS[action["target"]],
"parameters": {"mode_id": int(action["mode_id"])},
"timeout_ms": 5000,
"failure_policy": "abort",
}
)
assumptions.append(
f"Operation {operation_id} starts a firmware-owned cyclic loop with "
f"a {DYNAMIC_MODE_MAX_RUNTIME_MS} ms safety cap; "
"completion means that the controller acknowledged the mode and "
"reported it through 0x306, not that the loop terminated."
)
continue
if action.get("action_type") == "position": if action.get("action_type") == "position":
operation_id = ( operation_id = (
f"{action['target']}_{action['axis']}_{action['position_pct']}pct" f"{action['target']}_{action['axis']}_{action['position_pct']}pct"
@@ -16,7 +16,7 @@ STATE_VALUES = {"on": 1, "off": 2}
class T2GSeatCompiler: class T2GSeatCompiler:
"""Deterministic compiler for the HIL-verified T2G seat bench protocol.""" """Deterministic compiler for the HIL-verified T2G seat bench protocol."""
version = "2.0.0" version = "2.1.0"
def __init__(self, mapping_path: Path, calibration_path: Path): def __init__(self, mapping_path: Path, calibration_path: Path):
self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8")) self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8"))
@@ -41,13 +41,29 @@ class T2GSeatCompiler:
for item in operations for item in operations
if item["capability"] in {"seat.set_ventilation", "seat.set_heating"} if item["capability"] in {"seat.set_ventilation", "seat.set_heating"}
] ]
motion_operations = [item for item in operations if item not in thermal_operations] dynamic_operations = [
item
for item in operations
if item["capability"] in {
"seat.start_dynamic_mode",
"seat.stop_dynamic_mode",
}
]
motion_operations = [
item
for item in operations
if item not in thermal_operations and item not in dynamic_operations
]
commands: list[dict[str, Any]] = [] commands: list[dict[str, Any]] = []
if thermal_operations: if thermal_operations:
commands.extend( commands.extend(
self._compile_thermal(thermal_operations, actuator_state or {}) self._compile_thermal(thermal_operations, actuator_state or {})
) )
if dynamic_operations:
commands.extend(
self._compile_dynamic_mode(dynamic_operations, len(commands))
)
commands.extend(self._compile_motion(motion_operations, len(commands))) commands.extend(self._compile_motion(motion_operations, len(commands)))
canonical = json.dumps( canonical = json.dumps(
@@ -257,6 +273,55 @@ class T2GSeatCompiler:
commands.append(command) commands.append(command)
return commands return commands
def _compile_dynamic_mode(
self,
operations: list[dict[str, Any]],
command_offset: int,
) -> list[dict[str, Any]]:
if len(operations) != 1:
raise ValueError("one command may start or stop only one dynamic seat mode")
operation = operations[0]
self._require_supported(operation)
stopping = operation["capability"] == "seat.stop_dynamic_mode"
mode_id = 0 if stopping else int(operation["parameters"]["mode_id"])
if not stopping and mode_id not in {1, 2}:
raise ValueError("T2G dynamic mode_id must be 1 or 2")
return [
{
"id": f"t2g_dynamic_{operation['id']}",
"operation_ref": operation["id"],
"bus": "seat_can",
"message": "T2G_Seat_Move_Command_300",
"signals": {
"Direction": 0,
"Motor_Selector": 0,
"Opcode": int(
self.protocol[
"stop_dynamic_mode_opcode"
if stopping
else "start_dynamic_mode_opcode"
]
),
"Param_A": mode_id,
"Param_B": 0,
"Protocol_Version": int(self.protocol["version"]),
"Sequence": 0,
},
"send_offset_ms": (
command_offset * int(self.protocol["inter_axis_settle_ms"])
),
"completion": {
"message": "T2G_Seat_Dynamic_Mode_Status_306",
"signal": "Dynamic_State" if stopping else "Dynamic_Mode_ID",
"operator": "eq",
"value": 0 if stopping else mode_id,
"state_signal": "Dynamic_State",
"accepted_states": [0] if stopping else [1, 2],
"timeout_ms": max(int(operation.get("timeout_ms", 0)), 5000),
},
}
]
def _compile_timed_motion( def _compile_timed_motion(
self, self,
operation: dict[str, Any], operation: dict[str, Any],
@@ -156,7 +156,11 @@ class HardwareSeatExecutor:
} }
results: list[dict[str, Any]] = [] results: list[dict[str, Any]] = []
commands = command_plan["spec"]["commands"] commands = command_plan["spec"]["commands"]
has_motion = any(item["message"] == "T2G_Seat_Move_Command_300" for item in commands) has_motion = any(
item["message"] == "T2G_Seat_Move_Command_300"
and int(item.get("signals", {}).get("Opcode", 0)) not in {0x09, 0x0F}
for item in commands
)
bus: CanBus | None = None bus: CanBus | None = None
motion_armed = False motion_armed = False
position_sync_required = False position_sync_required = False
@@ -249,7 +253,12 @@ class HardwareSeatExecutor:
elif command["message"] == "T2G_Seat_Thermal_Control_29E": elif command["message"] == "T2G_Seat_Thermal_Control_29E":
results.extend(self._execute_thermal(bus, command, referenced)) results.extend(self._execute_thermal(bus, command, referenced))
elif command["message"] == "T2G_Seat_Move_Command_300": elif command["message"] == "T2G_Seat_Move_Command_300":
results.extend(self._execute_motion(bus, command, referenced)) if int(command["signals"]["Opcode"]) in {0x0E, 0x0F}:
results.extend(
self._execute_dynamic_mode(bus, command, referenced)
)
else:
results.extend(self._execute_motion(bus, command, referenced))
else: else:
raise HardwareExecutionError( raise HardwareExecutionError(
f"hardware executor refuses unknown message {command['message']}" f"hardware executor refuses unknown message {command['message']}"
@@ -697,6 +706,94 @@ class HardwareSeatExecutor:
) )
return output return output
def _execute_dynamic_mode(
self,
bus: CanBus,
command: dict[str, Any],
operations: list[dict[str, Any]],
) -> list[dict[str, Any]]:
if len(operations) != 1:
raise HardwareExecutionError(
"a T2G dynamic-mode frame must reference exactly one operation"
)
operation = operations[0]
stopping = operation["capability"] == "seat.stop_dynamic_mode"
mode_id = 0 if stopping else int(operation["parameters"]["mode_id"])
if not stopping and mode_id not in {1, 2}:
raise HardwareExecutionError("T2G dynamic mode_id must be 1 or 2")
signals = dict(command["signals"])
sequence = self._next_sequence()
signals["Sequence"] = sequence
command_frame = self._send_encoded(bus, command["message"], signals)
ack_values, ack_frame = self._wait_ack(
bus, sequence, 0x0F if stopping else 0x0E
)
completion = command["completion"]
def matches(values: dict[str, int], _: CanFrame) -> bool:
if stopping:
return (
values["Dynamic_Mode_ID"] == 0
and values["Dynamic_State"] == 0
)
return (
values["Dynamic_Mode_ID"] == mode_id
and values["Dynamic_State"] in set(
completion.get("accepted_states", [1, 2])
)
)
status_values, status_frame = self._receive_until(
bus,
0x306,
int(completion["timeout_ms"]),
matches,
)
mode_name = (
""
if stopping
else "木马模式"
if mode_id == 1
else "动态摇篮模式"
)
return [
{
"ref": operation["id"],
"domain": "seat",
"status": "succeeded",
"message": (
"physical 0x301/0x306 confirmed the dynamic seat mode is stopped"
if stopping
else (
f"physical 0x301/0x306 confirmed {mode_name} is active; "
"firmware will repeat it until stopped or the ten-minute cap"
)
),
"telemetry": {
"dynamic_mode_id": mode_id,
"dynamic_mode_name": mode_name,
"dynamic_state": status_values["Dynamic_State"],
"dynamic_phase": status_values["Dynamic_Phase"],
"dynamic_cycle_index": status_values["Dynamic_Cycle_Index"],
"dynamic_phase_elapsed_ms": status_values[
"Dynamic_Phase_Elapsed_Ms"
],
"repeat": (
"none" if stopping else "until_stopped_or_timeout"
),
"max_runtime_ms": 600_000,
"command_frame": command_frame.public(),
"ack_frame": ack_frame.public(),
"ack_values": ack_values,
"feedback_frame": status_frame.public(),
"hardware_feedback": True,
"simulated": False,
},
}
]
def _execute_motion( def _execute_motion(
self, self,
bus: CanBus, bus: CanBus,
+15
View File
@@ -82,6 +82,21 @@ def validate_semantics(
"message": "当前所有座椅能力仅支持前排副驾。", "message": "当前所有座椅能力仅支持前排副驾。",
} }
) )
if action.get("action_type") == "dynamic_mode_stop":
has_motion = True
continue
if action.get("action_type") == "dynamic_mode":
has_motion = True
if action.get("mode_id") not in {1, 2}:
issues.append(
{
"severity": "error",
"code": "UNSUPPORTED_DYNAMIC_MODE",
"path": f"{path}.mode_id",
"message": "动态座椅模式编号必须是 1 或 2。",
}
)
continue
if action.get("action_type") == "position": if action.get("action_type") == "position":
has_motion = True has_motion = True
axis = action.get("axis") axis = action.get("axis")
@@ -32,6 +32,41 @@ class SeatServiceIntegrationTests(unittest.TestCase):
for event in result["trace_events"]: for event in result["trace_events"]:
self.assertEqual(self.service.contracts.validate(event), []) self.assertEqual(self.service.contracts.validate(event), [])
def test_dynamic_mode_runs_until_explicit_natural_language_stop(self) -> None:
conversation_id = "conv-dynamic-stop"
started = self.service.process(
"\u5f00\u542f\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
)
stopped = self.service.process(
"\u5173\u95ed\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
provider="local",
conversation_id=conversation_id,
vehicle_id="denza_n7",
)
self.assertEqual(started["status"], "succeeded")
self.assertEqual(
started["actuator_state"]["passenger"]["dynamic_mode"],
{
"mode_id": 2,
"name": "\u52a8\u6001\u6447\u7bee\u6a21\u5f0f",
"state": "running",
"repeat": "until_stopped_or_timeout",
"max_runtime_ms": 600_000,
},
)
self.assertEqual(stopped["status"], "succeeded")
operation = stopped["function_config"]["spec"]["operations"][0]
self.assertEqual(operation["capability"], "seat.stop_dynamic_mode")
self.assertEqual(operation["parameters"], {})
self.assertEqual(
stopped["actuator_state"]["passenger"]["dynamic_mode"]["state"],
"idle",
)
def test_gear_request_is_preserved_and_simulated(self) -> None: def test_gear_request_is_preserved_and_simulated(self) -> None:
result = self.service.process("副驾座椅加热开到三档", provider="local") result = self.service.process("副驾座椅加热开到三档", provider="local")
+145 -2
View File
@@ -16,6 +16,7 @@ if str(SEAT_SOURCE) not in sys.path:
from cognitive_agent.infrastructure.can.gateway import CanFrame, CanGatewayError # noqa: E402 from cognitive_agent.infrastructure.can.gateway import CanFrame, CanGatewayError # noqa: E402
from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402 from cognitive_agent.application.seat_service import SeatDemoService # noqa: E402
from cognitive_agent.runtime.contracts import ContractValidator # noqa: E402
from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402 from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareSafetyError, HardwareSafetyError,
SeatHardwareSettings, SeatHardwareSettings,
@@ -242,6 +243,34 @@ class ReactiveFakeBus:
}, },
) )
) )
if opcode == 14:
self.received.append(
self._frame(
"T2G_Seat_Dynamic_Mode_Status_306",
{
"Dynamic_Mode_ID": command["Param_A"],
"Dynamic_State": 2,
"Dynamic_Phase": 2 if command["Param_A"] == 1 else 5,
"Dynamic_Active_Mask": 0x0F,
"Dynamic_Cycle_Index": 0,
"Dynamic_Phase_Elapsed_Ms": 100,
},
)
)
if opcode == 15:
self.received.append(
self._frame(
"T2G_Seat_Dynamic_Mode_Status_306",
{
"Dynamic_Mode_ID": 0,
"Dynamic_State": 0,
"Dynamic_Phase": 0,
"Dynamic_Active_Mask": 0,
"Dynamic_Cycle_Index": 0,
"Dynamic_Phase_Elapsed_Ms": 0,
},
)
)
if opcode == 0x0B and self.complete_motion: if opcode == 0x0B and self.complete_motion:
motor = command["Motor_Selector"] motor = command["Motor_Selector"]
self.position_valid = True self.position_valid = True
@@ -447,7 +476,7 @@ class ProtocolSignatureFakeBus:
self.received: deque[CanFrame] = deque() self.received: deque[CanFrame] = deque()
self.closed = False self.closed = False
if signature: if signature:
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 60"))) self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 D0")))
for motor in range(4): for motor in range(4):
self.received.append( self.received.append(
CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0])) CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0]))
@@ -464,6 +493,7 @@ class ProtocolSignatureFakeBus:
CanFrame(0x304, bytes([motor, 3, 0, 0, 0, 0, 0xF4, 0x01])) CanFrame(0x304, bytes([motor, 3, 0, 0, 0, 0, 0xF4, 0x01]))
) )
self.received.append(CanFrame(0x305, bytes(8))) self.received.append(CanFrame(0x305, bytes(8)))
self.received.append(CanFrame(0x306, bytes(8)))
self.received.append(CanFrame(0x4FA, bytes.fromhex("09 00 00 00 00 00 00 00"))) self.received.append(CanFrame(0x4FA, bytes.fromhex("09 00 00 00 00 00 00 00")))
def send(self, arbitration_id: int, data: bytes) -> CanFrame: def send(self, arbitration_id: int, data: bytes) -> CanFrame:
@@ -485,6 +515,29 @@ class FailingProtocolSignatureBus(ProtocolSignatureFakeBus):
raise RuntimeError("CAN receive failed during passive protocol probe") raise RuntimeError("CAN receive failed during passive protocol probe")
class LegacyProtocolSignatureFakeBus(ProtocolSignatureFakeBus):
def __init__(self):
super().__init__(False)
self.received.append(CanFrame(0x301, bytes.fromhex("10 00 00 00 00 00 F0 00")))
for motor in range(4):
self.received.append(
CanFrame(0x302, bytes([motor, 0, 0x08, 0, 0, 0, 0, 0]))
)
for motor, maximum in enumerate((480, 374, 917, 647)):
self.received.append(
CanFrame(
0x303,
bytes([motor, 0, 0, 0, maximum & 0xFF, maximum >> 8, 0, 0]),
)
)
for motor in range(4):
self.received.append(
CanFrame(0x304, bytes([motor, 3, 0, 0, 0, 0, 0xF4, 0x01]))
)
self.received.append(CanFrame(0x305, bytes(8)))
self.received.append(CanFrame(0x4FA, bytes(8)))
class FakeProgrammer: class FakeProgrammer:
def __init__(self): def __init__(self):
self.calls = 0 self.calls = 0
@@ -511,6 +564,7 @@ class T2GHardwareTests(unittest.TestCase):
ROOT / "domains" / "seat" / "can" / "signal-mapping.hardware.yaml", ROOT / "domains" / "seat" / "can" / "signal-mapping.hardware.yaml",
ROOT / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml", ROOT / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
) )
self.contracts = ContractValidator(ROOT)
def test_compiler_emits_verified_heating_payload(self) -> None: def test_compiler_emits_verified_heating_payload(self) -> None:
config = function_config( config = function_config(
@@ -569,6 +623,34 @@ class T2GHardwareTests(unittest.TestCase):
self.assertEqual(fallback["signals"]["Param_B"], 900) self.assertEqual(fallback["signals"]["Param_B"], 900)
self.assertEqual(fallback["completion"]["signal"], "Task_State") self.assertEqual(fallback["completion"]["signal"], "Task_State")
def test_compiler_emits_native_dynamic_mode_command(self) -> None:
config = function_config(
operation("seat.start_dynamic_mode", {"mode_id": 1})
)
plan = self.compiler.compile(config, {})
command = plan["spec"]["commands"][0]
frame = self.matrix.encode(command["message"], command["signals"])
self.assertEqual(frame["frame_id"], "0x300")
self.assertEqual(frame["data_hex"], "0E 10 00 00 01 00 00 00")
self.assertEqual(command["completion"]["message"], "T2G_Seat_Dynamic_Mode_Status_306")
self.assertEqual(command["completion"]["accepted_states"], [1, 2])
self.assertEqual(self.contracts.validate(plan), [])
def test_compiler_emits_native_dynamic_mode_stop_command(self) -> None:
config = function_config(operation("seat.stop_dynamic_mode", {}))
plan = self.compiler.compile(config, {})
command = plan["spec"]["commands"][0]
frame = self.matrix.encode(command["message"], command["signals"])
self.assertEqual(frame["frame_id"], "0x300")
self.assertEqual(frame["data_hex"], "0F 10 00 00 00 00 00 00")
self.assertEqual(command["completion"]["signal"], "Dynamic_State")
self.assertEqual(command["completion"]["accepted_states"], [0])
self.assertEqual(self.contracts.validate(plan), [])
def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None: def test_compiler_maps_large_relative_backrest_request_to_absolute_target(self) -> None:
config = function_config( config = function_config(
operation( operation(
@@ -696,6 +778,46 @@ class T2GHardwareTests(unittest.TestCase):
self.assertEqual([item.data[0] for item in bus.sent], [9, 1]) self.assertEqual([item.data[0] for item in bus.sent], [9, 1])
self.assertTrue(bus.closed) self.assertTrue(bus.closed)
def test_executor_waits_for_native_dynamic_mode_feedback(self) -> None:
config = function_config(
operation("seat.start_dynamic_mode", {"mode_id": 2})
)
plan = self.compiler.compile(config, {})
bus = ReactiveFakeBus(self.matrix)
report = HardwareSeatExecutor(
self.matrix, lambda: bus, ack_timeout_ms=20
).execute(plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(telemetry["dynamic_mode_id"], 2)
self.assertEqual(telemetry["dynamic_state"], 2)
self.assertEqual(telemetry["dynamic_phase"], 5)
self.assertEqual([item.data[0] for item in bus.sent], [9, 14])
self.assertTrue(bus.closed)
def test_executor_stops_dynamic_mode_without_homing_or_pre_stop(self) -> None:
config = function_config(operation("seat.stop_dynamic_mode", {}))
plan = self.compiler.compile(config, {})
bus = ReactiveFakeBus(self.matrix)
report = HardwareSeatExecutor(
self.matrix,
lambda: bus,
ack_timeout_ms=20,
synchronize_position_before_first_motion=True,
position_sync_state={"synchronized": False},
).execute(plan, config)
self.assertEqual(report["spec"]["status"], "succeeded")
telemetry = report["spec"]["results"][0]["telemetry"]
self.assertEqual(telemetry["dynamic_mode_id"], 0)
self.assertEqual(telemetry["dynamic_state"], 0)
self.assertEqual(telemetry["repeat"], "none")
self.assertEqual([item.data[0] for item in bus.sent], [15])
self.assertTrue(bus.closed)
def test_firmware_replacement_forces_four_axis_position_memory_sync(self) -> None: def test_firmware_replacement_forces_four_axis_position_memory_sync(self) -> None:
config = function_config( config = function_config(
operation("seat.set_backrest_recline", {"position_pct": 50}) operation("seat.set_backrest_recline", {"position_pct": 50})
@@ -1453,6 +1575,9 @@ class T2GHardwareTests(unittest.TestCase):
settings.validate_function_config( settings.validate_function_config(
function_config(operation("seat.set_cushion_tilt", {"position_pct": 40})) function_config(operation("seat.set_cushion_tilt", {"position_pct": 40}))
) )
settings.validate_function_config(
function_config(operation("seat.stop_dynamic_mode", {}))
)
with self.assertRaises(HardwareSafetyError): with self.assertRaises(HardwareSafetyError):
settings.validate_function_config( settings.validate_function_config(
function_config( function_config(
@@ -1559,7 +1684,7 @@ class T2GHardwareTests(unittest.TestCase):
self.assertIs(prepared, bus) self.assertIs(prepared, bus)
self.assertFalse(telemetry["programmed_this_request"]) self.assertFalse(telemetry["programmed_this_request"])
self.assertEqual(telemetry["firmware_revision"], 6) self.assertEqual(telemetry["firmware_revision"], 13)
self.assertEqual(bus.sent, []) self.assertEqual(bus.sent, [])
self.assertEqual(telemetry["max_hall_counts"], [480, 374, 917, 647]) self.assertEqual(telemetry["max_hall_counts"], [480, 374, 917, 647])
self.assertFalse(bus.closed) self.assertFalse(bus.closed)
@@ -1617,6 +1742,24 @@ class T2GHardwareTests(unittest.TestCase):
guard.prepare_bus() guard.prepare_bus()
self.assertTrue(bus.closed) self.assertTrue(bus.closed)
def test_firmware_guard_reports_legacy_revision_and_missing_dynamic_status(self) -> None:
bus = LegacyProtocolSignatureFakeBus()
guard = T2GFirmwareGuard(
lambda: bus,
None,
auto_flash=False,
probe_timeout_ms=20,
)
with self.assertRaises(FirmwareProgrammingError) as raised:
guard.prepare_bus()
message = str(raised.exception)
self.assertIn("firmware_revision=0 (expected 13)", message)
self.assertIn("missing_frame_ids=0x306", message)
self.assertIn("max_hall_counts=480,374,917,647", message)
self.assertTrue(bus.closed)
def test_programmer_command_downloads_and_hands_off_to_running_target(self) -> None: def test_programmer_command_downloads_and_hands_off_to_running_target(self) -> None:
firmware_root = ROOT / "firmware" / "t2g" / "Seat_Controller_demo" firmware_root = ROOT / "firmware" / "t2g" / "Seat_Controller_demo"
output_image = ( output_image = (
@@ -0,0 +1,679 @@
#include "seat_dynamic_mode.h"
#include <stddef.h>
#include <string.h>
#include "motor_control.h"
#include "seat_dynamic_mode_config.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
#define SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES (3u)
#define SEAT_DYNAMIC_ENTRY_TIMEOUT_MS (30000u)
#define SEAT_DYNAMIC_ENTRY_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS (30000u)
#define SEAT_DYNAMIC_CRADLE_RENDEZVOUS_PULSES (12u)
#define SEAT_DYNAMIC_CRADLE_FINAL_DUTY_PERCENT (50u)
typedef enum
{
SEAT_DYNAMIC_CONTROL_TIMED_ARRIVAL = 0u,
SEAT_DYNAMIC_CONTROL_HOLD,
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
} seat_dynamic_control_t;
typedef struct
{
uint8_t phase;
uint32_t durationMs;
uint32_t timeoutMs;
uint16_t targetHall[SEAT_MOVE_MOTOR_COUNT];
seat_dynamic_control_t control;
} seat_dynamic_phase_definition_t;
typedef struct
{
uint8_t modeId;
const uint16_t *entryTargetHall;
const seat_dynamic_phase_definition_t *phases;
uint8_t phaseCount;
const uint8_t *cruiseDutyPercent;
uint16_t rendezvousPulses;
uint8_t finalDutyPercent;
} seat_dynamic_mode_definition_t;
typedef struct
{
uint8_t modeId;
uint8_t state;
uint8_t phase;
uint8_t phaseIndex;
uint8_t faultMask;
uint16_t cycleIndex;
uint16_t internalSequence;
uint32_t modeStartedMs;
uint32_t phaseStartedMs;
uint32_t phaseDurationMs;
uint8_t rendezvousStage;
uint16_t targetHall[SEAT_MOVE_MOTOR_COUNT];
uint16_t stagingTargetHall[SEAT_MOVE_MOTOR_COUNT];
uint16_t finalTargetHall[SEAT_MOVE_MOTOR_COUNT];
} seat_dynamic_context_t;
static const motor_mode_t s_dynamicLogicalToPhysical[SEAT_MOVE_MOTOR_COUNT] =
{
(motor_mode_t)SEAT_MOVE_LOGICAL_M0_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M1_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M2_PHYSICAL_MODE,
(motor_mode_t)SEAT_MOVE_LOGICAL_M3_PHYSICAL_MODE
};
/* A cradle must feel like one coordinated body movement. All four axes move
* toward their staging poses in the same scheduler pass; no axis is held back
* for a late catch-up. M0/M1/M3 use the proven breakaway duty while M2 runs
* at loaded duty. After every axis reaches a point exactly 12 Hall pulses
* before the requested pose, they execute an equal-distance 50% final
* approach. This keeps both the visible start and final arrival coordinated
* while making completion independent of an arbitrary 30-second timer. */
static const uint8_t s_cradleCruiseDuty[SEAT_MOVE_MOTOR_COUNT] =
{
50u, 50u, 90u, 50u
};
static const uint8_t s_horseCruiseDuty[SEAT_MOVE_MOTOR_COUNT] =
{
SEAT_DYNAMIC_HORSE_M0_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M1_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M2_DUTY_PERCENT,
SEAT_DYNAMIC_HORSE_M3_DUTY_PERCENT
};
/* Targets are the N7 Hall coordinates supplied for the connected seat bench.
* They are intentionally stored as pulses rather than rounded percentages. */
static const uint16_t s_rockingHorseEntry[SEAT_MOVE_MOTOR_COUNT] =
{
SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL,
SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL
};
static const seat_dynamic_phase_definition_t s_rockingHorsePhases[] =
{
{
SEAT_DYNAMIC_PHASE_HORSE_FORWARD,
0u,
SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS,
{
SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL,
SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL
},
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
},
{
SEAT_DYNAMIC_PHASE_HORSE_BACKWARD,
0u,
SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS,
{
SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL,
SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL
},
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
}
};
static const uint16_t s_cradleEntry[SEAT_MOVE_MOTOR_COUNT] =
{
391u, 140u, 405u, 404u
};
static const seat_dynamic_phase_definition_t s_cradlePhases[] =
{
{
SEAT_DYNAMIC_PHASE_CRADLE_FORWARD,
0u,
SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS,
{ 246u, 281u, 701u, 494u },
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
},
{
SEAT_DYNAMIC_PHASE_CRADLE_RETURN,
0u,
SEAT_DYNAMIC_CRADLE_PHASE_TIMEOUT_MS,
{ 391u, 140u, 405u, 404u },
SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS
}
};
static const seat_dynamic_mode_definition_t s_dynamicModes[] =
{
{
SEAT_DYNAMIC_MODE_ROCKING_HORSE,
s_rockingHorseEntry,
s_rockingHorsePhases,
(uint8_t)(sizeof(s_rockingHorsePhases) /
sizeof(s_rockingHorsePhases[0])),
s_horseCruiseDuty,
SEAT_DYNAMIC_HORSE_RENDEZVOUS_PULSES,
SEAT_DYNAMIC_HORSE_FINAL_DUTY_PERCENT
},
{
SEAT_DYNAMIC_MODE_CRADLE,
s_cradleEntry,
s_cradlePhases,
(uint8_t)(sizeof(s_cradlePhases) / sizeof(s_cradlePhases[0])),
s_cradleCruiseDuty,
SEAT_DYNAMIC_CRADLE_RENDEZVOUS_PULSES,
SEAT_DYNAMIC_CRADLE_FINAL_DUTY_PERCENT
}
};
static seat_dynamic_context_t s_dynamic;
static bool SeatDynamic_TimeReached(uint32_t now, uint32_t deadline);
static uint32_t SeatDynamic_ElapsedMs(uint32_t now, uint32_t started);
static const seat_dynamic_mode_definition_t *SeatDynamic_Definition(
uint8_t modeId);
static uint8_t SeatDynamic_ActiveMask(void);
static bool SeatDynamic_StatusFailed(const seat_motor_status_t *status);
static bool SeatDynamic_TargetsReached(bool *failed);
static bool SeatDynamic_TargetsWithinTolerance(void);
static bool SeatDynamic_StartEntry(
const seat_dynamic_mode_definition_t *definition,
uint32_t now);
static bool SeatDynamic_StartPhase(
const seat_dynamic_mode_definition_t *definition,
uint8_t phaseIndex,
uint32_t now);
static bool SeatDynamic_StartRendezvousFinal(
const seat_dynamic_mode_definition_t *definition);
static void SeatDynamic_Fail(void);
static void SeatDynamic_Advance(
const seat_dynamic_mode_definition_t *definition,
uint32_t now);
void SeatDynamicMode_Init(void)
{
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.state = SEAT_DYNAMIC_STATE_IDLE;
}
bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence)
{
const seat_dynamic_mode_definition_t *definition =
SeatDynamic_Definition(modeId);
uint8_t motor;
if((definition == NULL) || SeatDynamicMode_IsActive() ||
SeatMotorManager_IsAnyActive())
{
return false;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) ||
!status.positionValid ||
(definition->entryTargetHall[motor] > status.maxHallCount))
{
return false;
}
}
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.modeId = modeId;
s_dynamic.state = SEAT_DYNAMIC_STATE_ENTRY;
s_dynamic.phase = SEAT_DYNAMIC_PHASE_ENTRY;
s_dynamic.internalSequence = commandSequence;
s_dynamic.modeStartedMs = Motor_GetTimeMs();
if(!SeatDynamic_StartEntry(definition, s_dynamic.modeStartedMs))
{
SeatDynamic_Fail();
return false;
}
return true;
}
void SeatDynamicMode_Service(void)
{
const seat_dynamic_mode_definition_t *definition;
uint32_t now;
uint32_t elapsed;
bool failed = false;
bool reached;
const seat_dynamic_phase_definition_t *phase;
if(!SeatDynamicMode_IsActive())
{
return;
}
definition = SeatDynamic_Definition(s_dynamic.modeId);
if(definition == NULL)
{
SeatDynamic_Fail();
return;
}
now = Motor_GetTimeMs();
if(SeatDynamic_TimeReached(
now, s_dynamic.modeStartedMs + SEAT_MOVE_DYNAMIC_MAX_RUNTIME_MS))
{
/* The ten-minute safety cap is owned by the ECU, not by a frontend
* lease or a host-side timer. Reuse the normal stop path so every
* bridge is de-energized and 0x306 returns to all-zero IDLE. */
SeatDynamicMode_Stop((uint16_t)(s_dynamic.internalSequence + 1u));
return;
}
reached = SeatDynamic_TargetsReached(&failed);
if(failed)
{
SeatDynamic_Fail();
return;
}
if(s_dynamic.state == SEAT_DYNAMIC_STATE_ENTRY)
{
if(reached)
{
if(!SeatDynamic_StartPhase(definition, 0u, now))
{
SeatDynamic_Fail();
}
return;
}
if(SeatDynamic_TimeReached(
now, s_dynamic.phaseStartedMs + SEAT_DYNAMIC_ENTRY_TIMEOUT_MS))
{
SeatDynamic_Fail();
}
return;
}
phase = &definition->phases[s_dynamic.phaseIndex];
elapsed = SeatDynamic_ElapsedMs(now, s_dynamic.phaseStartedMs);
if(phase->control == SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS)
{
if(reached)
{
if(s_dynamic.rendezvousStage == 0u)
{
if(!SeatDynamic_StartRendezvousFinal(definition))
{
SeatDynamic_Fail();
}
}
else
{
SeatDynamic_Advance(definition, now);
}
return;
}
if(elapsed > phase->timeoutMs)
{
SeatDynamic_Fail();
}
return;
}
if(elapsed < s_dynamic.phaseDurationMs)
{
return;
}
if(reached)
{
SeatDynamic_Advance(definition, now);
return;
}
if(SeatDynamic_TargetsWithinTolerance())
{
/* At the exact phase boundary an adaptive task may still be active
* while every Hall position is already inside the accepted discrete
* target band. Stop all bridges before entering the hold or opposite
* direction phase; waiting for mathematical equality creates a false
* timeout and can extend the requested phase. */
SeatMotorManager_StopAll(s_dynamic.internalSequence);
SeatDynamic_Advance(definition, now);
return;
}
if(elapsed > (s_dynamic.phaseDurationMs +
SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS))
{
SeatDynamic_Fail();
}
}
void SeatDynamicMode_Stop(uint16_t commandSequence)
{
if(SeatDynamicMode_IsActive())
{
Motor_RequestAbortAllFromIsr(commandSequence);
Motor_ServiceAll();
SeatMotorManager_StopAll(commandSequence);
}
(void)memset(&s_dynamic, 0, sizeof(s_dynamic));
s_dynamic.state = SEAT_DYNAMIC_STATE_IDLE;
}
bool SeatDynamicMode_IsActive(void)
{
return (s_dynamic.state == SEAT_DYNAMIC_STATE_ENTRY) ||
(s_dynamic.state == SEAT_DYNAMIC_STATE_RUNNING);
}
bool SeatDynamicMode_GetStatus(seat_dynamic_mode_status_t *status)
{
uint32_t elapsed;
if(status == NULL)
{
return false;
}
elapsed = SeatDynamicMode_IsActive() ?
SeatDynamic_ElapsedMs(Motor_GetTimeMs(),
s_dynamic.phaseStartedMs) : 0u;
status->modeId = s_dynamic.modeId;
status->state = s_dynamic.state;
status->phase = s_dynamic.phase;
status->activeMask = (uint8_t)
(SeatDynamic_ActiveMask() | (uint8_t)(s_dynamic.faultMask << 4u));
status->cycleIndex = s_dynamic.cycleIndex;
status->phaseElapsedMs = (elapsed > UINT16_MAX) ?
UINT16_MAX : (uint16_t)elapsed;
return true;
}
static bool SeatDynamic_TimeReached(uint32_t now, uint32_t deadline)
{
return ((int32_t)(now - deadline) >= 0);
}
static uint32_t SeatDynamic_ElapsedMs(uint32_t now, uint32_t started)
{
return now - started;
}
static const seat_dynamic_mode_definition_t *SeatDynamic_Definition(
uint8_t modeId)
{
uint8_t index;
for(index = 0u;
index < (uint8_t)(sizeof(s_dynamicModes) / sizeof(s_dynamicModes[0]));
index++)
{
if(s_dynamicModes[index].modeId == modeId)
{
return &s_dynamicModes[index];
}
}
return NULL;
}
static uint8_t SeatDynamic_ActiveMask(void)
{
uint8_t motor;
uint8_t mask = 0u;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
if(SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) && status.active)
{
mask |= (uint8_t)(1u << motor);
}
}
return mask;
}
static bool SeatDynamic_StatusFailed(const seat_motor_status_t *status)
{
return (status->taskState == SEAT_MOTOR_TASK_FAILED) ||
(status->result == SEAT_MOTOR_RESULT_STALL) ||
(status->result == SEAT_MOTOR_RESULT_TIMEOUT) ||
(status->result == SEAT_MOTOR_RESULT_DRIVER_FAULT) ||
(status->result == SEAT_MOTOR_RESULT_INVALID) ||
(status->result == SEAT_MOTOR_RESULT_CALIBRATION_FAILED);
}
static bool SeatDynamic_TargetsReached(bool *failed)
{
uint8_t motor;
bool reached = true;
if(failed == NULL)
{
return false;
}
*failed = false;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint32_t target = s_dynamic.targetHall[motor];
uint32_t delta;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status))
{
s_dynamic.faultMask |= (uint8_t)(1u << motor);
*failed = true;
return false;
}
if(SeatDynamic_StatusFailed(&status))
{
s_dynamic.faultMask |= (uint8_t)(1u << motor);
*failed = true;
return false;
}
delta = (status.hallCount >= target) ?
(status.hallCount - target) : (target - status.hallCount);
if(status.active ||
(delta > SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES))
{
reached = false;
}
}
return reached;
}
static bool SeatDynamic_TargetsWithinTolerance(void)
{
uint8_t motor;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint32_t target = s_dynamic.targetHall[motor];
uint32_t delta;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status) ||
SeatDynamic_StatusFailed(&status))
{
return false;
}
delta = (status.hallCount >= target) ?
(status.hallCount - target) : (target - status.hallCount);
if(delta > SEAT_DYNAMIC_TARGET_TOLERANCE_PULSES)
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartEntry(
const seat_dynamic_mode_definition_t *definition,
uint32_t now)
{
uint8_t motor;
s_dynamic.internalSequence++;
s_dynamic.phaseStartedMs = now;
s_dynamic.phaseDurationMs = SEAT_DYNAMIC_ENTRY_TIMEOUT_MS;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = definition->entryTargetHall[motor];
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
definition->entryTargetHall[motor],
SEAT_DYNAMIC_ENTRY_DUTY_PERCENT,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartPhase(
const seat_dynamic_mode_definition_t *definition,
uint8_t phaseIndex,
uint32_t now)
{
const seat_dynamic_phase_definition_t *phase;
uint8_t motor;
if((definition == NULL) || (phaseIndex >= definition->phaseCount))
{
return false;
}
phase = &definition->phases[phaseIndex];
s_dynamic.state = SEAT_DYNAMIC_STATE_RUNNING;
s_dynamic.phaseIndex = phaseIndex;
s_dynamic.phase = phase->phase;
s_dynamic.phaseStartedMs = now;
s_dynamic.phaseDurationMs = phase->durationMs;
s_dynamic.rendezvousStage = 0u;
s_dynamic.internalSequence++;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = phase->targetHall[motor];
s_dynamic.finalTargetHall[motor] = phase->targetHall[motor];
}
if(phase->control == SEAT_DYNAMIC_CONTROL_HOLD)
{
return true;
}
if(phase->control == SEAT_DYNAMIC_CONTROL_TARGET_RENDEZVOUS)
{
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
seat_motor_status_t status;
uint16_t target = phase->targetHall[motor];
uint16_t current;
if(!SeatMotorManager_GetStatus(s_dynamicLogicalToPhysical[motor],
&status))
{
return false;
}
current = (uint16_t)status.hallCount;
if(target > current + definition->rendezvousPulses)
{
s_dynamic.stagingTargetHall[motor] = (uint16_t)
(target - definition->rendezvousPulses);
}
else if(current > target +
definition->rendezvousPulses)
{
s_dynamic.stagingTargetHall[motor] = (uint16_t)
(target + definition->rendezvousPulses);
}
else
{
s_dynamic.stagingTargetHall[motor] = target;
}
s_dynamic.targetHall[motor] =
s_dynamic.stagingTargetHall[motor];
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
s_dynamic.targetHall[motor],
definition->cruiseDutyPercent[motor],
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallInTimeWithGrace(
s_dynamicLogicalToPhysical[motor],
phase->targetHall[motor],
phase->durationMs,
SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static bool SeatDynamic_StartRendezvousFinal(
const seat_dynamic_mode_definition_t *definition)
{
uint8_t motor;
if(definition == NULL)
{
return false;
}
s_dynamic.internalSequence++;
s_dynamic.rendezvousStage = 2u;
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
s_dynamic.targetHall[motor] = s_dynamic.finalTargetHall[motor];
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{
if(!SeatMotor_MoveAbsoluteHallDuty(
s_dynamicLogicalToPhysical[motor],
s_dynamic.finalTargetHall[motor],
definition->finalDutyPercent,
s_dynamic.internalSequence))
{
return false;
}
}
return true;
}
static void SeatDynamic_Fail(void)
{
uint16_t sequence = (uint16_t)(s_dynamic.internalSequence + 1u);
Motor_RequestAbortAllFromIsr(sequence);
Motor_ServiceAll();
SeatMotorManager_StopAll(sequence);
s_dynamic.internalSequence = sequence;
s_dynamic.state = SEAT_DYNAMIC_STATE_FAULT;
s_dynamic.phaseStartedMs = Motor_GetTimeMs();
}
static void SeatDynamic_Advance(
const seat_dynamic_mode_definition_t *definition,
uint32_t now)
{
uint8_t nextPhase = (uint8_t)(s_dynamic.phaseIndex + 1u);
if(nextPhase >= definition->phaseCount)
{
nextPhase = 0u;
s_dynamic.cycleIndex++;
}
if(!SeatDynamic_StartPhase(definition, nextPhase, now))
{
SeatDynamic_Fail();
}
}
@@ -0,0 +1,49 @@
#ifndef SEAT_DYNAMIC_MODE_H
#define SEAT_DYNAMIC_MODE_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
SEAT_DYNAMIC_MODE_NONE = 0u,
SEAT_DYNAMIC_MODE_ROCKING_HORSE = 1u,
SEAT_DYNAMIC_MODE_CRADLE = 2u
} seat_dynamic_mode_id_t;
typedef enum
{
SEAT_DYNAMIC_STATE_IDLE = 0u,
SEAT_DYNAMIC_STATE_ENTRY = 1u,
SEAT_DYNAMIC_STATE_RUNNING = 2u,
SEAT_DYNAMIC_STATE_FAULT = 3u
} seat_dynamic_state_t;
typedef enum
{
SEAT_DYNAMIC_PHASE_NONE = 0u,
SEAT_DYNAMIC_PHASE_ENTRY = 1u,
SEAT_DYNAMIC_PHASE_HORSE_FORWARD = 2u,
SEAT_DYNAMIC_PHASE_HORSE_BACKWARD = 3u,
SEAT_DYNAMIC_PHASE_CRADLE_FORWARD = 5u,
SEAT_DYNAMIC_PHASE_CRADLE_RETURN = 6u
} seat_dynamic_phase_t;
typedef struct
{
uint8_t modeId;
uint8_t state;
uint8_t phase;
uint8_t activeMask;
uint16_t cycleIndex;
uint16_t phaseElapsedMs;
} seat_dynamic_mode_status_t;
void SeatDynamicMode_Init(void);
bool SeatDynamicMode_Start(uint8_t modeId, uint16_t commandSequence);
void SeatDynamicMode_Service(void);
void SeatDynamicMode_Stop(uint16_t commandSequence);
bool SeatDynamicMode_IsActive(void);
bool SeatDynamicMode_GetStatus(seat_dynamic_mode_status_t *status);
#endif /* SEAT_DYNAMIC_MODE_H */
@@ -10,6 +10,13 @@
#define SEAT_MOTOR_SPEED_CAL_MEASURE_MS (1000ul) #define SEAT_MOTOR_SPEED_CAL_MEASURE_MS (1000ul)
#define SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT (95u) #define SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT (95u)
#define SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS (70000ul) #define SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS (70000ul)
#define SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS (500ul)
#define SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS (900ul)
#define SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS (1000ul)
#define SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS (1200ul)
#define SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS (700ul)
#define SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS (1900ul)
#define SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS (1600ul)
typedef char seat_motor_manager_requires_four_motors[ typedef char seat_motor_manager_requires_four_motors[
(MOTOR_MODE_COUNT == 4) ? 1 : -1]; (MOTOR_MODE_COUNT == 4) ? 1 : -1];
@@ -22,6 +29,8 @@ typedef struct
uint32_t commandSequence; uint32_t commandSequence;
uint32_t targetValueX100; uint32_t targetValueX100;
uint32_t durationMs; uint32_t durationMs;
uint32_t speedPlanDurationMs;
uint32_t deadlineGraceMs;
uint32_t deadlineMs; uint32_t deadlineMs;
uint32_t retryAtMs; uint32_t retryAtMs;
uint32_t speedValuePerSecondX100; uint32_t speedValuePerSecondX100;
@@ -95,6 +104,36 @@ static uint32_t SeatMotor_AbsDelta(uint32_t lhs, uint32_t rhs)
return (lhs >= rhs) ? (lhs - rhs) : (rhs - lhs); return (lhs >= rhs) ? (lhs - rhs) : (rhs - lhs);
} }
static uint32_t SeatMotor_TimedPlanExtensionMs(
motor_mode_t mode,
motor_direction_t direction)
{
/* The four loaded mechanisms do not have identical low-speed breakaway
* and coast behavior. Bench traces show repeatable early arrival against
* M2 when every axis follows the same ideal time-position line. Extend
* only the speed plan (never the phase deadline) by the measured lead so
* all four axes converge at the requested phase boundary. */
if(mode == MOTOR_MODE_0_LINEAR)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS;
}
if(mode == MOTOR_MODE_1_ANGLE)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS;
}
if(mode == MOTOR_MODE_3_LINEAR_TRACK)
{
return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ?
SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS :
SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS;
}
return 0ul;
}
static bool SeatMotor_TargetWithinHallTolerance( static bool SeatMotor_TargetWithinHallTolerance(
const seat_motor_task_t *task, const seat_motor_task_t *task,
const motor_position_info_t *info) const motor_position_info_t *info)
@@ -192,9 +231,10 @@ static bool SeatMotor_PrepareNewTask(motor_mode_t mode,
static bool SeatMotor_StartPositionTask(motor_mode_t mode, static bool SeatMotor_StartPositionTask(motor_mode_t mode,
uint32_t targetValueX100, uint32_t targetValueX100,
uint8_t dutyPercent, uint8_t dutyPercent,
uint32_t speedValuePerSecondX100, uint32_t speedValuePerSecondX100,
uint32_t durationLimitMs, uint32_t durationLimitMs,
uint32_t commandSequence, uint32_t speedPlanDurationMs,
uint32_t commandSequence,
bool dynamicSpeed, bool dynamicSpeed,
bool preserveRetry, bool preserveRetry,
seat_motor_task_state_t runningState) seat_motor_task_state_t runningState)
@@ -203,7 +243,7 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
seat_motor_task_t *task; seat_motor_task_t *task;
motor_position_info_t info; motor_position_info_t info;
uint32_t travelValueX100; uint32_t travelValueX100;
uint32_t planDurationMs; uint32_t planDurationMs = 0ul;
uint8_t retryCount; uint8_t retryCount;
if((false == SeatMotor_ModeValid(mode)) || if((false == SeatMotor_ModeValid(mode)) ||
@@ -257,7 +297,8 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
if(false != dynamicSpeed) if(false != dynamicSpeed)
{ {
planDurationMs = (durationLimitMs != 0ul) ? durationLimitMs : planDurationMs = (speedPlanDurationMs != 0ul) ?
speedPlanDurationMs : (durationLimitMs != 0ul) ? durationLimitMs :
(uint32_t)((((uint64_t)travelValueX100 * 1000ull) + (uint32_t)((((uint64_t)travelValueX100 * 1000ull) +
speedValuePerSecondX100 - 1ull) / speedValuePerSecondX100 - 1ull) /
speedValuePerSecondX100); speedValuePerSecondX100);
@@ -282,6 +323,7 @@ static bool SeatMotor_StartPositionTask(motor_mode_t mode,
SEAT_MOTOR_TASK_WAIT_START : runningState; SEAT_MOTOR_TASK_WAIT_START : runningState;
task->targetValueX100 = targetValueX100; task->targetValueX100 = targetValueX100;
task->durationMs = durationLimitMs; task->durationMs = durationLimitMs;
task->speedPlanDurationMs = planDurationMs;
task->deadlineMs = 0ul; task->deadlineMs = 0ul;
task->speedValuePerSecondX100 = speedValuePerSecondX100; task->speedValuePerSecondX100 = speedValuePerSecondX100;
task->dutyPercent = dutyPercent; task->dutyPercent = dutyPercent;
@@ -490,7 +532,8 @@ void SeatMotorManager_Service(void)
} }
else if(0ul != task->durationMs) else if(0ul != task->durationMs)
{ {
task->deadlineMs = now + task->durationMs; task->deadlineMs = now + task->durationMs +
task->deadlineGraceMs;
} }
else else
{ {
@@ -517,6 +560,7 @@ void SeatMotorManager_Service(void)
task->dutyPercent, task->dutyPercent,
task->speedValuePerSecondX100, task->speedValuePerSecondX100,
task->durationMs, task->durationMs,
task->speedPlanDurationMs,
task->commandSequence, task->commandSequence,
task->dynamicSpeed, task->dynamicSpeed,
true, true,
@@ -643,6 +687,7 @@ bool SeatMotor_MoveAbsoluteDuty(motor_mode_t mode,
dutyPercent, dutyPercent,
0ul, 0ul,
0ul, 0ul,
0ul,
commandSequence, commandSequence,
false, false,
false, false,
@@ -696,6 +741,7 @@ bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode,
dutyPercent, dutyPercent,
0ul, 0ul,
timeoutMs, timeoutMs,
0ul,
commandSequence, commandSequence,
false, false,
false, false,
@@ -795,6 +841,7 @@ bool SeatMotor_MoveAbsoluteSpeed(motor_mode_t mode,
0u, 0u,
speedValuePerSecondX100, speedValuePerSecondX100,
0ul, 0ul,
0ul,
commandSequence, commandSequence,
true, true,
false, false,
@@ -879,10 +926,27 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100, uint32_t targetValueX100,
uint32_t durationMs, uint32_t durationMs,
uint32_t commandSequence) uint32_t commandSequence)
{
return SeatMotor_MoveAbsoluteInTimeWithGrace(mode,
targetValueX100,
durationMs,
0ul,
commandSequence);
}
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence)
{ {
motor_position_info_t info; motor_position_info_t info;
uint32_t travel; uint32_t travel;
uint32_t speed; uint32_t speed;
uint32_t speedPlanDurationMs;
uint32_t speedPlanExtensionMs;
motor_direction_t direction;
bool started;
if((0ul == durationMs) || if((0ul == durationMs) ||
(MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
@@ -898,16 +962,94 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
MOTOR_SPEED_MIN_DUTY_PERCENT, MOTOR_SPEED_MIN_DUTY_PERCENT,
commandSequence); commandSequence);
} }
speed = (uint32_t)((((uint64_t)travel) * 1000ull) / durationMs); direction = (targetValueX100 >= info.valueX100) ?
return SeatMotor_StartPositionTask(mode, MOTOR_DIRECTION_TOWARD_FAR :
targetValueX100, MOTOR_DIRECTION_TOWARD_NEAR;
0u, speedPlanDurationMs = (durationMs > SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) ?
speed, (durationMs - SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) : durationMs;
durationMs, speedPlanExtensionMs = SeatMotor_TimedPlanExtensionMs(mode, direction);
commandSequence, if(speedPlanExtensionMs > (UINT32_MAX - speedPlanDurationMs))
true, {
false, return false;
SEAT_MOTOR_TASK_POSITION); }
speedPlanDurationMs += speedPlanExtensionMs;
speed = (uint32_t)
((((uint64_t)travel) * 1000ull) / speedPlanDurationMs);
started = SeatMotor_StartPositionTask(mode,
targetValueX100,
0u,
speed,
durationMs,
speedPlanDurationMs,
commandSequence,
true,
false,
SEAT_MOTOR_TASK_POSITION);
if(started)
{
tasks[(uint32_t)mode].deadlineGraceMs = deadlineGraceMs;
}
return started;
}
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence)
{
motor_position_info_t info;
uint32_t targetValueX100;
if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
(false == info.positionValid) || (0ul == info.maxHallCount) ||
(targetHallCount > info.maxHallCount))
{
return false;
}
targetValueX100 = (uint32_t)
(((uint64_t)targetHallCount * info.maxValueX100 +
(info.maxHallCount / 2ul)) / info.maxHallCount);
return SeatMotor_MoveAbsoluteDuty(mode,
targetValueX100,
dutyPercent,
commandSequence);
}
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence)
{
return SeatMotor_MoveAbsoluteHallInTimeWithGrace(mode,
targetHallCount,
durationMs,
0ul,
commandSequence);
}
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence)
{
motor_position_info_t info;
uint32_t targetValueX100;
if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
(false == info.positionValid) || (0ul == info.maxHallCount) ||
(targetHallCount > info.maxHallCount))
{
return false;
}
targetValueX100 = (uint32_t)
(((uint64_t)targetHallCount * info.maxValueX100 +
(info.maxHallCount / 2ul)) / info.maxHallCount);
return SeatMotor_MoveAbsoluteInTimeWithGrace(mode,
targetValueX100,
durationMs,
deadlineGraceMs,
commandSequence);
} }
bool SeatMotor_CalibrateRange(motor_mode_t mode, bool SeatMotor_CalibrateRange(motor_mode_t mode,
@@ -110,6 +110,30 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100, uint32_t targetValueX100,
uint32_t durationMs, uint32_t durationMs,
uint32_t commandSequence); uint32_t commandSequence);
/* Keep the same speed plan while allowing a bounded scheduler/settling grace
* before the manager reports TIMEOUT. */
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Hall-coordinate variants used by firmware-owned synchronized trajectories.
* The target is checked against the live calibrated MaxHall before conversion
* to the motor's physical value. */
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */ /* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */
bool SeatMotor_CalibrateRange(motor_mode_t mode, bool SeatMotor_CalibrateRange(motor_mode_t mode,
@@ -7,6 +7,7 @@
#include "motor_control.h" #include "motor_control.h"
#include "safety_manager.h" #include "safety_manager.h"
#include "seat_controller_thermal.h" #include "seat_controller_thermal.h"
#include "seat_dynamic_mode.h"
#include "seat_motor_manager.h" #include "seat_motor_manager.h"
#include "seat_move_config.h" #include "seat_move_config.h"
#include "seat_move_store.h" #include "seat_move_store.h"
@@ -67,7 +68,9 @@ typedef enum
SEAT_MOVE_OPCODE_CALIBRATE_RANGE = 0x0Au, SEAT_MOVE_OPCODE_CALIBRATE_RANGE = 0x0Au,
SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE = 0x0Bu, SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE = 0x0Bu,
SEAT_MOVE_OPCODE_QUERY_STATUS = 0x0Cu, SEAT_MOVE_OPCODE_QUERY_STATUS = 0x0Cu,
SEAT_MOVE_OPCODE_CLEAR_FAULT = 0x0Du SEAT_MOVE_OPCODE_CLEAR_FAULT = 0x0Du,
SEAT_MOVE_OPCODE_START_DYNAMIC_MODE = 0x0Eu,
SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE = 0x0Fu
} seat_move_opcode_t; } seat_move_opcode_t;
typedef enum typedef enum
@@ -170,12 +173,14 @@ static uint8_t s_motorPeriodicId = 0u;
static uint8_t s_diagnosticPeriodicId = 0u; static uint8_t s_diagnosticPeriodicId = 0u;
static bool s_motorPeriodicPending = false; static bool s_motorPeriodicPending = false;
static bool s_diagnosticPeriodicPending = false; static bool s_diagnosticPeriodicPending = false;
static bool s_dynamicStatusPending = true;
static uint8_t s_systemAlive = 0u; static uint8_t s_systemAlive = 0u;
static uint8_t s_diagnosticAlive[SEAT_MOVE_MOTOR_COUNT]; static uint8_t s_diagnosticAlive[SEAT_MOVE_MOTOR_COUNT];
static uint32_t s_nextStartMs = 0u; static uint32_t s_nextStartMs = 0u;
static uint32_t s_nextSystemStatusMs = 0u; static uint32_t s_nextSystemStatusMs = 0u;
static uint32_t s_nextMotorPollMs = 0u; static uint32_t s_nextMotorPollMs = 0u;
static uint32_t s_nextDiagnosticPollMs = 0u; static uint32_t s_nextDiagnosticPollMs = 0u;
static uint32_t s_nextDynamicStatusMs = 0u;
static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline); static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline);
static bool SeatMove_SequenceNewer(uint16_t candidate, uint16_t reference); static bool SeatMove_SequenceNewer(uint16_t candidate, uint16_t reference);
@@ -220,6 +225,7 @@ static bool SeatMove_SendSystemStatus(void);
static bool SeatMove_SendMotorStatus(uint8_t motor); static bool SeatMove_SendMotorStatus(uint8_t motor);
static bool SeatMove_SendDiagnostic(uint8_t motor); static bool SeatMove_SendDiagnostic(uint8_t motor);
static bool SeatMove_SendCurrentDiagnostic(uint8_t motor); static bool SeatMove_SendCurrentDiagnostic(uint8_t motor);
static bool SeatMove_SendDynamicStatus(void);
static void SeatMove_ServiceFeedback(void); static void SeatMove_ServiceFeedback(void);
bool SeatMoveApplication_Init(void) bool SeatMoveApplication_Init(void)
@@ -233,6 +239,7 @@ bool SeatMoveApplication_Init(void)
s_storeSavePending = false; s_storeSavePending = false;
s_storeSaveNotBeforeMs = 0u; s_storeSaveNotBeforeMs = 0u;
SeatMotorManager_Init(); SeatMotorManager_Init();
SeatDynamicMode_Init();
SeatMoveStore_Init(); SeatMoveStore_Init();
if(SeatMoveStore_IsValid() && SeatMoveStore_IsClean() && if(SeatMoveStore_IsValid() && SeatMoveStore_IsClean() &&
@@ -254,9 +261,11 @@ bool SeatMoveApplication_Init(void)
s_nextSystemStatusMs = now; s_nextSystemStatusMs = now;
s_nextMotorPollMs = now; s_nextMotorPollMs = now;
s_nextDiagnosticPollMs = now; s_nextDiagnosticPollMs = now;
s_nextDynamicStatusMs = now;
s_systemStatusPending = true; s_systemStatusPending = true;
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_dynamicStatusPending = true;
return true; return true;
} }
@@ -273,6 +282,15 @@ void SeatMoveApplication_Service(void)
{ {
uint8_t motor; uint8_t motor;
if(SeatDynamicMode_IsActive())
{
SeatDynamicMode_Stop(g_seatMoveLastSequence);
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
s_dynamicStatusPending = true;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{ {
s_pending[motor].valid = false; s_pending[motor].valid = false;
@@ -294,6 +312,7 @@ void SeatMoveApplication_Service(void)
} }
SeatMove_ServiceDispatcher(); SeatMove_ServiceDispatcher();
SeatMotorManager_Service(); SeatMotorManager_Service();
SeatDynamicMode_Service();
SeatMove_ServiceStateChanges(); SeatMove_ServiceStateChanges();
SeatMove_ServicePersistence(); SeatMove_ServicePersistence();
SeatMove_ServiceFeedback(); SeatMove_ServiceFeedback();
@@ -326,7 +345,9 @@ void Motor_CalibrationServiceHook(void)
void SeatMoveApplication_StopAll(uint16_t sequence) void SeatMoveApplication_StopAll(uint16_t sequence)
{ {
uint8_t logicalMotor; uint8_t logicalMotor;
bool dynamicWasActive = SeatDynamicMode_IsActive();
SeatDynamicMode_Stop(sequence);
Motor_RequestAbortAllFromIsr(sequence); Motor_RequestAbortAllFromIsr(sequence);
Motor_ServiceAll(); Motor_ServiceAll();
for(logicalMotor = 0u; for(logicalMotor = 0u;
@@ -342,6 +363,13 @@ void SeatMoveApplication_StopAll(uint16_t sequence)
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_systemStatusPending = true; s_systemStatusPending = true;
s_dynamicStatusPending = true;
if(dynamicWasActive)
{
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
}
} }
static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline) static bool SeatMove_TimeReached(uint32_t now, uint32_t deadline)
@@ -479,6 +507,8 @@ static bool SeatMove_ParseCommand(const bsp_seat_move_can_frame_t *frame,
case SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE: case SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE:
case SEAT_MOVE_OPCODE_QUERY_STATUS: case SEAT_MOVE_OPCODE_QUERY_STATUS:
case SEAT_MOVE_OPCODE_CLEAR_FAULT: case SEAT_MOVE_OPCODE_CLEAR_FAULT:
case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE:
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
break; break;
default: default:
*reject = SEAT_MOVE_REJECT_BAD_OPCODE; *reject = SEAT_MOVE_REJECT_BAD_OPCODE;
@@ -501,7 +531,9 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
*reject = SEAT_MOVE_REJECT_BAD_MOTOR; *reject = SEAT_MOVE_REJECT_BAD_MOTOR;
return false; return false;
} }
if((command->opcode == SEAT_MOVE_OPCODE_STOP_ALL) && if(((command->opcode == SEAT_MOVE_OPCODE_STOP_ALL) ||
(command->opcode == SEAT_MOVE_OPCODE_START_DYNAMIC_MODE) ||
(command->opcode == SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE)) &&
(command->motor != 0u)) (command->motor != 0u))
{ {
*reject = SEAT_MOVE_REJECT_BAD_MOTOR; *reject = SEAT_MOVE_REJECT_BAD_MOTOR;
@@ -590,6 +622,22 @@ static bool SeatMove_ParametersValid(const seat_move_command_t *command,
return false; return false;
} }
break; break;
case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE:
if(((command->paramA != SEAT_DYNAMIC_MODE_ROCKING_HORSE) &&
(command->paramA != SEAT_DYNAMIC_MODE_CRADLE)) ||
(command->paramB != 0u))
{
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
return false;
}
break;
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
if((command->paramA != 0u) || (command->paramB != 0u))
{
*reject = SEAT_MOVE_REJECT_PARAM_RANGE;
return false;
}
break;
default: default:
*reject = SEAT_MOVE_REJECT_BAD_OPCODE; *reject = SEAT_MOVE_REJECT_BAD_OPCODE;
return false; return false;
@@ -801,9 +849,27 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
g_seatMoveAcceptedCount++; g_seatMoveAcceptedCount++;
return; return;
} }
if(command.opcode == SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE)
{
SeatMoveApplication_StopAll(command.sequence);
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE,
&command);
g_seatMoveAcceptedCount++;
return;
}
if(command.opcode == SEAT_MOVE_OPCODE_STOP_MOTOR) if(command.opcode == SEAT_MOVE_OPCODE_STOP_MOTOR)
{ {
SeatMove_StopMotor(command.motor, command.sequence); if(SeatDynamicMode_IsActive())
{
/* A one-axis stop would invalidate a synchronized trajectory.
* Stop the complete dynamic mode instead. */
SeatMoveApplication_StopAll(command.sequence);
}
else
{
SeatMove_StopMotor(command.motor, command.sequence);
}
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED, SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE, SEAT_MOVE_REJECT_NONE,
&command); &command);
@@ -855,6 +921,64 @@ static void SeatMove_HandleCommand(const bsp_seat_move_can_frame_t *frame)
&command); &command);
return; return;
} }
if(command.opcode == SEAT_MOVE_OPCODE_START_DYNAMIC_MODE)
{
if(SeatDynamicMode_IsActive() || SeatMotorManager_IsAnyActive() ||
SeatMove_AnyPending())
{
g_seatMoveConflictCount++;
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_BUSY_SAME_MOTOR,
&command);
return;
}
if(SeatMove_PositionValidMask() != SEAT_MOVE_ACTIVE_MOTOR_MASK)
{
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SeatMoveStore_HasCorruptRecord() ?
SEAT_MOVE_REJECT_CAL_DATA_INVALID :
SEAT_MOVE_REJECT_POSITION_INVALID,
&command);
return;
}
SafetyManager_KickWatchdog();
if(!SeatMoveStore_MarkMovementStarted())
{
SafetyManager_LatchFault(SAFETY_FAULT_CONFIG_STORE);
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_INTERNAL_ERROR,
&command);
return;
}
s_storeSavePending = true;
s_storeSaveNotBeforeMs =
Motor_GetTimeMs() + SEAT_MOVE_STORE_SAVE_DEBOUNCE_MS;
if(!SeatDynamicMode_Start((uint8_t)command.paramA,
command.sequence))
{
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_INTERNAL_ERROR,
&command);
s_dynamicStatusPending = true;
return;
}
s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK;
s_dynamicStatusPending = true;
SeatMove_SetResponse(SEAT_MOVE_ACK_ACCEPTED,
SEAT_MOVE_REJECT_NONE,
&command);
g_seatMoveAcceptedCount++;
return;
}
if(SeatDynamicMode_IsActive())
{
g_seatMoveConflictCount++;
SeatMove_SetResponse(SEAT_MOVE_ACK_REJECTED,
SEAT_MOVE_REJECT_BUSY_SAME_MOTOR,
&command);
return;
}
if(SeatMove_IsPositionCommand(command.opcode)) if(SeatMove_IsPositionCommand(command.opcode))
{ {
if(!SeatMove_GetStatus(command.motor, &status)) if(!SeatMove_GetStatus(command.motor, &status))
@@ -1069,6 +1193,14 @@ static void SeatMove_ServicePersistence(void)
return; return;
} }
/* A continuously cycling dynamic mode must not commit Work Flash at each
* hold or phase boundary. The dirty record remains authoritative until
* STOP_ALL/STOP_DYNAMIC_MODE and one final settled save. */
if(SeatDynamicMode_IsActive())
{
return;
}
now = Motor_GetTimeMs(); now = Motor_GetTimeMs();
if(SeatMove_AnyPending() || SeatMotorManager_IsAnyActive() || if(SeatMove_AnyPending() || SeatMotorManager_IsAnyActive() ||
SeatMove_AnyPhysicalMotorMoving()) SeatMove_AnyPhysicalMotorMoving())
@@ -1166,8 +1298,11 @@ static uint8_t SeatMove_MapTaskResult(const seat_motor_status_t *status)
static uint8_t SeatMove_SystemState(void) static uint8_t SeatMove_SystemState(void)
{ {
seat_dynamic_mode_status_t dynamicStatus;
uint8_t activeMask = SeatMove_ActiveMask(); uint8_t activeMask = SeatMove_ActiveMask();
uint8_t faultedMask = SeatMove_FaultedMask(); uint8_t faultedMask = SeatMove_FaultedMask();
bool dynamicFault = SeatDynamicMode_GetStatus(&dynamicStatus) &&
(dynamicStatus.state == SEAT_DYNAMIC_STATE_FAULT);
if(SafetyManager_IsInhibited()) if(SafetyManager_IsInhibited())
{ {
@@ -1178,12 +1313,12 @@ static uint8_t SeatMove_SystemState(void)
{ {
return SEAT_MOVE_SYSTEM_CALIBRATION; return SEAT_MOVE_SYSTEM_CALIBRATION;
} }
if((faultedMask != 0u) || if((faultedMask != 0u) || dynamicFault ||
(SafetyManager_GetFault() != SAFETY_FAULT_NONE)) (SafetyManager_GetFault() != SAFETY_FAULT_NONE))
{ {
return SEAT_MOVE_SYSTEM_FAULT; return SEAT_MOVE_SYSTEM_FAULT;
} }
if(activeMask != 0u) if((activeMask != 0u) || SeatDynamicMode_IsActive())
{ {
return SEAT_MOVE_SYSTEM_ACTIVE; return SEAT_MOVE_SYSTEM_ACTIVE;
} }
@@ -1192,11 +1327,15 @@ static uint8_t SeatMove_SystemState(void)
static bool SeatMove_SendSystemStatus(void) static bool SeatMove_SendSystemStatus(void)
{ {
seat_dynamic_mode_status_t dynamicStatus;
uint8_t activeMask = SeatMove_ActiveMask(); uint8_t activeMask = SeatMove_ActiveMask();
uint8_t faultedMask = SeatMove_FaultedMask(); uint8_t faultedMask = SeatMove_FaultedMask();
uint8_t positionValidMask = SeatMove_PositionValidMask(); uint8_t positionValidMask = SeatMove_PositionValidMask();
bool calibrationBusy = SeatMove_CalibrationBusy(); bool calibrationBusy = SeatMove_CalibrationBusy();
bool faultSummary = (faultedMask != 0u) || bool dynamicStatusValid = SeatDynamicMode_GetStatus(&dynamicStatus);
bool dynamicFault = dynamicStatusValid &&
(dynamicStatus.state == SEAT_DYNAMIC_STATE_FAULT);
bool faultSummary = (faultedMask != 0u) || dynamicFault ||
(SafetyManager_GetFault() != SAFETY_FAULT_NONE) || (SafetyManager_GetFault() != SAFETY_FAULT_NONE) ||
SeatMoveStore_HasCorruptRecord(); SeatMoveStore_HasCorruptRecord();
uint8_t byte0 = (uint8_t)((g_seatMoveAck & 0x0Fu) | uint8_t byte0 = (uint8_t)((g_seatMoveAck & 0x0Fu) |
@@ -1205,6 +1344,7 @@ static bool SeatMove_SendSystemStatus(void)
uint8_t byte4 = g_seatMoveLastOpcode; uint8_t byte4 = g_seatMoveLastOpcode;
uint8_t byte5 = (uint8_t)((activeMask & 0x0Fu) | uint8_t byte5 = (uint8_t)((activeMask & 0x0Fu) |
(SafetyManager_IsInhibited() ? 0x10u : 0u) | (SafetyManager_IsInhibited() ? 0x10u : 0u) |
(SeatDynamicMode_IsActive() ? 0x20u : 0u) |
(calibrationBusy ? 0x40u : 0u) | (calibrationBusy ? 0x40u : 0u) |
(faultSummary ? 0x80u : 0u)); (faultSummary ? 0x80u : 0u));
uint8_t byte6 = (uint8_t)((faultedMask & 0x0Fu) | uint8_t byte6 = (uint8_t)((faultedMask & 0x0Fu) |
@@ -1304,7 +1444,9 @@ static bool SeatMove_SendDiagnostic(uint8_t motor)
UINT16_MAX : (uint16_t)status.hallCount; UINT16_MAX : (uint16_t)status.hallCount;
maxHallCount = (status.maxHallCount > UINT16_MAX) ? maxHallCount = (status.maxHallCount > UINT16_MAX) ?
UINT16_MAX : (uint16_t)status.maxHallCount; UINT16_MAX : (uint16_t)status.maxHallCount;
speedRaw = (status.hallSpeedPpsX100 + 100u) / 200u; /* 0x303 B6 is specified as 0.5 pulse/s per LSB. hallSpeed is x100,
* therefore divide by 50 (with half-up rounding), not by 200. */
speedRaw = (status.hallSpeedPpsX100 + 25u) / 50u;
if(speedRaw > UINT8_MAX) if(speedRaw > UINT8_MAX)
{ {
speedRaw = UINT8_MAX; speedRaw = UINT8_MAX;
@@ -1363,6 +1505,27 @@ static bool SeatMove_SendCurrentDiagnostic(uint8_t motor)
data1); data1);
} }
static bool SeatMove_SendDynamicStatus(void)
{
seat_dynamic_mode_status_t status;
uint32_t data0;
uint32_t data1;
if(!SeatDynamicMode_GetStatus(&status))
{
return false;
}
data0 = (uint32_t)status.modeId |
((uint32_t)status.state << 8u) |
((uint32_t)status.phase << 16u) |
((uint32_t)status.activeMask << 24u);
data1 = (uint32_t)status.cycleIndex |
((uint32_t)status.phaseElapsedMs << 16u);
return BspSeatMoveCan_Send(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID,
data0,
data1);
}
static void SeatMove_ServiceFeedback(void) static void SeatMove_ServiceFeedback(void)
{ {
uint32_t now = Motor_GetTimeMs(); uint32_t now = Motor_GetTimeMs();
@@ -1373,6 +1536,11 @@ static void SeatMove_ServiceFeedback(void)
s_systemStatusPending = true; s_systemStatusPending = true;
s_nextSystemStatusMs = now + SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS; s_nextSystemStatusMs = now + SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS;
} }
if(SeatMove_TimeReached(now, s_nextDynamicStatusMs))
{
s_dynamicStatusPending = true;
s_nextDynamicStatusMs = now + SEAT_MOVE_DYNAMIC_STATUS_PERIOD_MS;
}
if(!s_motorPeriodicPending && if(!s_motorPeriodicPending &&
SeatMove_TimeReached(now, s_nextMotorPollMs)) SeatMove_TimeReached(now, s_nextMotorPollMs))
{ {
@@ -1408,6 +1576,19 @@ static void SeatMove_ServiceFeedback(void)
} }
return; return;
} }
if(s_dynamicStatusPending)
{
if(SeatMove_SendDynamicStatus())
{
s_dynamicStatusPending = false;
g_seatMoveFeedbackTxCount++;
}
else
{
g_seatMoveFeedbackTxFailureCount++;
}
return;
}
for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++)
{ {
uint8_t bit = (uint8_t)(1u << motor); uint8_t bit = (uint8_t)(1u << motor);
@@ -0,0 +1,55 @@
#ifndef SEAT_DYNAMIC_MODE_CONFIG_H
#define SEAT_DYNAMIC_MODE_CONFIG_H
/* Rocking-horse tuning for the connected N7 passenger seat.
*
* M0/M1/M2/M3 are longitudinal, cushion tilt, backrest and legrest. Keep all
* Hall targets inside the calibrated ranges before building for another seat.
* The centre pose and two symmetric endpoints are deliberately separated from
* the mechanical endpoints. M1 uses a 120-Hall peak-to-peak stroke (about
* 18 mm of the calibrated vertical component) so the rider can feel a real
* rise/fall instead of only a backrest pitch. M0 uses a 100-Hall stroke at
* 90% duty to overcome the opposing horizontal component created by that
* diagonal M1 stroke. This file is the ECU-side customization
* point; config/modes/vehicles/denza_n7/rocking_horse.yaml mirrors these values
* for the long-term mode library and frontend.
*/
#define SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL (320u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL (190u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL (500u)
#define SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL (430u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL (270u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL (250u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL (460u)
#define SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL (400u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL (370u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL (130u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL (540u)
#define SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL (460u)
#define SEAT_DYNAMIC_HORSE_M0_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M1_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M2_DUTY_PERCENT (90u)
#define SEAT_DYNAMIC_HORSE_M3_DUTY_PERCENT (50u)
#define SEAT_DYNAMIC_HORSE_FINAL_DUTY_PERCENT (50u)
#define SEAT_DYNAMIC_HORSE_RENDEZVOUS_PULSES (8u)
#define SEAT_DYNAMIC_HORSE_PHASE_TIMEOUT_MS (15000u)
#if ((SEAT_DYNAMIC_HORSE_ENTRY_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M0_HALL > 480u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M1_HALL > 374u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M2_HALL > 917u) || \
(SEAT_DYNAMIC_HORSE_ENTRY_M3_HALL > 647u) || \
(SEAT_DYNAMIC_HORSE_FORWARD_M3_HALL > 647u) || \
(SEAT_DYNAMIC_HORSE_BACKWARD_M3_HALL > 647u))
#error "Rocking-horse Hall targets exceed the nominal N7 calibrated ranges."
#endif
#endif /* SEAT_DYNAMIC_MODE_CONFIG_H */
@@ -67,14 +67,25 @@
#define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u) #define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u)
/* Repository image identity carried by 0x301 byte 7 high nibble. Increment /* Repository image identity carried by 0x301 byte 7 high nibble. Increment
* whenever a host-visible safety or motion fix requires automatic reflash. */ * whenever a host-visible safety or motion fix requires automatic reflash. */
#define SEAT_MOVE_FIRMWARE_REVISION (6u) #define SEAT_MOVE_FIRMWARE_REVISION (13u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u) #define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u) #define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
#define SEAT_MOVE_CAN_MOTOR_STATUS_ID (0x302u) #define SEAT_MOVE_CAN_MOTOR_STATUS_ID (0x302u)
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u) #define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u) #define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_DYNAMIC_STATUS_ID (0x306u)
#define SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS (100u) #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
* ECU keeps cycling after the start ACK, but always stops after ten minutes
* if no STOP_DYNAMIC/STOP_ALL command arrives. */
#define SEAT_MOVE_DYNAMIC_MAX_RUNTIME_MS (600000u)
/* The phase duration is the requested synchronized-arrival target. A loaded
* bench axis may be physically slower (notably M2 for the 472-pulse breathing
* stroke), so normal Hall progress may continue through this bounded grace
* window. Stall/no-motion and driver-fault protections remain immediate. */
#define SEAT_MOVE_DYNAMIC_ARRIVAL_GRACE_MS (30000u)
#define SEAT_MOVE_MOTOR_STATUS_POLL_MS (25u) #define SEAT_MOVE_MOTOR_STATUS_POLL_MS (25u)
/* One motor is emitted per poll; 25 ms gives each axis a 100 ms 0x303/0x304 /* One motor is emitted per poll; 25 ms gives each axis a 100 ms 0x303/0x304
* update period and captures the 200 ms high-current confirmation window. */ * update period and captures the 200 ms high-current confirmation window. */
@@ -148,7 +159,8 @@
(SEAT_MOVE_CAN_SYSTEM_STATUS_ID > 0x7FFu) || \ (SEAT_MOVE_CAN_SYSTEM_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \ (SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \ (SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu)) (SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DYNAMIC_STATUS_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs." #error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif #endif
@@ -1,6 +1,6 @@
# Seat_Controller_demo # Seat_Controller_demo
`Seat_Controller_demo` 是 `CYT4BFBCJE` / `CY_MCU_rev_d` / Cortex-M0+ Flash 的单板统一座椅控制器工程,集成座椅通风、靠背加热和四电机移动。SDL 公共源码未修改,应用配置和功能代码均位于本工程目录。 `Seat_Controller_demo` 是 `CYT4BFBCJE` / `CY_MCU_rev_d` / Cortex-M0+ Flash 的单板统一座椅控制器工程,集成座椅通风、靠背加热、四电机移动和两个原生连续循环动态模式。动态模式收到停止/全停命令或启动累计10分钟时终止。SDL 公共源码未修改,应用配置和功能代码均位于本工程目录。
## 单板资源 ## 单板资源
@@ -22,8 +22,9 @@
- TX `0x301/0x302/0x303`:移动 ACK、状态和 Hall 诊断。 - TX `0x301/0x302/0x303`:移动 ACK、状态和 Hall 诊断。
- TX `0x304`:四轴电流诊断;B0 MotorID,B1 bits0..4依次为采样有效/零点有效/高电流/低Hall/标定中,B2..3 ADC raw,B4..5停止态零点raw,B6..7该轴堵转电流差值阈值raw。 - TX `0x304`:四轴电流诊断;B0 MotorID,B1 bits0..4依次为采样有效/零点有效/高电流/低Hall/标定中,B2..3 ADC raw,B4..5停止态零点raw,B6..7该轴堵转电流差值阈值raw。
- TX `0x305`:热管理电流诊断;B0采样有效标志,B1输出状态,B2..3通风电流raw,B4..5靠背加热电流raw,B6/B7为两路ADC累计错误计数(饱和到255)。 - TX `0x305`:热管理电流诊断;B0采样有效标志,B1输出状态,B2..3通风电流raw,B4..5靠背加热电流raw,B6/B7为两路ADC累计错误计数(饱和到255)。
- TX `0x306`:100 ms动态模式状态;ModeID、状态、阶段、四轴ActiveMask、循环序号和阶段已运行毫秒数。
CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为 1;支持 `MOVE_ABSOLUTE`、`MOVE_RELATIVE`、`MOVE_TIMED`、`STOP_MOTOR`、`STOP_ALL`、`CALIBRATE_RANGE`、`HOME_KNOWN_RANGE`、`QUERY_STATUS` 和 `CLEAR_FAULT`。四轴定义及完整字节布局见 `Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx`。 CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为 1;支持 `MOVE_ABSOLUTE`、`MOVE_RELATIVE`、`MOVE_TIMED`、`STOP_MOTOR`、`STOP_ALL`、`CALIBRATE_RANGE`、`HOME_KNOWN_RANGE`、`QUERY_STATUS`、`CLEAR_FAULT`、`START_DYNAMIC_MODE(0x0E)` 和 `STOP_DYNAMIC_MODE(0x0F)`。ModeID 1为木马模式,ModeID 2为动态摇篮;两者均按目标到位连续循环。四轴定义及完整字节布局见 `Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx`。
2026-07-30 四个独立量程入口已验证 CAN MotorID 与板级物理 Mode 同号映射:M0/physical0 水平前后、M1/physical1 坐盆上下、M2/physical2 靠背前后、M3/physical3 腿托上下。首次测试仍须目视确认没有错误执行机构动作。 2026-07-30 四个独立量程入口已验证 CAN MotorID 与板级物理 Mode 同号映射:M0/physical0 水平前后、M1/physical1 坐盆上下、M2/physical2 靠背前后、M3/physical3 腿托上下。首次测试仍须目视确认没有错误执行机构动作。
@@ -46,6 +47,7 @@ CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为
- 四轴分别具有启动宽限(M0/M1/M2=1 s,M3=2 s)。普通移动仅在“500 ms Hall双边沿窗口不超过2个边沿”且“相对停止态零点的轴电流超过独立阈值并持续200 ms”同时成立时报告 `STALL`;低Hall但电流低或采样过期时不误报堵转,连续5 s无运动后关断并报告 `TIMEOUT`。 - 四轴分别具有启动宽限(M0/M1/M2=1 s,M3=2 s)。普通移动仅在“500 ms Hall双边沿窗口不超过2个边沿”且“相对停止态零点的轴电流超过独立阈值并持续200 ms”同时成立时报告 `STALL`;低Hall但电流低或采样过期时不误报堵转,连续5 s无运动后关断并报告 `TIMEOUT`。
- 真实台架诊断必须同时保存命令期间的 `0x304` 电流峰值和 `0x303` Hall 变化:高电流且无 Hall 表示机械阻塞或 Hall 公共参考异常,低电流且双向无 Hall 更符合驱动桥、连接器、电机线束或 Hall 地线开路。2026-08-01 的 M0/M1/M2 异常最终定位为 Hall 地线断开;2026-08-02 恢复接线后完整 33 条自动 HIL 全部通过。不得靠提高 Duty、阈值或超时掩盖同类线路问题。 - 真实台架诊断必须同时保存命令期间的 `0x304` 电流峰值和 `0x303` Hall 变化:高电流且无 Hall 表示机械阻塞或 Hall 公共参考异常,低电流且双向无 Hall 更符合驱动桥、连接器、电机线束或 Hall 地线开路。2026-08-01 的 M0/M1/M2 异常最终定位为 Hall 地线断开;2026-08-02 恢复接线后完整 33 条自动 HIL 全部通过。不得靠提高 Duty、阈值或超时掩盖同类线路问题。
- 四轴普通绝对/相对移动默认使用90% PWM占空比;堵转电流差值阈值在重新带载标定前统一使用500 raw保守默认值,优先避免正常运动误报。PWM软件上限保持95%。 - 四轴普通绝对/相对移动默认使用90% PWM占空比;堵转电流差值阈值在重新带载标定前统一使用500 raw保守默认值,优先避免正常运动误报。PWM软件上限保持95%。
- 动态模式按Hall目标和阶段时长做低速位置进度闭环;四轴同步启动,在相位边界以±3 Hall判定到位后统一停止再切换。任何轴不能在阶段宽限内到位时全停并通过`0x306`报告FAULT。
- 位置任务因端点/堵转事件停止时,若最终Hall已进入目标±2脉冲范围则按到位完成;底层LastEvent仍由`0x303`保留用于诊断。超过该范围的STALL/TIMEOUT仍按失败处理。 - 位置任务因端点/堵转事件停止时,若最终Hall已进入目标±2脉冲范围则按到位完成;底层LastEvent仍由`0x303`保留用于诊断。超过该范围的STALL/TIMEOUT仍按失败处理。
- `HOME_KNOWN_RANGE`/全量标定的寻端语义与普通移动分离。端点要求500 ms内Hall双边沿为0,避免慢速移动仅有下降沿时被误判为端点;已知量程归零若第一次寻近端少于4个真实Hall边沿,会先向远端验证至少4个双边沿再重新寻近端,并校验回程与离端边沿数(允许1边沿误差),禁止把电机未启动直接清零。无法证明往返运动时归零失败并使PositionValid失效。位置和量程仍按Hall上升沿计数。 - `HOME_KNOWN_RANGE`/全量标定的寻端语义与普通移动分离。端点要求500 ms内Hall双边沿为0,避免慢速移动仅有下降沿时被误判为端点;已知量程归零若第一次寻近端少于4个真实Hall边沿,会先向远端验证至少4个双边沿再重新寻近端,并校验回程与离端边沿数(允许1边沿误差),禁止把电机未启动直接清零。无法证明往返运动时归零失败并使PositionValid失效。位置和量程仍按Hall上升沿计数。
- `STOP_ALL` 不受旧 Sequence 限制;CAN RX ISR 先立即终止四路 PWM,因此也能打断阻塞式全行程标定,主流程随后完成 ACK 和 Sequence 重同步。 - `STOP_ALL` 不受旧 Sequence 限制;CAN RX ISR 先立即终止四路 PWM,因此也能打断阻塞式全行程标定,主流程随后完成 ACK 和 Sequence 重同步。
@@ -70,5 +72,18 @@ CAN 为 500 kbit/s、经典标准帧、11-bit ID、DLC 8。移动协议版本为
- `Tests/manual_hil/05_seat_function_bridge.py`:在ZCANPRO内运行的CAN桥接。 - `Tests/manual_hil/05_seat_function_bridge.py`:在ZCANPRO内运行的CAN桥接。
- `Tests/manual_hil/run_seat_function_gui.bat`:Windows选择界面,从仓库根目录 `座椅功能模式配置表.xlsx` 连续动态读取当前11种模式/12个场景,显示物理目标、重算Hall、CAN结果并保存人工观察;小憩模式为单场景,并包含新增“小腿解压悬浮模式”。 - `Tests/manual_hil/run_seat_function_gui.bat`:Windows选择界面,从仓库根目录 `座椅功能模式配置表.xlsx` 连续动态读取当前11种模式/12个场景,显示物理目标、重算Hall、CAN结果并保存人工观察;小憩模式为单场景,并包含新增“小腿解压悬浮模式”。
- `Tests/manual_hil/02_power_memory.py` 默认只做正常掉电恢复;`03_protection_faults.py` 默认只做无需注入设备的协议保护。 - `Tests/manual_hil/02_power_memory.py` 默认只做正常掉电恢复;`03_protection_faults.py` 默认只做无需注入设备的协议保护。
- `Tests/manual_hil/07_dynamic_modes.py`:revision 13原生动态模式HIL。验证带水平耦合补偿的木马模式和动态摇篮的完整阶段顺序、精确Hall目标、四轴全程联动、自动跨周期和停止;ECU另有600 s硬截止。
2026-08-04已验证公共动态运行时的自动续环、循环中停止,以及单次连续运行600.024 s后的ECU安全截止;任何轴堵转、无运动或驱动故障仍会保持全停,固件不绕过底层保护。
同日revision 9在M1执行已知量程回近端后通过重新设计的摇篮实测:Step 1→2启动/到位离散0.077/0.338 s,Step 2→3为0.079/0.495 s,全部目标在±3 Hall内,CycleIndex自动进入1,下一轮四轴启动离散0.082 s,停止后`0x306`全零。
同日revision 10删除M1延迟追赶:正向/返回水平与靠背约走25%时,坐盆已走44.0%/39.0%;四轴启动离散0.070/0.050 s、到位离散0.342/0.339 s,下一轮启动离散0.051 s。四轴从阶段开始全程联动,停止后`0x306`全零。
同日revision 11用木马模式替换旧ModeID 1轨迹。木马在前俯`[290,220,460,400]`和后仰`[350,160,540,460]`间连续往复;稳态前俯/后仰启动离散0.080/0.079 s,到位离散0.234/0.232 s,水平到25%时坐盆已达41.7%/36.7%,完整周期约9.0 s。目标、自动续环和停止全零均PASS,配置见`Config/seat_dynamic_mode_config.h`。
同日revision 12将坐盆目标扩大到`250↔130` Hall并把M1巡航占空比提升为90%,坐盆峰峰垂直分量约18 mm。稳态前俯/后仰启动离散0.054/0.081 s,到位离散0.389/0.182 s,水平到25%时坐盆已达46.6%/35.8%,完整周期约9.2 s;无FAULT、目标、自动续环和停止全零均PASS。
同日revision 13将M0水平目标扩大到`270↔370` Hall并把巡航Duty提升为90%,用100 Hall峰峰行程补偿M1斜向运动。实测水平峰峰行程约43.1 mm、估算净水平摆幅约26.7 mm;稳态前俯/后仰启动离散0.077/0.052 s,到位离散0.337/0.182 s,四轴巡航持续推进、无FAULT、目标、自动续环和停止全零均PASS。
本轮不执行 `01_calibration_position.py` 或 `04_waveform_full_function.py`,两者均已设置归档保护。具体顺序和日志回传要求见 `Tests/manual_hil/README_CN.md`。 本轮不执行 `01_calibration_position.py` 或 `04_waveform_full_function.py`,两者均已设置归档保护。具体顺序和日志回传要求见 `Tests/manual_hil/README_CN.md`。
@@ -1,13 +1,13 @@
# 座椅控制器 CAN/HIL 测试规范 # 座椅控制器 CAN/HIL 测试规范
版本:2.3(2026-07-30) 版本:3.0(2026-08-03)
对象:`Seat_Controller_demo`、`量程测试脚本` 对象:`Seat_Controller_demo`、`量程测试脚本`
MCU:CYT4BFBCJE / CY_MCU_rev_d / Cortex-M0+ Flash MCU:CYT4BFBCJE / CY_MCU_rev_d / Cortex-M0+ Flash
总线:Classic CAN,11-bit 标准帧,500 kbit/s,DLC=8,Intel/Little-endian 总线:Classic CAN,11-bit 标准帧,500 kbit/s,DLC=8,Intel/Little-endian
## 1. 目的与范围 ## 1. 目的与范围
本规范用于验证统一控制器的座椅通风、靠背加热、四轴移动、自动标定、反馈周期和安全停机。测试分为静态构建、无机械动作 CAN 检查、单轴低风险动作、多轴并发、全行程标定和故障注入。未经安全确认不得直接跳到多轴或标定阶段。 本规范用于验证统一控制器的座椅通风、靠背加热、四轴移动、原生动态模式、自动标定、反馈周期和安全停机。测试分为静态构建、无机械动作 CAN 检查、单轴低风险动作、多轴并发、动态时序、全行程标定和故障注入。未经安全确认不得直接跳到多轴、动态模式或标定阶段。
配套文件: 配套文件:
@@ -66,20 +66,32 @@ Byte0=Opcode;Byte1=`0x10 | MotorID | (Direction<<2)`;Byte2..3=Sequence;Byt
| `0B` | 已知量程回最近端 | `0x4B52` | 最近端寻找10~600 s | | `0B` | 已知量程回最近端 | `0x4B52` | 最近端寻找10~600 s |
| `0C` | 查询状态 | 0 | 0 | | `0C` | 查询状态 | 0 | 0 |
| `0D` | 清故障 | `0xC33C` | 0 | | `0D` | 清故障 | `0xC33C` | 0 |
| `0E` | 启动原生动态模式 | ModeID:1木马模式、2动态摇篮 | 0 |
| `0F` | 停止原生动态模式 | 0 | 0 |
### 3.3 移动反馈 ### 3.3 移动反馈
- `0x301`:Byte0 低半字节 ACK、高半字节 SystemState;Byte1 RejectReason;Byte2..3 Sequence;Byte4 Opcode;Byte5 ActiveMask/抑制/标定/故障;Byte6 FaultedMask/PositionValidMask;Byte7 低半字节 Alive、高半字节 FirmwareRevision(当前为 2)。 - `0x301`:Byte0 低半字节 ACK、高半字节 SystemState;Byte1 RejectReason;Byte2..3 Sequence;Byte4 Opcode;Byte5 ActiveMask/抑制/动态/标定/故障;Byte6 FaultedMask/PositionValidMask;Byte7 低半字节 Alive、高半字节 FirmwareRevision(当前为 13)。
- `0x302`:Byte0 MotorID;Byte1 TaskState/Result;Byte2 方向、Active、PositionValid、端点、堵转和驱动故障;Byte3..4 Sequence;Byte5..6 归一化位置;Byte7 驱动等级。 - `0x302`:Byte0 MotorID;Byte1 TaskState/Result;Byte2 方向、Active、PositionValid、端点、堵转和驱动故障;Byte3..4 Sequence;Byte5..6 归一化位置;Byte7 驱动等级。
- `0x303`:Byte0 MotorID;Byte1 LastEvent/FaultFlags;Byte2..3 HallCount;Byte4..5 MaxHallCount;Byte6 Hall速度(0.5 pulse/s/LSB);Byte7 Retry/Alive。 - `0x303`:Byte0 MotorID;Byte1 LastEvent/FaultFlags;Byte2..3 HallCount;Byte4..5 MaxHallCount;Byte6 Hall速度(0.5 pulse/s/LSB);Byte7 Retry/Alive。
- `0x306`:原生动态模式状态,周期100 ms;Byte0 ModeID,Byte1 State(0 IDLE、1 ENTRY、2 RUNNING、3 FAULT),Byte2 Phase(1 ENTRY、2 HORSE_FORWARD、3 HORSE_BACKWARD、5 CRADLE_FORWARD、6 CRADLE_RETURN;4保留),Byte3低半字节ActiveMask、高半字节FaultMask,Byte4..5 CycleIndex,Byte6..7 PhaseElapsedMs。
ACK:0 NONE、1 ACCEPTED、2 REJECTED、3 DUPLICATE、4 STOP_ALL_ACCEPTED。RejectReason 与 TaskState/Result 以移动矩阵“枚举定义”页为准。`0x301` Byte5 bit5 当前固定为0,预留给未来通信监控。 ACK:0 NONE、1 ACCEPTED、2 REJECTED、3 DUPLICATE、4 STOP_ALL_ACCEPTED。RejectReason 与 TaskState/Result 以移动矩阵“枚举定义”页为准。`0x301` Byte5 bit5 表示原生动态模式正在执行。
RejectReason:0 NONE、1 BAD_DLC、2 BAD_VERSION、3 BAD_OPCODE、4 BAD_MOTOR、5 BAD_DIRECTION、6 PARAM_RANGE、7 POSITION_INVALID、8 SEQUENCE_OLD、9 BUSY_SAME_MOTOR、10 INTERLOCK、11 FAULT_ACTIVE、12 SERVICE_MODE_REQUIRED、13 CAL_DATA_INVALID、14 INTERNAL_ERROR。 RejectReason:0 NONE、1 BAD_DLC、2 BAD_VERSION、3 BAD_OPCODE、4 BAD_MOTOR、5 BAD_DIRECTION、6 PARAM_RANGE、7 POSITION_INVALID、8 SEQUENCE_OLD、9 BUSY_SAME_MOTOR、10 INTERLOCK、11 FAULT_ACTIVE、12 SERVICE_MODE_REQUIRED、13 CAL_DATA_INVALID、14 INTERNAL_ERROR。
TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION、5 STOPPING、6 COMPLETE、7 FAULT。Result:0 NONE、1 RUNNING、2 OK、3 STOPPED_BY_CMD、4 ENDPOINT_REACHED、5 STALL、6 TIMEOUT、7 DRIVER_FAULT、8 HALL_FAULT、9 INVALID_CMD、10 INTERLOCKED、11 CALIBRATION_FAILED。 TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION、5 STOPPING、6 COMPLETE、7 FAULT。Result:0 NONE、1 RUNNING、2 OK、3 STOPPED_BY_CMD、4 ENDPOINT_REACHED、5 STALL、6 TIMEOUT、7 DRIVER_FAULT、8 HALL_FAULT、9 INVALID_CMD、10 INTERLOCKED、11 CALIBRATION_FAILED。
### 3.4 本地位置记忆(SMV8) ### 3.4 原生动态模式时序
- ModeID 1“木马模式”revision 13:入口 Hall `[320,190,500,430]`;向前俯摆到`[270,250,460,400]`,再向后仰摆到`[370,130,540,460]`,随后在两个端点间连续往复。M1峰峰行程120 Hall;M0峰峰行程100 Hall,以更大的水平速度和行程补偿M1斜向运动;每阶段先到目标前8 Hall会合,再以50% Duty完成等距末段,单段15 s仅作为无到位安全上限。
- ModeID 2“动态摇篮”revision 13:Step 1入口 Hall `[391,140,405,404]`;Step 1→Step 2四轴从阶段开始全程联动并以位置会合方式到`[246,281,701,494]`;Step 2→Step 3同步返回`[391,140,405,404]`。阶段完成条件是四轴全部目标到位,不再声明或等待固定30 s,也不允许任一轴延迟到末段追赶。
- 两种模式的每个运动阶段均在同一调度批次启动四轴;木马巡航占空比M0/M1/M2/M3为90/90/90/50%,动态摇篮为50/50/90/50%,到会合点后再同步执行等距末段。起动与到位离散均要求不大于1.0 s。
- Step 3与下一轮Step 1的坐标完全相同,因此该边界物理位移为0;ECU原子递增CycleIndex并立即同步启动下一轮Step 1→Step 2,不伪造一次零位移电机启动。
- 四轴命令同时启动,动态目标容差为±3 Hall。底层5 s无运动、堵转、驱动故障和最终到位超时仍会立即或在其既定保护时间内全停,并令`0x306.State=FAULT`。
- `STOP_DYNAMIC(0x0F)`和`STOP_ALL(0x09)`都必须终止循环;未收到停止指令时,ECU从启动命令起累计600 s后自动执行同等安全停止。正常或到时停止后`0x306`回到全零IDLE。
### 3.5 本地位置记忆(SMV8)
- 两个128-byte Work Flash 小扇区交替保存,记录含 Magic/Version、单调 Sequence、Clean标志、四轴绝对 Hall、MaxHall、最大速度、累计 Hall 上升沿、物理绝对量、PositionValid 和 CRC32。 - 两个128-byte Work Flash 小扇区交替保存,记录含 Magic/Version、单调 Sequence、Clean标志、四轴绝对 Hall、MaxHall、最大速度、累计 Hall 上升沿、物理绝对量、PositionValid 和 CRC32。
- 每个动作命令真正启动前先写入较新 dirty 记录;写完才允许 PWM 启动。若此时掉电,新记录阻止旧 clean 位置被恢复。 - 每个动作命令真正启动前先写入较新 dirty 记录;写完才允许 PWM 启动。若此时掉电,新记录阻止旧 clean 位置被恢复。
@@ -109,7 +121,8 @@ TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION
5. 每轴 300~500 ms 正反向点动并目视签字确认映射。 5. 每轴 300~500 ms 正反向点动并目视签字确认映射。
6. 单轴定时、同轴换向、单轴停止和四轴并发互不干涉。 6. 单轴定时、同轴换向、单轴停止和四轴并发互不干涉。
7. 已知量程回最近端、每次移动结束保存、正常掉电恢复、运动中掉电失效及恢复、绝对/相对位置精度。 7. 已知量程回最近端、每次移动结束保存、正常掉电恢复、运动中掉电失效及恢复、绝对/相对位置精度。
8. 堵转、驱动故障、CAN bus-off、C-SPY 结束和看门狗路径。 8. `07_dynamic_modes.py`:完整验证两个原生动态模式的命令ACK、0x306阶段顺序、阶段时长、四轴启动/到位离散度、自动跨周期和停止。
9. 堵转、驱动故障、CAN bus-off、C-SPY 结束和看门狗路径。
每个阶段开始前先发送全停,结束后再次发送全停。任何实物动作与预期不符时停止后续用例。 每个阶段开始前先发送全停,结束后再次发送全停。任何实物动作与预期不符时停止后续用例。
@@ -152,7 +165,20 @@ TaskState:0 IDLE、1 ACCEPTED、2 POSITION_RUN、3 TIMED_RUN、4 CALIBRATION
- 重复帧只返回 DUPLICATE,不产生第二次动作;旧 Sequence 拒绝且输出不变;旧 Sequence 的 STOP_ALL 仍必须执行。 - 重复帧只返回 DUPLICATE,不产生第二次动作;旧 Sequence 拒绝且输出不变;旧 Sequence 的 STOP_ALL 仍必须执行。
- 未标定时绝对/相对位置命令拒绝,CurrentPosition=`0xFFFF`;定时命令仍可用。 - 未标定时绝对/相对位置命令拒绝,CurrentPosition=`0xFFFF`;定时命令仍可用。
### 6.4 标定与位置 ### 6.4 动态模式
- 动态摇篮revision 13不使用固定30 s/60 s完成条件;Step 1→2和Step 2→3只在四轴全部进入±3 Hall目标带后切换,单段保护上限30 s。四轴启动离散度和最终到位离散度均不大于1.0 s;水平与靠背均达到25%行程时,坐盆归一化进度必须不小于15%,用于阻止末段追赶回归。
- 木马模式不设置停驻阶段;前俯与后仰只在四轴全部进入±3 Hall目标带后反向,单段保护上限15 s。入口只用于建立中间姿态,验收必须额外观察至少一个后仰→前俯→后仰的完整稳态循环。
- 台架轴在标称时间尚未到位时,只有Hall正常进展且在30 s窗口内到达精确目标才可继续循环;不得虚报到位或省略目标。无Hall进展、堵转、驱动故障或超过窗口必须进入FAULT并全停。
- 2026-08-04已验证公共动态运行时单次连续运行600.024 s后自动回到全零IDLE且无FAULT;循环中`STOP_DYNAMIC(0x0F)`也可立即安全停止。该公共安全逻辑由两个当前模式共用。
- 当前台架动态摇篮在向前收拢时由M1坐盆轴触发STALL;`0x306`故障位为`0x2`。独立单轴90%固定占空比复测同样在约45.2%(Hall约169)失速,无法到达75%(281 pulse)目标,证明阻断点不在动态调度。修复机构卡滞、负载、供电/驱动或Hall问题前,不得屏蔽堵转保护、降低需求目标或伪造循环PASS。
- 2026-08-04 revision 9重新设计并先执行M1已知量程回近端:Hall 169连续回到0,Near/PositionValid有效且MaxHall保持374。随后Step 1→2实测17.329 s,四轴启动/到位离散0.077/0.338 s;Step 2→3实测15.155 s,离散0.079/0.495 s;目标分别精确到`[246,281,701,494]`和`[391,140,405,405]`,CycleIndex进入1且无FAULT。下一轮四轴重新启动离散0.082 s,停止后`0x306`全零,PASS。
- 2026-08-04 revision 10移除M1延迟追赶后,Step 1→2在水平进度25.5%、靠背35.3%时坐盆已达44.0%;Step 2→3在水平25.5%、靠背28.7%时坐盆已达39.0%。启动离散0.070/0.050 s,到位离散0.342/0.339 s,目标精确、下一轮启动离散0.051 s、无FAULT、停止全零,PASS。
- 2026-08-04 revision 11木马模式实测稳态前俯/后仰启动离散0.080/0.079 s,到位离散0.234/0.232 s;水平到25%时坐盆已达41.7%/36.7%,目标误差不超过1 Hall,稳态完整周期约9.0 s。自动进入下一轮且停止后`0x306`全零,PASS。
- 2026-08-04 revision 12木马模式把坐盆峰峰行程由60增至120 Hall,实测稳态前俯/后仰启动离散0.054/0.081 s,到位离散0.389/0.182 s;水平到25%时坐盆已达46.6%/35.8%,目标误差不超过2 Hall,稳态完整周期约9.2 s。自动进入下一轮、无FAULT且停止后`0x306`全零,PASS。
- 2026-08-04 revision 13木马模式把M0峰峰行程由60增至100 Hall、巡航Duty由50%增至90%。按实测MaxHall=499换算水平峰峰行程约43.1 mm,扣除M1斜向行程估算的16.4 mm反向水平分量后净水平摆幅约26.7 mm。稳态前俯/后仰启动离散0.077/0.052 s,到位离散0.337/0.182 s;巡航检查点四轴均持续推进,目标误差不超过1 Hall,自动续环、无FAULT且停止全零,PASS。
### 6.5 标定与位置
- 每轴按负端→正端完成,标定中 `0x301 SystemState=CALIBRATION`、Byte5 bit6=1,`0x302 TaskState=CALIBRATION`,周期反馈不中断。 - 每轴按负端→正端完成,标定中 `0x301 SystemState=CALIBRATION`、Byte5 bit6=1,`0x302 TaskState=CALIBRATION`,周期反馈不中断。
- MaxHallCount 软件允许范围(实测值的70%~130%):M0 336~624,M1 261~486,M2 641~1192,M3 452~841,且大于等于8。台架目标值分别为480、374、917、647;超出允许范围标定失败。 - MaxHallCount 软件允许范围(实测值的70%~130%):M0 336~624,M1 261~486,M2 641~1192,M3 452~841,且大于等于8。台架目标值分别为480、374、917、647;超出允许范围标定失败。
@@ -74,6 +74,10 @@
#define MOTOR_SPEED_CONTROL_START_DELAY_MS (500ul) #define MOTOR_SPEED_CONTROL_START_DELAY_MS (500ul)
#define MOTOR_SPEED_CONTROL_CHECK_MS (100ul) #define MOTOR_SPEED_CONTROL_CHECK_MS (100ul)
#define MOTOR_SPEED_CONTROL_WINDOW_MS (500ul) #define MOTOR_SPEED_CONTROL_WINDOW_MS (500ul)
#define MOTOR_SPEED_PROGRESS_CONTROL_START_MS (100ul)
#define MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT (5u)
#define MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT (0u)
#define MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES (1ul)
#define MOTOR_SPEED_CONTROL_HISTORY_SAMPLES \ #define MOTOR_SPEED_CONTROL_HISTORY_SAMPLES \
(MOTOR_SPEED_CONTROL_WINDOW_MS / MOTOR_SPEED_CONTROL_CHECK_MS) (MOTOR_SPEED_CONTROL_WINDOW_MS / MOTOR_SPEED_CONTROL_CHECK_MS)
#define MOTOR_SPEED_TOLERANCE_PERCENT (10ul) #define MOTOR_SPEED_TOLERANCE_PERCENT (10ul)
@@ -210,6 +214,11 @@
#error "MOTOR_SPEED_CONTROL_WINDOW_MS must be a multiple of the speed check period." #error "MOTOR_SPEED_CONTROL_WINDOW_MS must be a multiple of the speed check period."
#endif #endif
#if ((MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT == 0u) || \
(MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT >= MOTOR_SPEED_MIN_DUTY_PERCENT))
#error "Adaptive speed duty must be below the fixed-duty command minimum."
#endif
#if ((MOTOR_PWM_DUTY_PERCENT < MOTOR_SPEED_MIN_DUTY_PERCENT) || \ #if ((MOTOR_PWM_DUTY_PERCENT < MOTOR_SPEED_MIN_DUTY_PERCENT) || \
(MOTOR_PWM_DUTY_PERCENT > MOTOR_SPEED_MAX_DUTY_PERCENT)) (MOTOR_PWM_DUTY_PERCENT > MOTOR_SPEED_MAX_DUTY_PERCENT))
#error "The default motor duty must stay inside the speed-control duty limits." #error "The default motor duty must stay inside the speed-control duty limits."
@@ -711,9 +720,9 @@ static bool Motor_CalculateSpeedPlan(uint32_t motorIndex,
profile->speedStepPpsX100 - 1ul) / profile->speedStepPpsX100 - 1ul) /
profile->speedStepPpsX100; profile->speedStepPpsX100;
initialDuty = dutySteps * MOTOR_SPEED_DUTY_STEP_PERCENT; initialDuty = dutySteps * MOTOR_SPEED_DUTY_STEP_PERCENT;
if(initialDuty < MOTOR_SPEED_MIN_DUTY_PERCENT) if(initialDuty < MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT)
{ {
initialDuty = MOTOR_SPEED_MIN_DUTY_PERCENT; initialDuty = MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT;
} }
else if(initialDuty > MOTOR_SPEED_MAX_DUTY_PERCENT) else if(initialDuty > MOTOR_SPEED_MAX_DUTY_PERCENT)
{ {
@@ -1187,6 +1196,21 @@ static void Motor_SafetyTaskFromIsr(void)
continue; continue;
} }
/* Long synchronized trajectories use pulse-density control whenever
* their requested speed is below the loaded mechanism's proven 50%
* breakaway duty. A zero-duty pause is intentional de-energization,
* not a no-motion fault. The absolute task/phase deadline still
* bounds the pause, and the activity window is reset when drive
* resumes. */
if((false != state->speedControlEnabled) &&
(state->speedCurrentDutyPercent ==
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT))
{
state->lowHallTracking = false;
state->stallCurrentTracking = false;
continue;
}
if(false != state->calibrating) if(false != state->calibrating)
{ {
state->lowHallTracking = false; state->lowHallTracking = false;
@@ -1804,6 +1828,14 @@ static void Motor_CommitPreparedHardwareStartUnsafe(uint32_t motorIndex)
(false == state->calibrating)) (false == state->calibrating))
{ {
dutyPercent = state->speedInitialDutyPercent; dutyPercent = state->speedInitialDutyPercent;
/* Dynamic trajectories may need a low steady-state PWM to stretch a
* short Hall travel over tens of seconds. Give every loaded axis the
* proven 50% breakaway pulse first; after the 500 ms measurement delay
* the closed loop may ramp below the public fixed-duty command floor. */
if(dutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
dutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
} }
state->speedCurrentDutyPercent = dutyPercent; state->speedCurrentDutyPercent = dutyPercent;
Motor_ResetSpeedMeasurementUnsafe(state); Motor_ResetSpeedMeasurementUnsafe(state);
@@ -2302,6 +2334,9 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
uint32_t elapsedMs; uint32_t elapsedMs;
uint32_t actualPpsX100; uint32_t actualPpsX100;
uint32_t targetPpsX100; uint32_t targetPpsX100;
uint32_t planElapsedMs;
uint32_t plannedProgressPulses;
uint32_t actualProgressPulses;
uint32_t savedIntrStatus; uint32_t savedIntrStatus;
uint8_t historyIndex; uint8_t historyIndex;
uint8_t newDutyPercent; uint8_t newDutyPercent;
@@ -2347,6 +2382,39 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
state->speedLastSampleTicks = nowTicks; state->speedLastSampleTicks = nowTicks;
elapsedTicks = (uint32_t)(nowTicks - baseTicks); elapsedTicks = (uint32_t)(nowTicks - baseTicks);
planElapsedMs = MOTOR_TICKS_TO_MS(
(uint32_t)(nowTicks - state->speedControlStartTicks));
if(planElapsedMs > state->speedDurationMs)
{
planElapsedMs = state->speedDurationMs;
}
plannedProgressPulses = (uint32_t)
(((uint64_t)state->speedPlannedHallPulses * planElapsedMs +
(state->speedDurationMs / 2ul)) / state->speedDurationMs);
actualProgressPulses = rawEdges - state->speedBaselineRawEdges;
/* Loaded axes need a short 50% breakaway pulse, but waiting for the full
* 500 ms speed window lets a short-travel axis build an unrecoverable
* lead. Position progress is meaningful from the first 100 ms sample,
* so de-energize an already-ahead axis immediately. Driving it at an
* arbitrary sub-breakaway duty can create current without Hall progress
* and was the cause of false cradle STALLs. */
if(((uint32_t)(nowTicks - state->speedControlStartTicks) >=
MOTOR_MS_TO_TICKS(MOTOR_SPEED_PROGRESS_CONTROL_START_MS)) &&
(actualProgressPulses >
(plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES)))
{
if(state->speedCurrentDutyPercent !=
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT)
{
state->speedCurrentDutyPercent =
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
Motor_ApplyDutyPercent(motorIndex,
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT);
}
Cy_SysLib_ExitCriticalSection(savedIntrStatus);
return;
}
if(((uint32_t)(nowTicks - state->speedControlStartTicks) < if(((uint32_t)(nowTicks - state->speedControlStartTicks) <
MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_START_DELAY_MS)) || MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_START_DELAY_MS)) ||
(elapsedTicks < MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_WINDOW_MS))) (elapsedTicks < MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_WINDOW_MS)))
@@ -2369,11 +2437,43 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
newDutyPercent = state->speedCurrentDutyPercent; newDutyPercent = state->speedCurrentDutyPercent;
state->speedActualPpsX100 = actualPpsX100; state->speedActualPpsX100 = actualPpsX100;
if((((uint64_t)actualPpsX100) * 100ull) < /* Speed-only regulation cannot synchronize very short and long axes: the
(((uint64_t)targetPpsX100) * * loaded motor breakaway pulse puts a short travel several seconds ahead
(100ul - MOTOR_SPEED_TOLERANCE_PERCENT))) * even when its eventual speed is correct. Track the time-position line
* as well. An axis ahead of the line drops immediately to the adaptive
* floor; an axis behind it ramps up. Within one Hall pulse, the ordinary
* measured-speed loop keeps each energized burst bounded. */
if(actualProgressPulses >
(plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES))
{ {
if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT) newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
}
else if((actualProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES) <
plannedProgressPulses)
{
if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT)
{
newDutyPercent = (uint8_t)
(newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT);
if(newDutyPercent > MOTOR_SPEED_MAX_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MAX_DUTY_PERCENT;
}
}
}
else if((((uint64_t)actualPpsX100) * 100ull) <
(((uint64_t)targetPpsX100) *
(100ul - MOTOR_SPEED_TOLERANCE_PERCENT)))
{
if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
}
else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT)
{ {
newDutyPercent = (uint8_t) newDutyPercent = (uint8_t)
(newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT); (newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT);
@@ -2400,6 +2500,10 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT; newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT;
} }
} }
else if(newDutyPercent != MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT)
{
newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT;
}
} }
else else
{ {
@@ -2408,6 +2512,15 @@ static void Motor_SpeedControlService(uint32_t motorIndex)
if(newDutyPercent != state->speedCurrentDutyPercent) if(newDutyPercent != state->speedCurrentDutyPercent)
{ {
if((state->speedCurrentDutyPercent ==
MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT) &&
(newDutyPercent >= MOTOR_SPEED_MIN_DUTY_PERCENT))
{
/* Resume gets a fresh 500 ms activity window, matching a normal
* hardware start. This prevents the first loaded-current sample
* from being compared with the intentionally quiet pause. */
Motor_ResetActivityWindowUnsafe(state);
}
state->speedCurrentDutyPercent = newDutyPercent; state->speedCurrentDutyPercent = newDutyPercent;
Motor_ApplyDutyPercent(motorIndex, newDutyPercent); Motor_ApplyDutyPercent(motorIndex, newDutyPercent);
} }
@@ -2275,6 +2275,12 @@
<file> <file>
<name>$PROJ_DIR$\..\App\seat_move_application.h</name> <name>$PROJ_DIR$\..\App\seat_move_application.h</name>
</file> </file>
<file>
<name>$PROJ_DIR$\..\App\seat_dynamic_mode.c</name>
</file>
<file>
<name>$PROJ_DIR$\..\App\seat_dynamic_mode.h</name>
</file>
<file> <file>
<name>$PROJ_DIR$\..\App\seat_move_store.c</name> <name>$PROJ_DIR$\..\App\seat_move_store.c</name>
</file> </file>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,577 @@
"""Revision-13 native dynamic-mode CAN/Hall verification for ZCANPRO.
Run this file from the ZCANPRO extension-script window after programming the
repository HEX. It validates one complete rocking-horse cycle and one
complete cradle cycle, then stops safely. STOP_ALL and thermal-off are always
attempted from ``finally``.
"""
import importlib
import os
import sys
import time
# ZCANPRO keeps one interpreter for scripts from multiple worktrees. Loading
# by a unique module name prevents an older desktop copy of ``hil_common``
# from winning through ``sys.modules`` or the retained import search path.
_BASE_DIR = os.path.dirname(os.path.abspath(__file__))
_COMMON_PATH = os.path.join(_BASE_DIR, "hil_common.py")
_COMMON_NAME = "cognitive_agent_dynamic_hil_common_v13"
_COMMON_SPEC = importlib.util.spec_from_file_location(_COMMON_NAME, _COMMON_PATH)
if _COMMON_SPEC is None or _COMMON_SPEC.loader is None:
raise RuntimeError("Cannot load repository hil_common.py")
_hil_common = importlib.util.module_from_spec(_COMMON_SPEC)
sys.modules[_COMMON_NAME] = _hil_common
_COMMON_SPEC.loader.exec_module(_hil_common)
HilSession = _hil_common.HilSession
command_payload = _hil_common.command_payload
parse_302 = _hil_common.parse_302
parse_303 = _hil_common.parse_303
parse_306 = _hil_common.parse_306
SELECTED_MODES = ("ROCKING_HORSE", "DYNAMIC_CRADLE")
EXPECTED_REVISION = 13
EXPECTED_MAX_HALL = (480, 374, 917, 647)
HALL_TOLERANCE = 3
PHASE_DURATION_TOLERANCE_S = 0.55
ARRIVAL_SPREAD_TOLERANCE_S = 1.00
ARRIVAL_EARLY_TOLERANCE_S = 1.00
LINKED_REFERENCE_PROGRESS = 0.25
LINKED_MIN_CUSHION_PROGRESS = 0.15
MODE_DEFINITIONS = {
"ROCKING_HORSE": {
"mode_id": 1,
"sequence": 0x0700,
"first_phase": 2,
"cycle_duration_ms": None,
"entry_target": (320, 190, 500, 430),
"phase_order": (2, 3),
"phases": {
2: {
"name": "rock_forward",
"completion": "all_targets_reached",
"timeout_ms": 15000,
"target": (270, 250, 460, 400),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
3: {
"name": "rock_backward",
"completion": "all_targets_reached",
"timeout_ms": 15000,
"target": (370, 130, 540, 460),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
},
},
"DYNAMIC_CRADLE": {
"mode_id": 2,
"sequence": 0x0720,
"first_phase": 5,
"cycle_duration_ms": None,
"entry_target": (391, 140, 405, 404),
"phase_order": (5, 6),
"phases": {
5: {
"name": "forward",
"completion": "all_targets_reached",
"timeout_ms": 30000,
"target": (246, 281, 701, 494),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
6: {
"name": "return",
"completion": "all_targets_reached",
"timeout_ms": 30000,
"target": (391, 140, 405, 404),
"moving_axes": (0, 1, 2, 3),
"continuous_four_axis_cruise": True,
},
},
},
}
session = None
def z_notify(event_type, obj):
del obj
if event_type == "stop" and session is not None:
session.request_stop()
def hall_snapshot(s, capture_s=0.35):
if capture_s > 0:
s.capture_state(capture_s, clear=False)
values = []
for motor in range(4):
diagnostic = s.latest.get((0x303, motor))
values.append(None if diagnostic is None else diagnostic["hall"])
return values
def hall_match(actual, expected):
return all(
value is not None and abs(value - target) <= HALL_TOLERANCE
for value, target in zip(actual, expected)
)
def wait_idle_status(s, timeout_s=3.0):
deadline = time.monotonic() + timeout_s
latest = None
while time.monotonic() < deadline and not s.stop_requested:
for can_id, data, frame in s.receive():
del frame
if can_id != 0x306:
continue
latest = parse_306(data)
if latest is not None and latest["mode_id"] == 0 and \
latest["state"] == 0:
return latest
time.sleep(0.005)
return latest
def stop_dynamic(s, sequence, label):
response = s.send_command(
command_payload(0x0F, 0, 0, sequence),
expected_ack=1,
expected_reject=0,
timeout_s=2.0,
note=label + " STOP_DYNAMIC",
)
idle = wait_idle_status(s)
passed = response is not None and response["ack"] == 1 and \
response["reject"] == 0 and idle is not None and \
idle["mode_id"] == 0 and idle["state"] == 0
s.record(label + "-STOP", passed,
{"ack": response, "dynamic_status": idle},
"Opcode 0x0F accepted and 0x306 returns IDLE")
return passed
def new_segment(status, observed_at, start_hall):
phase_start = observed_at - status["phase_elapsed_ms"] / 1000.0
return {
"cycle_index": status["cycle_index"],
"phase": status["phase"],
"phase_start": phase_start,
"first_observed": observed_at,
"last_status": dict(status),
"first_active": {},
"arrival": {},
"arrival_source": {},
"terminal": {},
"target_hall_at_end": None,
"start_hall": list(start_hall),
"latest_hall": list(start_hall),
"cruise_checkpoint": None,
}
def observe_cycle(s, label, definition):
expected_order = definition["phase_order"]
first_phase = definition["first_phase"]
segments = []
current = None
started = False
deadline = time.monotonic() + 115.0
failure_status = None
while time.monotonic() < deadline and not s.stop_requested:
frames = s.receive()
if not frames:
time.sleep(0.005)
continue
now = time.monotonic()
for can_id, data, frame in frames:
del frame
if can_id == 0x306:
status = parse_306(data)
if status is None or status["mode_id"] != definition["mode_id"]:
continue
if status["state"] == 3:
failure_status = status
break
phase = status["phase"]
if phase not in definition["phases"]:
continue
key = (status["cycle_index"], phase)
current_key = None if current is None else (
current["cycle_index"], current["phase"])
if key != current_key:
if current is not None:
current["target_hall_at_end"] = hall_snapshot(s, 0)
segments.append(current)
current = new_segment(status, now, hall_snapshot(s, 0))
if phase == first_phase:
if not started:
started = True
elif status["cycle_index"] >= 1:
# Capture the returned Step-1 pose now, before the
# next Step-1 -> Step-2 movement changes Hall.
current["target_hall_at_end"] = hall_snapshot(s, 0)
elif current is not None:
current["last_status"] = dict(status)
elif can_id == 0x302 and current is not None:
motor = parse_302(data)
if motor is None or motor["motor"] >= 4:
continue
motor_id = motor["motor"]
if motor["active"]:
current["first_active"].setdefault(motor_id, now)
if current["cycle_index"] >= 1 and \
current["phase"] == first_phase and all(
axis in current["first_active"]
for axis in definition["phases"][first_phase]["moving_axes"]
):
# Step 3 and Step 1 have the same Hall pose, so their
# boundary has no physical displacement. The next
# observable requirement is that all four axes launch
# the following Step-1 -> Step-2 leg synchronously.
segments.append(current)
current = None
break
elif motor_id in current["first_active"]:
current["terminal"][motor_id] = {
"time": now,
"status": dict(motor),
}
if definition["phases"][current["phase"]].get(
"completion") != "all_targets_reached":
current["arrival"].setdefault(motor_id, now)
current["arrival_source"].setdefault(
motor_id, "0x302 COMPLETE")
elif can_id == 0x303:
diagnostic = parse_303(data)
if diagnostic is not None and diagnostic["fault_flags"]:
failure_status = diagnostic
break
if diagnostic is not None and current is not None:
motor_id = diagnostic["motor"]
current["latest_hall"][motor_id] = diagnostic["hall"]
target = definition["phases"][current["phase"]]["target"][motor_id]
if motor_id in current["first_active"] and \
abs(diagnostic["hall"] - target) <= HALL_TOLERANCE:
# A phase may roll into the next command before the
# staggered 0x302 slot publishes COMPLETE for the last
# axis. The independent 0x303 Hall target is equivalent
# physical arrival evidence and avoids a telemetry race.
current["arrival"].setdefault(motor_id, now)
current["arrival_source"].setdefault(
motor_id, "0x303 Hall target")
phase_definition = definition["phases"][current["phase"]]
if phase_definition.get("continuous_four_axis_cruise") and \
current["cruise_checkpoint"] is None:
progress = []
for axis in range(4):
start = current["start_hall"][axis]
end = phase_definition["target"][axis]
hall = current["latest_hall"][axis]
if start is None or hall is None:
progress.append(0.0)
else:
travel = abs(end - start)
progress.append(
1.0 if travel == 0 else
min(1.0, abs(hall - start) / float(travel))
)
if progress[0] >= LINKED_REFERENCE_PROGRESS and \
progress[2] >= LINKED_REFERENCE_PROGRESS:
current["cruise_checkpoint"] = {
"elapsed_s": now - current["phase_start"],
"normalized_progress": progress,
"hall": list(current["latest_hall"]),
}
if failure_status is not None:
break
if current is None and started and segments and \
segments[-1]["cycle_index"] >= 1 and \
segments[-1]["phase"] == first_phase:
break
complete = (
failure_status is None
and len(segments) == len(expected_order) + 1
and tuple(item["phase"] for item in segments[:-1]) == expected_order
and segments[-1]["phase"] == first_phase
and segments[-1]["cycle_index"] >= 1
)
return complete, segments, failure_status
def validate_segments(s, label, definition, segments):
phase_results = []
for index, segment in enumerate(segments[:-1]):
phase_definition = definition["phases"][segment["phase"]]
next_segment = segments[index + 1]
observed_duration = next_segment["phase_start"] - segment["phase_start"]
target_completion = phase_definition.get("completion") == \
"all_targets_reached"
if target_completion:
expected_duration = None
arrival_grace = 0.0
timeout_s = phase_definition["timeout_ms"] / 1000.0
duration_ok = 0.0 < observed_duration <= (
timeout_s + PHASE_DURATION_TOLERANCE_S)
else:
expected_duration = phase_definition["duration_ms"] / 1000.0
arrival_grace = phase_definition["arrival_grace_ms"] / 1000.0
timeout_s = expected_duration + arrival_grace
duration_ok = (
observed_duration >= expected_duration -
PHASE_DURATION_TOLERANCE_S
and observed_duration <= expected_duration + arrival_grace +
PHASE_DURATION_TOLERANCE_S
)
moving_axes = phase_definition["moving_axes"]
terminals = segment["terminal"]
arrivals = segment["arrival"]
active_starts = segment["first_active"]
if moving_axes:
all_active_seen = all(axis in active_starts for axis in moving_axes)
all_arrival_seen = all(axis in arrivals for axis in moving_axes)
start_times = [active_starts[axis] for axis in moving_axes
if axis in active_starts]
terminal_times = [arrivals[axis] for axis in moving_axes
if axis in arrivals]
start_spread = max(start_times) - min(start_times) \
if len(start_times) == len(moving_axes) else None
arrival_spread = max(terminal_times) - min(terminal_times) \
if all_arrival_seen else None
latest_arrival = max(terminal_times) - segment["phase_start"] \
if all_arrival_seen else None
terminal_status_ok = all_arrival_seen if target_completion else all(
value["status"]["task"] == 6
and value["status"]["result"] == 2
for value in terminals.values()
) and all_arrival_seen
used_arrival_grace = False if target_completion else (
observed_duration > expected_duration +
PHASE_DURATION_TOLERANCE_S
)
sync_ok = (
all_active_seen
and all_arrival_seen
and start_spread <= ARRIVAL_SPREAD_TOLERANCE_S
# When the loaded bench cannot meet the nominal arrival time,
# faster axes stop at target while the healthy slow axis uses
# its bounded grace. Simultaneous start remains mandatory;
# arrival spread is only an exact-timing assertion when the
# phase did not need that grace.
and arrival_spread <= ARRIVAL_SPREAD_TOLERANCE_S
and (target_completion or latest_arrival >=
expected_duration - ARRIVAL_EARLY_TOLERANCE_S)
and latest_arrival <= timeout_s + PHASE_DURATION_TOLERANCE_S
and terminal_status_ok
)
else:
start_spread = 0.0
arrival_spread = 0.0
latest_arrival = None
used_arrival_grace = False
sync_ok = not active_starts and segment["last_status"]["active_mask"] == 0
target_ok = hall_match(
segment["target_hall_at_end"], phase_definition["target"]
)
checkpoint = segment["cruise_checkpoint"]
if phase_definition.get("continuous_four_axis_cruise"):
cruise_ok = checkpoint is not None and \
checkpoint["normalized_progress"][1] >= \
LINKED_MIN_CUSHION_PROGRESS
else:
cruise_ok = True
passed = duration_ok and sync_ok and target_ok and cruise_ok
details = {
"phase": phase_definition["name"],
"cycle_index": segment["cycle_index"],
"expected_duration_s": expected_duration,
"arrival_grace_s": arrival_grace,
"completion": phase_definition.get("completion", "elapsed_time"),
"timeout_s": timeout_s,
"observed_duration_s": round(observed_duration, 3),
"used_arrival_grace": used_arrival_grace,
"moving_axes": list(moving_axes),
"active_start_spread_s": None if start_spread is None else round(start_spread, 3),
"arrival_spread_s": None if arrival_spread is None else round(arrival_spread, 3),
"latest_arrival_from_phase_start_s": (
None if latest_arrival is None else round(latest_arrival, 3)
),
"terminal_status": {
str(axis): terminals[axis]["status"]
for axis in terminals
},
"arrival_source": {
str(axis): segment["arrival_source"][axis]
for axis in arrivals
},
"actual_hall_at_phase_end": segment["target_hall_at_end"],
"expected_hall_at_phase_end": list(phase_definition["target"]),
"duration_ok": duration_ok,
"synchronization_ok": sync_ok,
"target_ok": target_ok,
"continuous_four_axis_cruise_ok": cruise_ok,
"cruise_checkpoint": checkpoint,
}
s.record("%s-%s" % (label, phase_definition["name"].upper()),
passed, details,
"continuous cruise, synchronized start/arrival, and Hall target within tolerance")
phase_results.append(passed)
cycle_duration = segments[-1]["phase_start"] - segments[0]["phase_start"]
if definition["cycle_duration_ms"] is None:
cycle_expected = None
cycle_grace = 0.0
cycle_limit = sum(
definition["phases"][phase]["timeout_ms"]
for phase in definition["phase_order"]
) / 1000.0
cycle_ok = 0.0 < cycle_duration <= cycle_limit + \
PHASE_DURATION_TOLERANCE_S
else:
cycle_expected = definition["cycle_duration_ms"] / 1000.0
cycle_grace = sum(
definition["phases"][phase]["arrival_grace_ms"]
for phase in definition["phase_order"]
) / 1000.0
cycle_tolerance = PHASE_DURATION_TOLERANCE_S * \
len(definition["phase_order"])
cycle_limit = cycle_expected + cycle_grace
cycle_ok = (
cycle_duration >= cycle_expected - cycle_tolerance
and cycle_duration <= cycle_limit + cycle_tolerance
)
repeat_hall = segments[-1]["target_hall_at_end"]
repeat_starts = segments[-1]["first_active"]
repeat_axes = definition["phases"][first_phase]["moving_axes"]
repeat_start_values = [repeat_starts[axis] for axis in repeat_axes
if axis in repeat_starts]
repeat_start_spread = max(repeat_start_values) - min(repeat_start_values) \
if len(repeat_start_values) == len(repeat_axes) else None
repeat_ok = hall_match(repeat_hall, definition["entry_target"]) and \
repeat_start_spread is not None and \
repeat_start_spread <= ARRIVAL_SPREAD_TOLERANCE_S
s.record(label + "-CYCLE-REPEAT", cycle_ok and repeat_ok,
{"expected_cycle_s": cycle_expected,
"maximum_cycle_s": cycle_limit,
"observed_cycle_s": round(cycle_duration, 3),
"cycle_index": segments[-1]["cycle_index"],
"actual_repeat_hall": repeat_hall,
"expected_repeat_hall": list(definition["entry_target"]),
"next_cycle_active_start_spread_s": (
None if repeat_start_spread is None else
round(repeat_start_spread, 3)
)},
"one complete cycle reaches Step 1 and synchronously starts the next Step 2 leg")
return all(phase_results) and cycle_ok and repeat_ok
def run_mode(s, label, definition):
start = s.send_command(
command_payload(0x0E, 0, 0, definition["sequence"],
definition["mode_id"], 0),
expected_ack=1,
expected_reject=0,
timeout_s=2.0,
note=label + " START_DYNAMIC_MODE",
)
accepted = start is not None and start["ack"] == 1 and \
start["reject"] == 0 and start["opcode"] == 0x0E
s.record(label + "-START", accepted, start,
"Opcode 0x0E accepted for native mode_id=%d" % definition["mode_id"])
if not accepted:
return False
complete, segments, failure = observe_cycle(s, label, definition)
s.record(label + "-PHASE-SEQUENCE", complete,
{"observed": [
{"cycle_index": item["cycle_index"], "phase": item["phase"]}
for item in segments
], "failure": failure},
"0x306 reports one ordered cycle and automatic repeat")
validated = complete and validate_segments(s, label, definition, segments)
stopped = stop_dynamic(s, definition["sequence"] + 1, label)
return validated and stopped
def z_main():
global session
session = None
results = {}
try:
selected = tuple(str(value).strip().upper() for value in SELECTED_MODES)
unknown = [value for value in selected if value not in MODE_DEFINITIONS]
if unknown:
raise RuntimeError("Unknown SELECTED_MODES=" + repr(unknown))
session = HilSession("07_dynamic_modes")
s = session
s.write_operator_template([
"Secure the empty seat bench and keep hardware E-stop reachable.",
"ROCKING_HORSE pitches forward/back smoothly with all axes engaged: PASS/FAIL ___",
"DYNAMIC_CRADLE moves forward/returns smoothly without steps: PASS/FAIL ___",
"No unintended thermal output, noise, collision, or cable strain: PASS/FAIL ___",
])
s.thermal_off()
s.stop_all(0)
initial = s.capture_state(2.0)
system = initial["system"]
ready = (
system is not None
and system["firmware_revision"] == EXPECTED_REVISION
and system["position_valid_mask"] == 0x0F
and system["active_mask"] == 0
and system["faulted_mask"] == 0
and all(
item is not None
and abs(item["max_hall"] - EXPECTED_MAX_HALL[index])
<= max(4, round(EXPECTED_MAX_HALL[index] * 0.05))
for index, item in enumerate(initial["diagnostics"])
)
)
s.record("DYNAMIC-PRECONDITION", ready, initial,
"revision 13, four valid axes, no active/fault, confirmed ranges")
if not ready:
for label in selected:
s.record(label + "-MODE", False, "not executed",
"dynamic precondition failed", status="NOT_RUN")
return
for label in selected:
if s.stop_requested:
break
results[label] = run_mode(s, label, MODE_DEFINITIONS[label])
# A native-mode failure is isolated by STOP_DYNAMIC inside
# run_mode(). Continue with the other definition so one
# physically infeasible trajectory cannot hide its HIL result.
time.sleep(0.5)
except Exception as error:
if session is not None:
session.record("SCRIPT", False, repr(error), "unhandled exception")
finally:
if session is not None:
try:
session.stop_all(0)
session.thermal_off()
except Exception as error:
session.log("FINAL_SAFE_OFF_ERROR " + repr(error))
final_state = None
try:
final_state = session.capture_state(1.0)
except Exception as error:
session.log("FINAL_CAPTURE_ERROR " + repr(error))
session.finish({
"selected_modes": list(SELECTED_MODES),
"mode_results": results,
"expected_firmware_revision": EXPECTED_REVISION,
"hall_tolerance_pulses": HALL_TOLERANCE,
"phase_duration_tolerance_s": PHASE_DURATION_TOLERANCE_S,
"arrival_spread_tolerance_s": ARRIVAL_SPREAD_TOLERANCE_S,
"final_state": final_state,
})
@@ -41,6 +41,26 @@ Bus-Off、加热ADC错误和DRV8718故障只有在具备对应故障注入条件
脚本启动时会强制重新加载 `hil_common.py`。ZCANPRO使用常驻Python解释器,若不重载,修改公共辅助模块后再次运行可能继续使用旧类定义;日志中的 `HIL_COMMON_VERSION` 可用于确认实际加载版本。 脚本启动时会强制重新加载 `hil_common.py`。ZCANPRO使用常驻Python解释器,若不重载,修改公共辅助模块后再次运行可能继续使用旧类定义;日志中的 `HIL_COMMON_VERSION` 可用于确认实际加载版本。
### Revision 13 动态模式
烧录 revision 13 后,在 ZCANPRO 扩展脚本窗口运行 `07_dynamic_modes.py`。脚本先验证
`0x301` 的固件版本、四轴 PositionValid/MaxHall 与故障状态,再依次运行木马模式和动态摇篮。
验收证据包括 `0x306` 阶段顺序/自动续环、`0x302` 四轴同步启停与终态、`0x303` 精确 Hall
目标;任一失败或脚本停止都会发送 `STOP_ALL` 并关闭热管理。两种模式均须由停止指令终止,
未收到停止时由ECU在启动累计10分钟后自动停止。
木马入口为`[320,190,500,430]`,随后在前俯`[270,250,460,400]`和后仰
`[370,130,540,460]`之间连续往复,不设置停驻。坐盆峰峰行程120 Hall、折算垂直分量约
18 mm;水平峰峰行程100 Hall且巡航占空比90%,用于抵消坐盆斜向运动的反向水平分量。每个半周期四轴同批启动,先到目标前
8 Hall会合,再以50% Duty完成等距末段。配置入口为`Config/seat_dynamic_mode_config.h`,N7
长期库`config/modes/vehicles/denza_n7/rocking_horse.yaml`必须镜像同一参数。
2026-08-04 revision 13实物复测:稳态前俯/后仰启动离散0.077/0.052 s,到位离散
0.337/0.182 s;四轴在巡航检查点均持续推进,目标误差不超过1 Hall。按实测M0 MaxHall=499
换算水平峰峰行程约43.1 mm,扣除坐盆估算反向水平分量后净摆幅约26.7 mm;自动进入下一轮,
`STOP_DYNAMIC(0x0F)`后`0x306`全零且无FAULT,PASS。证据为
`artifacts/hil/rocking_horse_rev13_horizontal_compensation_20260804.json`。
- 通风1/2/3档、关闭;加热1/2/3档、关闭;通风2档+加热2档。 - 通风1/2/3档、关闭;加热1/2/3档、关闭;通风2档+加热2档。
- 每轴从当前位置偏移约12%并回到原位置,验证M0/M1/M2/M3的执行器、方向、CAN ACK、终态位置和Hall反馈。 - 每轴从当前位置偏移约12%并回到原位置,验证M0/M1/M2/M3的执行器、方向、CAN ACK、终态位置和Hall反馈。
- 若SMV8位置有效则直接使用且不发生归零动作;若某轴无效则先仅将该轴回最近端,验证Hall≈0与MaxHall保持不变后再测试。 - 若SMV8位置有效则直接使用且不发生归零动作;若某轴无效则先仅将该轴回最近端,验证Hall≈0与MaxHall保持不变后再测试。
@@ -9,8 +9,8 @@ import time
import zcanpro import zcanpro
SEAT_RX_IDS = (0x301, 0x302, 0x303, 0x304, 0x305, 0x4FA) SEAT_RX_IDS = (0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x4FA)
HIL_COMMON_VERSION = "2026-07-30-hall-current-stall-v4" HIL_COMMON_VERSION = "2026-08-04-dynamic-modes-v8"
CONFIRMED_MAX_HALL = (480, 374, 917, 647) CONFIRMED_MAX_HALL = (480, 374, 917, 647)
HOME_KNOWN_RANGE_KEY = 0x4B52 HOME_KNOWN_RANGE_KEY = 0x4B52
HOME_LEG_TIMEOUT_S = 120 HOME_LEG_TIMEOUT_S = 120
@@ -112,6 +112,20 @@ def parse_305(data):
} }
def parse_306(data):
if data is None or len(data) < 8:
return None
return {
"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),
}
def command_payload(opcode, motor, direction, sequence, param_a=0, def command_payload(opcode, motor, direction, sequence, param_a=0,
param_b=0, version=1): param_b=0, version=1):
control = ((version & 0x03) << 4) | ((direction & 0x03) << 2) | \ control = ((version & 0x03) << 4) | ((direction & 0x03) << 2) | \
@@ -228,6 +242,8 @@ class HilSession(object):
value["hall_activity_low"] value["hall_activity_low"]
elif can_id == 0x305: elif can_id == 0x305:
self.latest[0x305] = parse_305(data) self.latest[0x305] = parse_305(data)
elif can_id == 0x306:
self.latest[0x306] = parse_306(data)
elif can_id == 0x4FA: elif can_id == 0x4FA:
self.latest[0x4FA] = data self.latest[0x4FA] = data
output.append((can_id, data, frame)) output.append((can_id, data, frame))
@@ -311,7 +327,7 @@ class HilSession(object):
def capture_state(self, duration_s=2.0, clear=True): def capture_state(self, duration_s=2.0, clear=True):
if clear: if clear:
for key in list(self.latest): for key in list(self.latest):
if key in (0x301, 0x305) or isinstance(key, tuple): if key in (0x301, 0x305, 0x306) or isinstance(key, tuple):
del self.latest[key] del self.latest[key]
deadline = time.monotonic() + duration_s deadline = time.monotonic() + duration_s
while time.monotonic() < deadline and not self.stop_requested: while time.monotonic() < deadline and not self.stop_requested:
@@ -325,6 +341,7 @@ class HilSession(object):
"currents": [self.latest.get((0x304, motor)) "currents": [self.latest.get((0x304, motor))
for motor in range(4)], for motor in range(4)],
"thermal_current": self.latest.get(0x305), "thermal_current": self.latest.get(0x305),
"dynamic_mode": self.latest.get(0x306),
} }
def reset_current_peaks(self, motor=None): def reset_current_peaks(self, motor=None):
@@ -0,0 +1,51 @@
version: 1.0.0
status: imported_from_can_dev_and_runtime_verified
source:
repository: https://git.kendeji.fun/dev-team/project-code
branch: CAN_DEV
commit: 6e7ed04a468dfcde7efda9e30bc15144bf02faf9
implementation: Cognitive_Agent/native/canfd/can_read_trans.cpp
adapter:
device: ZLG_USBCANFD_200U
device_type: 41
device_index: 0
channel_env: COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL
default_channel: 1
termination_default: false
can_fd:
clock_hz: 60000000
controller_mode: 0
work_mode: normal
standard: iso
arbitration:
bitrate: 500000
tseg1: 14
tseg2: 3
sjw: 2
smp: 0
brp: 5
total_tq: 20
sample_point_pct: 80
data:
bitrate: 2000000
tseg1: 10
tseg2: 2
sjw: 2
smp: 0
brp: 1
total_tq: 15
sample_point_pct: 80
air_outlet:
direction_convention:
horizontal: minimum_left_maximum_right
vertical: minimum_down_maximum_up
ranges:
driver_left: {horizontal: [-25, 40], vertical: [-40, 30]}
driver_right: {horizontal: [-40, 30], vertical: [-40, 30]}
passenger_left: {horizontal: [-10, 40], vertical: [-40, 30]}
passenger_right: {horizontal: [-40, 5], vertical: [-40, 30]}
safety:
raw_frame_passthrough: false
body_channel_must_differ_from_seat_channel: true
send_only_named_capabilities: true
default_feedback_timeout_ms: 1200
@@ -0,0 +1,88 @@
VERSION "CAN_DEV native vehicle controls 6e7ed04"
NS_ :
NS_DESC_
CM_
BA_DEF_
BA_
VAL_
BS_:
BU_: CognitiveAgent VehicleECU
BO_ 478 Media_0x1DE: 8 CognitiveAgent
SG_ Wind_Model_S : 0|4@1+ (1,0) [0|7] "" VehicleECU
SG_ Wind_Volume_Size_S : 4|4@1+ (1,0) [0|7] "" VehicleECU
SG_ Medium_AC_Ctrl_Way_S : 16|2@1+ (1,0) [0|2] "" VehicleECU
SG_ ACB_Close_Button_S : 28|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Ctrl_Set_Main_Driver_Tempt_S : 32|8@1+ (1,0) [18|32] "degC" VehicleECU
SG_ Ctrl_Set_Copilot_Tempratur_S : 40|8@1+ (1,0) [18|32] "degC" VehicleECU
SG_ Ctrl_Set_Rear_Temperature_S : 48|8@1+ (1,0) [18|32] "degC" VehicleECU
BO_ 1028 Right_BCM_0x404: 8 VehicleECU
SG_ ACHMC_AC_On_Status_S : 8|2@1+ (1,0) [0|2] "" CognitiveAgent
SG_ AC_Ctrl_Way_S : 10|2@1+ (1,0) [0|2] "" CognitiveAgent
SG_ AC_Cycle_Mode_404_S : 12|2@1+ (1,0) [0|3] "" CognitiveAgent
SG_ AC_Wind_Model_404_S : 16|4@1+ (1,0) [0|7] "" CognitiveAgent
SG_ Ac_wind_rnk_S : 20|4@1+ (1,0) [0|7] "" CognitiveAgent
SG_ Main_Driver_Set_Temperature_S : 32|8@1+ (1,0) [0|255] "degC" CognitiveAgent
SG_ Copilot_Driver_Set_Temperature_S : 40|8@1+ (1,0) [0|255] "degC" CognitiveAgent
BO_ 811 Media_0x32B: 8 CognitiveAgent
SG_ Sub_ID : 0|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Second_Row_Lock_Request_Instruction_S : 14|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Indoor_Light_Request_Command_S : 24|3@1+ (1,0) [0|2] "" VehicleECU
SG_ Atmosphere_Light_Request_Command_S : 27|5@1+ (1,0) [0|7] "" VehicleECU
SG_ Ambient_Red_S : 8|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Ambient_Green_S : 16|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Ambient_Blue_S : 24|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Ambient_Brightness_S : 32|7@1+ (1,0) [0|100] "%" VehicleECU
BO_ 1073 Media_0x431: 8 CognitiveAgent
SG_ Shade_Voi_Ctrl_Instrct_S : 4|4@1+ (1,0) [0|4] "" VehicleECU
SG_ Left_Front_Window_Ctrl_S : 48|3@1+ (1,0) [0|3] "" VehicleECU
SG_ Right_Front_Window_Ctrl_S : 51|3@1+ (1,0) [0|3] "" VehicleECU
SG_ Left_Rear_Window_Ctrl_S : 56|3@1+ (1,0) [0|3] "" VehicleECU
SG_ Right_Rear_Window_Ctrl_S : 59|3@1+ (1,0) [0|3] "" VehicleECU
BO_ 1217 Media_0x4C1: 8 CognitiveAgent
SG_ Sub_ID : 0|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Driver_Vent_S : 8|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Driver_Heat_S : 12|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Passenger_Vent_S : 16|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Passenger_Heat_S : 20|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Left_Rear_Vent_S : 24|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Left_Rear_Heat_S : 28|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Right_Rear_Vent_S : 32|4@1+ (1,0) [0|5] "" VehicleECU
SG_ Right_Rear_Heat_S : 36|4@1+ (1,0) [0|5] "" VehicleECU
BO_ 1269 Media_0x4F5: 8 CognitiveAgent
SG_ Sunroof_Target_Position_S : 24|8@1+ (1,0) [0|255] "%" VehicleECU
SG_ Shade_Target_Position_S : 32|8@1+ (1,0) [0|255] "%" VehicleECU
SG_ RoofLamp_Switch_Set_Item_4F5_S : 50|2@1+ (1,0) [0|2] "" VehicleECU
BO_ 1263 Media_0x4EF: 8 CognitiveAgent
SG_ Sub_ID : 0|8@1+ (1,0) [0|255] "" VehicleECU
SG_ Outlet_Up_Down_1_S : 8|8@1+ (1,-128) [-128|126] "" VehicleECU
SG_ Outlet_Up_Down_2_S : 16|8@1+ (1,-128) [-128|126] "" VehicleECU
SG_ Outlet_Left_Right_1_S : 24|8@1+ (1,-128) [-128|126] "" VehicleECU
SG_ Outlet_Left_Right_2_S : 32|8@1+ (1,-128) [-128|126] "" VehicleECU
BO_ 434 Media_0x1B2: 8 CognitiveAgent
SG_ Front_Left_Reading_Light_S : 0|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Front_Right_Reading_Light_S : 2|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Middle_Left_Reading_Light_S : 4|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Middle_Right_Reading_Light_S : 6|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Middle_Left_Rotary_Light_S : 8|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Middle_Right_Rotary_Light_S : 10|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Rear_Left_Reading_Light_S : 12|2@1+ (1,0) [0|2] "" VehicleECU
SG_ Rear_Right_Reading_Light_S : 14|2@1+ (1,0) [0|2] "" VehicleECU
BO_ 904 Window_Status_0x388: 8 VehicleECU
BO_ 1031 Door_Status_0x407: 8 VehicleECU
BO_ 1066 Seat_Status_0x42A: 8 VehicleECU
BO_ 1070 Light_Status_0x42E: 8 VehicleECU
BO_ 757 Outlet_Status_0x2F5: 8 VehicleECU
BO_ 891 Outlet_Status_0x37B: 8 VehicleECU
BO_ 1050 Shade_Status_0x41A: 8 VehicleECU
+26
View File
@@ -0,0 +1,26 @@
apiVersion: cognitive-agent/v1alpha1
kind: PlatformManifest
metadata:
id: t2g-cabin-bench-platform
name: T2G seat and vehicle dual-channel CAN bench
version: 1.0.0
description: One ZLG USBCANFD-200U owns seat CAN channel 0 and vehicle CAN FD channel 1.
spec:
platform: t2g_cabin_bench
execution_enabled: true
dbc_files:
- ../t2g_seat_bench/can/dbc/seat.dbc
- can/dbc/vehicle-native.dbc
buses:
- name: seat_can
backend: zlgcan
channel_env: COGNITIVE_AGENT_SEAT_CAN_CHANNEL
fd: false
- name: body_can
backend: zlgcan
channel_env: COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL
fd: true
supported_domains: [seat, climate, door, window, outlet, interior_light, ambient_light, roof]
calibrations:
seat: ../t2g_seat_bench/calibration/seat.yaml
body_can: calibration/body_can.yaml
@@ -13,6 +13,9 @@ protocol:
absolute_position_opcode: 1 absolute_position_opcode: 1
timed_duty_opcode: 3 timed_duty_opcode: 3
stop_all_opcode: 9 stop_all_opcode: 9
start_dynamic_mode_opcode: 14
stop_dynamic_mode_opcode: 15
dynamic_status_frame_id: 774
stop_all_motor_selector: 0 stop_all_motor_selector: 0
home_known_range_opcode: 11 home_known_range_opcode: 11
home_known_range_key: 19282 home_known_range_key: 19282
@@ -35,6 +38,19 @@ protocol:
timed_duty_max_duration_ms: 30000 timed_duty_max_duration_ms: 30000
increase_direction: 1 increase_direction: 1
decrease_direction: 2 decrease_direction: 2
dynamic_modes:
max_runtime_ms: 600000
arrival_grace_ms: 30000
rocking_horse:
mode_id: 1
phases:
- {id: rock_forward, completion: all_targets_reached, timeout_ms: 15000}
- {id: rock_backward, completion: all_targets_reached, timeout_ms: 15000}
cradle:
mode_id: 2
phases:
- {id: forward, completion: all_targets_reached, timeout_ms: 30000}
- {id: return, completion: all_targets_reached, timeout_ms: 30000}
axes: axes:
seat.set_longitudinal_position: seat.set_longitudinal_position:
motor_selector: 0 motor_selector: 0
+12
View File
@@ -95,8 +95,20 @@ BO_ 773 T2G_Seat_Thermal_Current_Diagnostic_305: 8 T2G_SEAT
SG_ Fan_Adc_Error_Count : 48|8@1+ (1,0) [0|255] "" HOST SG_ Fan_Adc_Error_Count : 48|8@1+ (1,0) [0|255] "" HOST
SG_ Heater_Adc_Error_Count : 56|8@1+ (1,0) [0|255] "" HOST SG_ Heater_Adc_Error_Count : 56|8@1+ (1,0) [0|255] "" HOST
BO_ 774 T2G_Seat_Dynamic_Mode_Status_306: 8 T2G_SEAT
SG_ Dynamic_Mode_ID : 0|8@1+ (1,0) [0|2] "" HOST
SG_ Dynamic_State : 8|8@1+ (1,0) [0|3] "" HOST
SG_ Dynamic_Phase : 16|8@1+ (1,0) [0|6] "" HOST
SG_ Dynamic_Active_Mask : 24|4@1+ (1,0) [0|15] "" HOST
SG_ Dynamic_Fault_Mask : 28|4@1+ (1,0) [0|15] "" HOST
SG_ Dynamic_Cycle_Index : 32|16@1+ (1,0) [0|65535] "cycle" HOST
SG_ Dynamic_Phase_Elapsed_Ms : 48|16@1+ (1,0) [0|65535] "ms" HOST
VAL_ 670 Driver_Seat_Ventilation_On 0 "invalid" 1 "on" 2 "off" 3 "reserved" ; VAL_ 670 Driver_Seat_Ventilation_On 0 "invalid" 1 "on" 2 "off" 3 "reserved" ;
VAL_ 670 Driver_Seat_Heating_On 0 "invalid" 1 "on" 2 "off" 3 "reserved" ; VAL_ 670 Driver_Seat_Heating_On 0 "invalid" 1 "on" 2 "off" 3 "reserved" ;
VAL_ 769 Ack 0 "none" 1 "accepted" 2 "rejected" 3 "duplicate" 4 "stop_all_accepted" ; VAL_ 769 Ack 0 "none" 1 "accepted" 2 "rejected" 3 "duplicate" 4 "stop_all_accepted" ;
VAL_ 770 Task_State 0 "idle" 1 "accepted" 2 "position_run" 3 "timed_run" 4 "calibration" 5 "stopping" 6 "complete" 7 "fault" ; VAL_ 770 Task_State 0 "idle" 1 "accepted" 2 "position_run" 3 "timed_run" 4 "calibration" 5 "stopping" 6 "complete" 7 "fault" ;
VAL_ 770 Result 0 "none" 1 "running" 2 "ok" 3 "stopped_by_cmd" 4 "endpoint_reached" 5 "stall" 6 "timeout" 7 "driver_fault" 8 "hall_fault" 9 "invalid_cmd" 10 "interlocked" 11 "calibration_failed" ; VAL_ 770 Result 0 "none" 1 "running" 2 "ok" 3 "stopped_by_cmd" 4 "endpoint_reached" 5 "stall" 6 "timeout" 7 "driver_fault" 8 "hall_fault" 9 "invalid_cmd" 10 "interlocked" 11 "calibration_failed" ;
VAL_ 774 Dynamic_Mode_ID 0 "none" 1 "rocking_horse" 2 "cradle" ;
VAL_ 774 Dynamic_State 0 "idle" 1 "entry" 2 "running" 3 "fault" ;
VAL_ 774 Dynamic_Phase 0 "none" 1 "entry" 2 "horse_forward" 3 "horse_backward" 4 "reserved" 5 "cradle_forward" 6 "cradle_return" ;
@@ -1,4 +1,4 @@
"""Load model-facing capability context from domain-owned catalogs.""" """Load model-facing capability context from domain and native-CAN catalogs."""
from __future__ import annotations from __future__ import annotations
@@ -21,18 +21,25 @@ class CapabilityRegistry:
def _load(self) -> dict[str, Any]: def _load(self) -> dict[str, Any]:
domains = [] domains = []
for catalog_path in sorted( catalog_paths = [
(self.root / "domains").glob("*/config/capabilities.yaml") *(self.root / "domains").glob("*/config/capabilities.yaml"),
): *(self.root / "config" / "native_can_capabilities").glob("*.yaml"),
]
for catalog_path in sorted(catalog_paths):
if catalog_path.parts[-3] == "_template": if catalog_path.parts[-3] == "_template":
continue continue
catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8")) catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))
domain = catalog["spec"]["domain"] domain = catalog["spec"]["domain"]
native_can_catalog = catalog_path.parent.name == "native_can_capabilities"
domains.append( domains.append(
{ {
"domain": domain, "domain": domain,
"availability": ( "availability": (
"executable" if domain in self.executable_domains else "catalog_only" "native_can"
if native_can_catalog
else "executable"
if domain in self.executable_domains
else "catalog_only"
), ),
"capabilities": catalog["spec"]["capabilities"], "capabilities": catalog["spec"]["capabilities"],
} }
@@ -45,6 +52,9 @@ class CapabilityRegistry:
"execution_policy": { "execution_policy": {
"executable_domains": sorted(self.executable_domains), "executable_domains": sorted(self.executable_domains),
"catalog_only_behavior": "return_as_deferred_request", "catalog_only_behavior": "return_as_deferred_request",
"native_can_behavior": (
"compile_for_body_can_when_hardware_mode_is_authorized"
),
"planning_environment": "dry_run_candidate_synthesis", "planning_environment": "dry_run_candidate_synthesis",
"named_preset_required": False, "named_preset_required": False,
"fuzzy_goal_behavior": ( "fuzzy_goal_behavior": (
@@ -67,6 +67,13 @@ def _new_state(
axis: _motion_state(axis, position) axis: _motion_state(axis, position)
for axis, position in MOTION_INITIAL_POSITIONS.items() for axis, position in MOTION_INITIAL_POSITIONS.items()
}, },
"dynamic_mode": {
"mode_id": 0,
"name": "",
"state": "idle",
"repeat": "none",
"max_runtime_ms": 600_000,
},
} }
for target in TARGETS for target in TARGETS
}, },
@@ -246,6 +253,28 @@ class ConversationStateStore:
state["last_target"] = action["target"] state["last_target"] = action["target"]
continue continue
target = action["target"] target = action["target"]
if action.get("action_type") == "dynamic_mode_stop":
state["devices"][target]["dynamic_mode"] = {
"mode_id": 0,
"name": "",
"state": "idle",
"repeat": "none",
"max_runtime_ms": 600_000,
}
state["last_target"] = target
continue
if action.get("action_type") == "dynamic_mode":
state["devices"][target]["dynamic_mode"] = {
"mode_id": int(action["mode_id"]),
"name": str(action.get("mode_name", "")),
"state": "running",
"repeat": str(action.get("repeat", "infinite")),
"max_runtime_ms": int(
action.get("max_runtime_ms", 600_000)
),
}
state["last_target"] = target
continue
if action.get("action_type") == "position": if action.get("action_type") == "position":
axis = action["axis"] axis = action["axis"]
position_state = _motion_state(axis, action["position_pct"]) position_state = _motion_state(axis, action["position_pct"])
+207 -2
View File
@@ -102,6 +102,195 @@ class ModeOperation:
} }
@dataclass(frozen=True)
class ModeTimelinePhase:
id: str
name: str
duration_ms: int | None
transition: str
targets: dict[str, Any]
effects: dict[str, Any]
completion: str
timeout_ms: int | None
motion: dict[str, Any]
arrival_grace_ms: int = 0
@classmethod
def from_dict(cls, document: dict[str, Any]) -> "ModeTimelinePhase":
phase_id = str(document.get("id", "")).strip()
if not phase_id:
raise ValueError("mode timeline phase id must not be empty")
duration_ms = document.get("duration_ms")
completion = str(
document.get(
"completion",
"elapsed_time" if duration_ms is not None else "all_targets_reached",
)
).strip()
if completion not in {"elapsed_time", "all_targets_reached"}:
raise ValueError(
f"mode timeline phase {phase_id} completion must be elapsed_time or all_targets_reached"
)
if completion == "elapsed_time":
if (
not isinstance(duration_ms, int)
or isinstance(duration_ms, bool)
or duration_ms <= 0
):
raise ValueError(
f"mode timeline phase {phase_id} duration_ms must be a positive integer"
)
elif duration_ms is not None:
raise ValueError(
f"mode timeline phase {phase_id} duration_ms must be omitted for all_targets_reached"
)
timeout_ms = document.get("timeout_ms")
if timeout_ms is not None and (
not isinstance(timeout_ms, int)
or isinstance(timeout_ms, bool)
or timeout_ms <= 0
):
raise ValueError(
f"mode timeline phase {phase_id} timeout_ms must be a positive integer"
)
if completion == "all_targets_reached" and timeout_ms is None:
raise ValueError(
f"mode timeline phase {phase_id} timeout_ms is required for all_targets_reached"
)
arrival_grace_ms = document.get("arrival_grace_ms", 0)
if (
not isinstance(arrival_grace_ms, int)
or isinstance(arrival_grace_ms, bool)
or arrival_grace_ms < 0
):
raise ValueError(
f"mode timeline phase {phase_id} arrival_grace_ms must be a non-negative integer"
)
targets = document.get("targets", {})
effects = document.get("effects", {})
motion = document.get("motion", {})
if (
not isinstance(targets, dict)
or not isinstance(effects, dict)
or not isinstance(motion, dict)
):
raise ValueError(
f"mode timeline phase {phase_id} targets/effects/motion must be objects"
)
return cls(
id=phase_id,
name=str(document.get("name", phase_id)),
duration_ms=duration_ms,
transition=str(document.get("transition", "hold")),
targets=dict(targets),
effects=dict(effects),
completion=completion,
timeout_ms=timeout_ms,
motion=dict(motion),
arrival_grace_ms=arrival_grace_ms,
)
def to_yaml(self) -> dict[str, Any]:
result = {
"id": self.id,
"name": self.name,
"transition": self.transition,
"targets": dict(self.targets),
"effects": dict(self.effects),
"completion": self.completion,
}
if self.duration_ms is not None:
result["duration_ms"] = self.duration_ms
if self.timeout_ms is not None:
result["timeout_ms"] = self.timeout_ms
if self.motion:
result["motion"] = dict(self.motion)
if self.arrival_grace_ms:
result["arrival_grace_ms"] = self.arrival_grace_ms
return result
@dataclass(frozen=True)
class ModeTimeline:
repeat: str
cycle_duration_ms: int | None
entry: dict[str, Any]
phases: tuple[ModeTimelinePhase, ...]
max_runtime_ms: int | None = None
@classmethod
def from_dict(cls, document: dict[str, Any]) -> "ModeTimeline":
repeat = str(document.get("repeat", "")).strip()
if repeat not in {"once", "infinite", "until_stopped_or_timeout"}:
raise ValueError(
"mode timeline repeat must be once, infinite, or until_stopped_or_timeout"
)
max_runtime_ms = document.get("max_runtime_ms")
if max_runtime_ms is not None and (
not isinstance(max_runtime_ms, int)
or isinstance(max_runtime_ms, bool)
or max_runtime_ms <= 0
):
raise ValueError("mode timeline max_runtime_ms must be a positive integer")
if repeat == "until_stopped_or_timeout" and max_runtime_ms is None:
raise ValueError(
"mode timeline max_runtime_ms is required for until_stopped_or_timeout"
)
duration_ms = document.get("cycle_duration_ms")
if duration_ms is not None and (
not isinstance(duration_ms, int)
or isinstance(duration_ms, bool)
or duration_ms <= 0
):
raise ValueError("mode timeline cycle_duration_ms must be positive")
raw_entry = document.get("entry", {})
raw_phases = document.get("phases", [])
if not isinstance(raw_entry, dict):
raise ValueError("mode timeline entry must be an object")
if not isinstance(raw_phases, list) or not raw_phases:
raise ValueError("mode timeline must contain at least one phase")
phases = tuple(ModeTimelinePhase.from_dict(item) for item in raw_phases)
phase_ids = [phase.id for phase in phases]
if len(set(phase_ids)) != len(phase_ids):
raise ValueError("mode timeline phase ids must be unique")
timed_phases = all(phase.duration_ms is not None for phase in phases)
if timed_phases:
calculated_duration = sum(
int(phase.duration_ms or 0) for phase in phases
)
if calculated_duration != duration_ms:
raise ValueError(
"mode timeline cycle_duration_ms does not equal the sum of phases: "
f"declared={duration_ms}, calculated={calculated_duration}"
)
elif duration_ms is not None:
raise ValueError(
"mode timeline cycle_duration_ms must be omitted when a phase completes by target arrival"
)
return cls(
repeat=repeat,
cycle_duration_ms=duration_ms,
entry=dict(raw_entry),
phases=phases,
max_runtime_ms=max_runtime_ms,
)
def to_yaml(self) -> dict[str, Any]:
result = {
"repeat": self.repeat,
"entry": dict(self.entry),
"phases": [phase.to_yaml() for phase in self.phases],
}
if self.cycle_duration_ms is not None:
result["cycle_duration_ms"] = self.cycle_duration_ms
if self.max_runtime_ms is not None:
result["max_runtime_ms"] = self.max_runtime_ms
return result
def public_dict(self) -> dict[str, Any]:
return self.to_yaml()
@dataclass(frozen=True) @dataclass(frozen=True)
class ModeProfile: class ModeProfile:
id: str id: str
@@ -113,6 +302,7 @@ class ModeProfile:
source: str source: str
common: bool = True common: bool = True
vehicle_ids: tuple[str, ...] = () vehicle_ids: tuple[str, ...] = ()
timeline: ModeTimeline | None = None
@classmethod @classmethod
def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile": def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile":
@@ -128,6 +318,9 @@ class ModeProfile:
name, name,
[str(alias) for alias in document.get("aliases", [])], [str(alias) for alias in document.get("aliases", [])],
) )
raw_timeline = document.get("timeline")
if raw_timeline is not None and not isinstance(raw_timeline, dict):
raise ValueError(f"{path} timeline must be an object")
return cls( return cls(
id=str(document["id"]), id=str(document["id"]),
name=name, name=name,
@@ -138,10 +331,15 @@ class ModeProfile:
source=str(path), source=str(path),
common=common, common=common,
vehicle_ids=vehicle_ids, vehicle_ids=vehicle_ids,
timeline=(
ModeTimeline.from_dict(raw_timeline)
if isinstance(raw_timeline, dict)
else None
),
) )
def public_dict(self) -> dict[str, Any]: def public_dict(self) -> dict[str, Any]:
return { result = {
"id": self.id, "id": self.id,
"name": self.name, "name": self.name,
"aliases": list(self.aliases), "aliases": list(self.aliases),
@@ -153,7 +351,11 @@ class ModeProfile:
"common": self.common, "common": self.common,
"vehicle_ids": list(self.vehicle_ids), "vehicle_ids": list(self.vehicle_ids),
}, },
"behavior": "cyclic" if self.timeline is not None else "static",
} }
if self.timeline is not None:
result["timeline"] = self.timeline.public_dict()
return result
class ModeProfileRegistry: class ModeProfileRegistry:
@@ -370,7 +572,7 @@ def _mode_profile_document(
"common": profile.common, "common": profile.common,
"vehicle_ids": list(profile.vehicle_ids), "vehicle_ids": list(profile.vehicle_ids),
} }
return { document = {
"id": profile.id, "id": profile.id,
"name": profile.name, "name": profile.name,
"aliases": list(profile.aliases), "aliases": list(profile.aliases),
@@ -379,6 +581,9 @@ def _mode_profile_document(
"scope": scope, "scope": scope,
"operations": [operation.to_yaml() for operation in operations], "operations": [operation.to_yaml() for operation in operations],
} }
if profile.timeline is not None:
document["timeline"] = profile.timeline.to_yaml()
return document
def _replace_yaml(path: Path, document: dict[str, Any]) -> None: def _replace_yaml(path: Path, document: dict[str, Any]) -> None:
@@ -27,7 +27,14 @@ SEAT_MOTION_CAPABILITIES = {
SEAT_RELATIVE_PHYSICAL_PARAMETERS = {"distance_mm", "angle_delta_deg"} SEAT_RELATIVE_PHYSICAL_PARAMETERS = {"distance_mm", "angle_delta_deg"}
SAVE_PRESET_CAPABILITY = "seat.save_posture_preset" SAVE_PRESET_CAPABILITY = "seat.save_posture_preset"
APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset" APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset"
DYNAMIC_MODE_CAPABILITY = "seat.start_dynamic_mode"
DYNAMIC_MODE_STOP_CAPABILITY = "seat.stop_dynamic_mode"
DYNAMIC_MODE_MAX_RUNTIME_MS = 600_000
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$") HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
NATIVE_CAN_ONLY_CAPABILITIES = {
"climate.set_control_mode",
"roof.set_shade_motion",
}
class ModelPlanAdapter: class ModelPlanAdapter:
@@ -69,7 +76,7 @@ class ModelPlanAdapter:
) )
continue continue
domain, availability, capability = registered domain, domain_availability, capability = registered
parameters = { parameters = {
str(item["name"]): item.get("value") str(item["name"]): item.get("value")
for item in request.get("parameters", []) for item in request.get("parameters", [])
@@ -93,7 +100,26 @@ class ModelPlanAdapter:
) )
continue continue
if availability != "executable": availability = (
"native_can"
if capability_name in NATIVE_CAN_ONLY_CAPABILITIES
else domain_availability
)
native_only_request = (
domain == "seat"
and capability_name in SEAT_CAPABILITIES
and (
request.get("target_scope") in {"left_rear", "right_rear"}
or parameters.get("level") in {4, 5}
)
) or (
capability_name == "climate.set_outlet_mode"
and (
request.get("target_scope") == "vehicle"
or parameters.get("mode") not in {"face", "feet"}
)
)
if availability != "executable" or native_only_request:
values = [value for value in parameters.values() if value is not None] values = [value for value in parameters.values() if value is not None]
deferred.append( deferred.append(
{ {
@@ -104,6 +130,11 @@ class ModelPlanAdapter:
"requested_value": values[0] if len(values) == 1 else None, "requested_value": values[0] if len(values) == 1 else None,
"parameters": parameters, "parameters": parameters,
"reason": request["reason"], "reason": request["reason"],
"execution_backend": (
"native_can"
if availability == "native_can" or native_only_request
else availability
),
} }
) )
continue continue
@@ -182,6 +213,20 @@ class ModelPlanAdapter:
} }
) )
continue continue
if capability_name == DYNAMIC_MODE_STOP_CAPABILITY:
dynamic_action = self._dynamic_mode_stop_action(
index, request, parameters, ambiguities
)
if dynamic_action is not None:
actions.append(dynamic_action)
continue
if capability_name == DYNAMIC_MODE_CAPABILITY:
dynamic_action = self._dynamic_mode_action(
index, request, parameters, ambiguities
)
if dynamic_action is not None:
actions.append(dynamic_action)
continue
action = self._seat_action( action = self._seat_action(
index, request, parameters, ambiguities, context, vehicle_profile index, request, parameters, ambiguities, context, vehicle_profile
) )
@@ -352,6 +397,79 @@ class ModelPlanAdapter:
return None return None
return {"target": target, "preset_name": preset_name.strip()} return {"target": target, "preset_name": preset_name.strip()}
@staticmethod
def _dynamic_mode_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前动态座椅模式仅控制副驾。",
}
)
return None
mode_id = parameters.get("mode_id")
if (
not isinstance(mode_id, (int, float))
or isinstance(mode_id, bool)
or not float(mode_id).is_integer()
or int(mode_id) not in {1, 2}
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.mode_id",
"question": "动态座椅模式编号必须是 1(木马模式)或 2(动态摇篮)。",
}
)
return None
normalized_mode_id = int(mode_id)
return {
"action_type": "dynamic_mode",
"target": target,
"mode_id": normalized_mode_id,
"mode_name": (
"木马模式"
if normalized_mode_id == 1
else "动态摇篮模式"
),
"repeat": "until_stopped_or_timeout",
"max_runtime_ms": DYNAMIC_MODE_MAX_RUNTIME_MS,
}
@staticmethod
def _dynamic_mode_stop_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前动态座椅模式仅控制副驾。",
}
)
return None
if parameters:
ambiguities.append(
{
"field": f"requests[{index}].parameters",
"question": "停止动态座椅模式不接受额外参数。",
}
)
return None
return {
"action_type": "dynamic_mode_stop",
"target": target,
}
@staticmethod @staticmethod
def _find_preset( def _find_preset(
presets: list[dict[str, Any]], presets: list[dict[str, Any]],
@@ -0,0 +1,348 @@
"""Deterministic parsing for the trusted vehicle CAN command vocabulary.
The parser only emits named capabilities. It never accepts a raw CAN ID or
payload from a user utterance; byte-level compilation stays in NativeCanRuntime.
"""
from __future__ import annotations
import re
from typing import Any
WINDOW_TARGETS = {
"全部": "all",
"所有": "all",
"四个": "all",
"全车": "all",
"前排": "front",
"后排": "rear",
"左前": "front_left",
"前左": "front_left",
"右前": "front_right",
"前右": "front_right",
"左后": "rear_left",
"后左": "rear_left",
"右后": "rear_right",
"后右": "rear_right",
}
OUTLET_TARGETS = {
"主驾左": "driver_left",
"主驾右": "driver_right",
"副驾左": "passenger_left",
"副驾右": "passenger_right",
}
LIGHT_TARGETS = {
"全部": "all",
"所有": "all",
"左前": "front_left",
"右前": "front_right",
"中排左旋钮": "middle_left_rotary",
"中排右旋钮": "middle_right_rotary",
"中排左": "middle_left",
"中排右": "middle_right",
"左后": "rear_left",
"右后": "rear_right",
}
SEAT_TARGETS = {
"driver": "driver",
"passenger": "passenger",
"left-rear": "left_rear",
"right-rear": "right_rear",
}
def _action(
domain: str,
capability: str,
target: str,
parameters: dict[str, Any],
reason: str,
) -> dict[str, Any]:
return {
"domain": domain,
"capability": capability,
"target": target,
"parameters": parameters,
"operation": "set",
"reason": reason,
"execution_backend": "native_can",
}
def _parse_exact_command(text: str) -> list[dict[str, Any]]:
command = " ".join(text.strip().lower().split())
match = re.fullmatch(r"door rear (lock|unlock)", command)
if match:
return [_action("door", "door.set_rear_lock", "rear", {"state": match[1]}, "用户输入了受信任的后排门锁命令。")]
match = re.fullmatch(
r"window (all|front|rear|front-left|front-right|rear-left|rear-right) (open|close|stop)",
command,
)
if match:
return [_action("window", "window.set_motion", match[1].replace("-", "_"), {"motion": match[2]}, "用户输入了受信任的车窗动作命令。")]
match = re.fullmatch(
r"window (front-left|front-right|rear-left|rear-right) position (\d+)",
command,
)
if match and 0 <= int(match[2]) <= 100:
return [_action("window", "window.set_position", match[1].replace("-", "_"), {"position_pct": int(match[2])}, "用户输入了受信任的车窗位置命令。")]
match = re.fullmatch(r"ac (on|off)", command)
if match:
return [_action("climate", "climate.set_power", "vehicle", {"state": match[1]}, "用户输入了受信任的空调开关命令。")]
match = re.fullmatch(r"ac fan ([1-7])", command)
if match:
return [_action("climate", "climate.set_fan_level", "vehicle", {"level": int(match[1])}, "用户输入了受信任的空调风量命令。")]
match = re.fullmatch(
r"ac mode (face|face-feet|feet|feet-defrost|defrost|face-feet-defrost|face-defrost)",
command,
)
if match:
return [_action("climate", "climate.set_outlet_mode", "vehicle", {"mode": match[1].replace("-", "_")}, "用户输入了受信任的空调出风模式命令。")]
match = re.fullmatch(r"ac temp (driver|passenger|rear) (\d+)", command)
if match and 18 <= int(match[2]) <= 32:
return [_action("climate", "climate.set_temperature", match[1], {"temperature_c": int(match[2])}, "用户输入了受信任的空调温度命令。")]
match = re.fullmatch(r"ac control (manual|auto)", command)
if match:
return [_action("climate", "climate.set_control_mode", "vehicle", {"mode": match[1]}, "用户输入了受信任的空调控制模式命令。")]
match = re.fullmatch(
r"outlet (driver-left|driver-right|passenger-left|passenger-right) percent (\d+) (\d+)",
command,
)
if match and all(0 <= int(value) <= 100 for value in match.groups()[1:]):
return [_action("outlet", "outlet.set_position_percent", match[1].replace("-", "_"), {"horizontal_pct": int(match[2]), "vertical_pct": int(match[3])}, "用户输入了受信任的电动风口百分比命令。")]
match = re.fullmatch(
r"outlet (driver-left|driver-right|passenger-left|passenger-right) position (-?\d+) (-?\d+)",
command,
)
if match and all(-128 <= int(value) <= 126 for value in match.groups()[1:]):
return [_action("outlet", "outlet.set_calibrated_position", match[1].replace("-", "_"), {"horizontal_value": int(match[2]), "vertical_value": int(match[3])}, "用户输入了受信任的电动风口标定坐标命令。")]
match = re.fullmatch(
r"seat (driver|passenger|left-rear|right-rear) (heat|vent) (off|[1-5])",
command,
)
if match:
parameters: dict[str, Any] = {"state": "off" if match[3] == "off" else "on"}
if match[3] != "off":
parameters["level"] = int(match[3])
capability = "seat.set_heating" if match[2] == "heat" else "seat.set_ventilation"
return [_action("seat", capability, SEAT_TARGETS[match[1]], parameters, "用户输入了受信任的整车座椅热舒适命令。")]
match = re.fullmatch(r"light (indoor|wall) (on|off)", command)
if match:
capability = "interior_light.set_indoor_power" if match[1] == "indoor" else "interior_light.set_wall_power"
return [_action("interior_light", capability, "vehicle", {"state": match[2]}, "用户输入了受信任的车内灯命令。")]
match = re.fullmatch(
r"light reading (all|front-left|front-right|middle-left|middle-right|middle-left-rotary|middle-right-rotary|rear-left|rear-right) (on|off)",
command,
)
if match:
return [_action("interior_light", "interior_light.set_reading_power", match[1].replace("-", "_"), {"state": match[2]}, "用户输入了受信任的阅读灯命令。")]
match = re.fullmatch(r"light ambient (on|off)", command)
if match:
return [_action("ambient_light", "ambient_light.set_power", "vehicle", {"state": match[1]}, "用户输入了受信任的氛围灯开关命令。")]
match = re.fullmatch(r"light ambient scene (camping|nap|cinema|meditation|romance)", command)
if match:
mode = {"nap": "rest", "romance": "romantic"}.get(match[1], match[1])
return [_action("ambient_light", "ambient_light.set_mode", "vehicle", {"mode": mode}, "用户输入了受信任的氛围灯场景命令。")]
match = re.fullmatch(r"light ambient rgb (\d+) (\d+) (\d+) (\d+)", command)
if match:
red, green, blue, brightness = map(int, match.groups())
if all(1 <= value <= 255 for value in (red, green, blue)) and 0 <= brightness <= 100:
return [_action("ambient_light", "ambient_light.set_illumination", "vehicle", {"color_hex": f"#{red:02X}{green:02X}{blue:02X}", "brightness_pct": brightness}, "用户输入了受信任的氛围灯 RGB 命令。")]
match = re.fullmatch(r"shade (open|close|half|stop)", command)
if match:
return [_action("roof", "roof.set_shade_motion", "vehicle", {"motion": match[1]}, "用户输入了受信任的遮阳帘动作命令。")]
match = re.fullmatch(r"shade position (\d+)", command)
if match and 0 <= int(match[1]) <= 100:
return [_action("roof", "roof.set_shade_position", "vehicle", {"position_pct": int(match[1])}, "用户输入了受信任的遮阳帘位置命令。")]
return []
def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
compact = re.sub(r"\s+", "", text)
actions: list[dict[str, Any]] = []
if "空调" in compact or re.search(r"(?:AC|A/C)", text, re.IGNORECASE):
if re.search(r"(?:打开|开启|启动)空调", compact):
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "on"}, "用户要求打开空调。"))
elif re.search(r"(?:关闭|关掉|关上)空调", compact):
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "off"}, "用户要求关闭空调。"))
temperature = re.search(
r"(?:(主驾|副驾|后排)(?:空调)?(?:温度)?|空调(?:温度)?|温度)(?:调到|设为|设置为|到)?(1[89]|2\d|3[0-2])(?:℃|度)?",
compact,
)
if temperature:
target = {"主驾": "driver", "副驾": "passenger", "后排": "rear"}.get(temperature[1], "vehicle")
actions.append(_action("climate", "climate.set_temperature", target, {"temperature_c": int(temperature[2])}, "用户指定了空调温度。"))
fan = re.search(r"(?:风量|风速)(?:调到|设为|设置为|到)?([1-7])(?:档)?", compact)
if fan:
actions.append(_action("climate", "climate.set_fan_level", "vehicle", {"level": int(fan[1])}, "用户指定了空调风量。"))
if re.search(r"自动(?:控制|模式)", compact):
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "auto"}, "用户指定空调自动控制。"))
elif re.search(r"手动(?:控制|模式)", compact):
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "manual"}, "用户指定空调手动控制。"))
outlet_modes = [
("吹面吹脚除霜", "face_feet_defrost"),
("吹面除霜", "face_defrost"),
("吹脚除霜", "feet_defrost"),
("吹面吹脚", "face_feet"),
("除霜", "defrost"),
("吹脚", "feet"),
("吹面", "face"),
]
for label, mode in outlet_modes:
if label in compact:
actions.append(_action("climate", "climate.set_outlet_mode", "vehicle", {"mode": mode}, "用户指定了空调出风模式。"))
break
if "车窗" in compact:
target = next((value for label, value in WINDOW_TARGETS.items() if label in compact), "all")
position = re.search(r"(?:位置|开度|调到|设为)(\d{1,3})%", compact)
if position and target not in {"all", "front", "rear"} and int(position[1]) <= 100:
actions.append(_action("window", "window.set_position", target, {"position_pct": int(position[1])}, "用户指定了单个车窗的绝对开度。"))
else:
motion = (
"stop" if re.search(r"停止|停下|暂停", compact)
else "close" if re.search(r"关闭|关上|升起|关窗", compact)
else "open" if re.search(r"打开|开启|降下|开窗", compact)
else None
)
if motion:
actions.append(_action("window", "window.set_motion", target, {"motion": motion}, "用户指定了车窗目标和动作。"))
if re.search(r"后排.*(?:门锁|车门)|(?:门锁|车门).*后排", compact):
state = "unlock" if re.search(r"解锁|开锁", compact) else "lock" if re.search(r"上锁|落锁|锁上", compact) else None
if state:
actions.append(_action("door", "door.set_rear_lock", "rear", {"state": state}, "用户指定了后排门锁状态。"))
if re.search(r"遮阳帘|天幕帘", compact):
position = re.search(r"(?:位置|开度|调到|设为)(\d{1,3})%", compact)
if position and int(position[1]) <= 100:
actions.append(_action("roof", "roof.set_shade_position", "vehicle", {"position_pct": int(position[1])}, "用户指定了遮阳帘开度。"))
else:
motion = (
"stop" if re.search(r"停止|停下|暂停", compact)
else "half" if re.search(r"半开|一半", compact)
else "close" if re.search(r"关闭|关上", compact)
else "open" if re.search(r"打开|开启", compact)
else None
)
if motion:
actions.append(_action("roof", "roof.set_shade_motion", "vehicle", {"motion": motion}, "用户指定了遮阳帘动作。"))
for label, capability in (("室内灯", "interior_light.set_indoor_power"), ("车内灯", "interior_light.set_indoor_power"), ("壁灯", "interior_light.set_wall_power")):
if label in compact:
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
if state:
actions.append(_action("interior_light", capability, "vehicle", {"state": state}, "用户指定了车内灯状态。"))
break
if "阅读灯" in compact:
target = next((value for label, value in LIGHT_TARGETS.items() if label in compact), "all")
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
if state:
actions.append(_action("interior_light", "interior_light.set_reading_power", target, {"state": state}, "用户指定了阅读灯区域和状态。"))
outlet_target = next((value for label, value in OUTLET_TARGETS.items() if f"{label}风口" in compact), None)
outlet_position = re.search(r"(?:左右|水平)(\d{1,3})%?.*?(?:上下|垂直)(\d{1,3})%?", compact)
if outlet_target and outlet_position and all(int(value) <= 100 for value in outlet_position.groups()):
actions.append(_action("outlet", "outlet.set_position_percent", outlet_target, {"horizontal_pct": int(outlet_position[1]), "vertical_pct": int(outlet_position[2])}, "用户指定了已标定风口的双轴百分比位置。"))
return actions
def _signature(action: dict[str, Any]) -> tuple[str, str, str, tuple[tuple[str, str], ...]]:
return (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope"))),
tuple(sorted((str(key), repr(value)) for key, value in action.get("parameters", {}).items())),
)
def merge_native_can_intent(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
"""Add deterministic vehicle-CAN actions without duplicating model requests."""
clauses = [part for part in re.split(r"[,,。;;!!??]+", utterance) if part.strip()]
exact_actions = [action for clause in clauses for action in _parse_exact_command(clause)]
actions = exact_actions or _parse_chinese_commands(utterance)
if not actions:
return parsed
result = dict(parsed)
existing = [
*result.get("actions", []),
*result.get("native_can_actions", []),
*[
item
for item in result.get("deferred_requests", [])
if item.get("execution_backend") == "native_can"
],
]
signatures = {_signature(item) for item in existing}
unique_actions = [item for item in actions if _signature(item) not in signatures]
if not unique_actions:
return result
result["native_can_actions"] = [
*result.get("native_can_actions", []),
*unique_actions,
]
result["deferred_requests"] = [
*result.get("deferred_requests", []),
*[
{
"domain": item["domain"],
"capability": item["capability"],
"target_scope": item["target"],
"desired_change": item["operation"],
"requested_value": next(iter(item["parameters"].values())) if len(item["parameters"]) == 1 else None,
"parameters": dict(item["parameters"]),
"reason": item["reason"],
"execution_backend": "native_can",
}
for item in unique_actions
],
]
result["model_requests"] = [
*result.get("model_requests", []),
*[
{
"domain": item["domain"],
"capability": item["capability"],
"target_scope": item["target"],
"operation": item["operation"],
"parameters": [
{"name": name, "value": value}
for name, value in item["parameters"].items()
],
"reason": item["reason"],
}
for item in unique_actions
],
]
domains = list(result.get("detected_domains", []))
for item in unique_actions:
if item["domain"] not in domains:
domains.append(item["domain"])
result["detected_domains"] = domains
result["ambiguities"] = [
item
for item in result.get("ambiguities", [])
if item.get("field") != "capability"
]
result["decision_summary"] = [
*result.get("decision_summary", []),
*[item["reason"] for item in unique_actions],
]
result["confidence"] = max(float(result.get("confidence", 0)), 0.98)
return result
@@ -0,0 +1,864 @@
"""Trusted compilation and execution support for CAN_DEV vehicle controls."""
from __future__ import annotations
import hashlib
import json
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable
import yaml
from cognitive_agent.infrastructure.can.gateway import CanBus, CanFrame
def _now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _set_bits(payload: bytearray, start: int, length: int, value: int) -> None:
if length <= 0 or start < 0 or start + length > len(payload) * 8:
raise ValueError("invalid bit range")
if value < 0 or value >= (1 << length):
raise ValueError(f"value {value} does not fit in {length} bits")
for offset in range(length):
bit = start + offset
mask = 1 << (bit % 8)
if value & (1 << offset):
payload[bit // 8] |= mask
else:
payload[bit // 8] &= ~mask
@dataclass(frozen=True)
class NativeFrameSpec:
frame_id: int
payload: bytes
is_fd: bool = False
bitrate_switch: bool = False
repeat_count: int = 1
period_ms: int = 0
feedback_ids: tuple[int, ...] = ()
expected_feedback: str = ""
special_handler: str = ""
special_parameters: dict[str, Any] | None = None
class NativeCanCompileError(ValueError):
"""Raised when a request is outside the CAN_DEV verified vocabulary."""
class NativeCanRuntime:
"""Compile named capabilities into byte-exact CAN_DEV frames."""
compiler_version = "1.0.0"
mapping_version = "can-dev-6e7ed04"
def __init__(self, repository_root: Path):
self.root = repository_root
calibration_path = (
repository_root
/ "platforms"
/ "t2g_cabin_bench"
/ "calibration"
/ "body_can.yaml"
)
self.calibration_path = calibration_path
self.calibration = yaml.safe_load(calibration_path.read_text(encoding="utf-8"))
self.capabilities: dict[str, dict[str, Any]] = {}
catalog_paths = [
*(repository_root / "domains").glob("*/config/capabilities.yaml"),
*(repository_root / "config" / "native_can_capabilities").glob("*.yaml"),
]
for path in sorted(catalog_paths):
catalog = yaml.safe_load(path.read_text(encoding="utf-8"))
for capability in catalog.get("spec", {}).get("capabilities", []):
self.capabilities[capability["name"]] = capability
@property
def supported_capabilities(self) -> frozenset[str]:
return frozenset(
{
"door.set_rear_lock",
"window.set_motion",
"window.set_position",
"climate.set_power",
"climate.set_ac",
"climate.set_fan_level",
"climate.set_outlet_mode",
"climate.set_temperature",
"climate.set_control_mode",
"outlet.set_position_percent",
"outlet.set_calibrated_position",
"seat.set_heating",
"seat.set_ventilation",
"interior_light.set_indoor_power",
"interior_light.set_wall_power",
"interior_light.set_reading_power",
"ambient_light.set_power",
"ambient_light.set_mode",
"ambient_light.set_illumination",
"roof.set_shade_motion",
"roof.set_shade_position",
}
)
def supports(self, action: dict[str, Any]) -> bool:
return str(action.get("capability")) in self.supported_capabilities
def build_function_config(
self,
domain: str,
actions: list[dict[str, Any]],
request_id: str,
provider: str,
) -> dict[str, Any]:
operations = []
for index, action in enumerate(actions, start=1):
capability_name = str(action["capability"])
if capability_name not in self.supported_capabilities:
raise NativeCanCompileError(
f"CAN_DEV does not implement {capability_name}"
)
capability = self.capabilities[capability_name]
target = str(action.get("target", action.get("target_scope", "vehicle")))
if target not in capability.get("target_scopes", []):
raise NativeCanCompileError(
f"{capability_name} does not support target {target}"
)
suffix = capability_name.split(".", 1)[1]
operation_id = f"{target}_{suffix}_{index}"[:63]
operations.append(
{
"id": operation_id,
"capability": capability_name,
"target": {
"device": capability["target_devices"][0],
"zone": target,
},
"parameters": dict(action.get("parameters", {})),
"timeout_ms": 15_000 if capability_name == "window.set_position" else 4_000,
"failure_policy": "abort",
}
)
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"id": f"native-{domain}-{request_id}",
"name": f"{domain.replace('_', ' ').title()} native CAN function",
"version": "1.0.0",
"request_id": request_id,
"generated_at": _now(),
"labels": {
"provider": provider,
"execution_scope": "vehicle_can_channel_1",
"source_branch": "CAN_DEV",
},
},
"spec": {
"domain": domain,
"operations": operations,
"assumptions": [
"Only named, range-checked CAN_DEV capabilities may reach the body CAN channel.",
"The body CAN channel uses ISO CAN FD at 500 kbit/s arbitration and 2 Mbit/s data rate.",
],
},
}
def compile(self, function_config: dict[str, Any]) -> dict[str, Any]:
commands: list[dict[str, Any]] = []
for operation_index, operation in enumerate(
function_config["spec"]["operations"], start=1
):
specs = self._frame_specs(operation)
for frame_index, spec in enumerate(specs, start=1):
signals: dict[str, int | bool | str] = {
"frame_id": f"0x{spec.frame_id:03X}",
"data_hex": " ".join(f"{byte:02X}" for byte in spec.payload),
"is_fd": spec.is_fd,
"bitrate_switch": spec.bitrate_switch,
"feedback_ids": ",".join(
f"0x{frame_id:03X}" for frame_id in spec.feedback_ids
),
"expected_feedback": spec.expected_feedback,
}
if spec.special_handler:
signals["special_handler"] = spec.special_handler
signals["special_parameters"] = json.dumps(
spec.special_parameters or {},
ensure_ascii=False,
sort_keys=True,
)
command: dict[str, Any] = {
"id": f"native_{operation_index}_{frame_index}_control",
"operation_ref": operation["id"],
"bus": "body_can",
"message": f"Native_0x{spec.frame_id:03X}",
"signals": signals,
"send_offset_ms": (operation_index - 1) * 20,
}
if spec.repeat_count > 1:
command["repeat"] = {
"count": spec.repeat_count,
"period_ms": spec.period_ms,
}
commands.append(command)
canonical = json.dumps(
function_config, ensure_ascii=False, sort_keys=True
).encode("utf-8")
metadata = function_config["metadata"]
domain = function_config["spec"]["domain"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "CommandPlan",
"metadata": {
"id": f"plan-native-{domain}-{metadata['request_id']}",
"name": f"{domain.replace('_', ' ').title()} native CAN command plan",
"version": "1.0.0",
"request_id": metadata["request_id"],
"labels": {
"execution_scope": "vehicle_can_channel_1",
"source_branch": "CAN_DEV",
},
},
"spec": {
"domain": domain,
"platform": "t2g_cabin_bench",
"source": {
"config_id": metadata["id"],
"config_version": metadata["version"],
"config_sha256": hashlib.sha256(canonical).hexdigest(),
"compiler_version": self.compiler_version,
"mapping_version": self.mapping_version,
},
"commands": commands,
},
}
def execute_virtual(
self,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
started_at = _now()
operations = {
operation["id"]: operation
for operation in function_config["spec"]["operations"]
}
results = []
for command in command_plan["spec"]["commands"]:
operation = operations[command["operation_ref"]]
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "succeeded",
"message": "已完成 CAN_DEV 帧编译;dry-run 未打开物理 CAN。",
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": self.command_frame_public(command),
"source_branch": "CAN_DEV",
"simulated": True,
},
}
)
return self._report(function_config, results, started_at, "dry_run")
@staticmethod
def command_frame_public(command: dict[str, Any]) -> dict[str, Any]:
signals = command["signals"]
return {
"frame_id": signals["frame_id"],
"payload_size": len(bytes.fromhex(str(signals["data_hex"]))),
"data_hex": signals["data_hex"],
"channel": 1,
"frame_format": "can_fd" if signals["is_fd"] else "classic_can",
"bitrate_switch": bool(signals["bitrate_switch"]),
"repeat": command.get("repeat", {"count": 1, "period_ms": 0}),
}
@staticmethod
def _report(
function_config: dict[str, Any],
results: list[dict[str, Any]],
started_at: str,
mode: str,
) -> dict[str, Any]:
succeeded = all(item["status"] == "succeeded" for item in results)
request_id = function_config["metadata"]["request_id"]
domain = function_config["spec"]["domain"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": f"report-native-{domain}-{request_id}",
"name": f"{domain.replace('_', ' ').title()} native CAN execution report",
"version": "1.0.0",
"request_id": request_id,
"labels": {
"simulation": "false" if mode == "vehicle" else "true",
"source_branch": "CAN_DEV",
},
},
"spec": {
"mode": mode,
"status": "succeeded" if succeeded else "failed",
"started_at": started_at,
"finished_at": _now(),
"results": results,
},
}
def _frame_specs(self, operation: dict[str, Any]) -> list[NativeFrameSpec]:
capability = operation["capability"]
target = operation["target"]["zone"]
parameters = operation["parameters"]
if capability in {"climate.set_power", "climate.set_ac"}:
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
_set_bits(payload, 28, 2, state)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_power:{parameters['state']}")]
if capability == "climate.set_fan_level":
level = self._integer(parameters, "level", 1, 7)
payload = bytearray(8)
_set_bits(payload, 4, 4, level)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_fan:{level}")]
if capability == "climate.set_outlet_mode":
mode = self._enum(
parameters,
"mode",
{
"face": 1,
"face_feet": 2,
"feet": 3,
"feet_defrost": 4,
"defrost": 5,
"face_feet_defrost": 6,
"face_defrost": 7,
},
)
payload = bytearray(8)
_set_bits(payload, 0, 4, mode)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_mode:{mode}")]
if capability == "climate.set_control_mode":
mode = self._enum(parameters, "mode", {"manual": 1, "auto": 2})
payload = bytearray(8)
_set_bits(payload, 16, 2, mode)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_control:{mode}")]
if capability == "climate.set_temperature":
temperature = self._integer(parameters, "temperature_c", 18, 32)
starts = {
"driver": (32,),
"passenger": (40,),
"rear": (48,),
"rear_row": (48,),
"rear_left": (48,),
"rear_right": (48,),
"vehicle": (32, 40),
"front_row": (32, 40),
}
if target not in starts:
raise NativeCanCompileError(
f"climate.set_temperature does not support target {target}"
)
specs = []
for start in starts[target]:
payload = bytearray(8)
_set_bits(payload, start, 8, temperature)
label = "driver" if start == 32 else "passenger" if start == 40 else "rear"
specs.append(
self._classic(
0x1DE,
payload,
1 if start == 48 else 3,
100,
(0x404,),
f"climate_temperature:{label}:{temperature}",
)
)
return specs
if capability == "door.set_rear_lock":
state = self._enum(parameters, "state", {"lock": 1, "unlock": 2})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 14, 2, state)
return [self._classic(0x32B, payload, feedback_ids=(0x407,), expected=f"rear_lock:{parameters['state']}")]
if capability == "window.set_motion":
motion = self._enum(parameters, "motion", {"open": 1, "close": 2, "stop": 3})
payload = self._window_payload(target, motion)
return [self._fd(0x431, payload, 3, 100, (0x388,), f"window_motion:{target}:{parameters['motion']}")]
if capability == "window.set_position":
position = self._integer(parameters, "position_pct", 0, 100)
if target not in {"front_left", "rear_left", "front_right", "rear_right"}:
raise NativeCanCompileError("absolute window position requires one window")
return [
NativeFrameSpec(
frame_id=0x431,
payload=self._window_payload(target, 3),
is_fd=True,
bitrate_switch=True,
feedback_ids=(0x388,),
expected_feedback=f"window_position:{target}:{position}",
special_handler="window_position",
special_parameters={"target": target, "position_pct": position},
)
]
if capability in {"seat.set_heating", "seat.set_ventilation"}:
return [self._body_seat_frame(capability, target, parameters)]
if capability in {"outlet.set_position_percent", "outlet.set_calibrated_position"}:
return [self._outlet_frame(capability, target, parameters)]
if capability == "interior_light.set_indoor_power":
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 24, 3, state)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"indoor_light:{parameters['state']}")]
if capability == "interior_light.set_wall_power":
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
payload[0] = 0x11
_set_bits(payload, 20, 2, state)
return [self._classic(0x32B, payload)]
if capability == "interior_light.set_reading_power":
return [self._reading_light_frame(target, parameters)]
if capability == "ambient_light.set_power":
value = self._enum(parameters, "state", {"off": 6, "on": 7})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 27, 5, value)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"ambient_power:{parameters['state']}")]
if capability == "ambient_light.set_mode":
value = self._enum(
parameters,
"mode",
{"camping": 1, "rest": 2, "cinema": 3, "meditation": 4, "romantic": 5, "off": 6, "on": 7},
)
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 27, 5, value)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"ambient_mode:{value}")]
if capability == "ambient_light.set_illumination":
color = str(parameters.get("color_hex", ""))
if len(color) != 7 or not color.startswith("#"):
raise NativeCanCompileError("color_hex must use #RRGGBB")
try:
red, green, blue = (int(color[index : index + 2], 16) for index in (1, 3, 5))
except ValueError as exc:
raise NativeCanCompileError("color_hex must use #RRGGBB") from exc
if not all(1 <= value <= 255 for value in (red, green, blue)):
raise NativeCanCompileError("CAN_DEV RGB components must be between 1 and 255")
brightness = self._integer(parameters, "brightness_pct", 0, 100)
payload = bytearray((0x2B, red, green, blue, 0, 0, 0, 0))
_set_bits(payload, 32, 7, brightness)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected="ambient_rgb")]
if capability == "roof.set_shade_motion":
motion = self._enum(parameters, "motion", {"open": 1, "close": 2, "half": 3, "stop": 4})
payload = bytearray(8)
_set_bits(payload, 4, 4, motion)
return [self._classic(0x431, payload, feedback_ids=(0x41A,), expected=f"shade_motion:{motion}")]
if capability == "roof.set_shade_position":
position = self._integer(parameters, "position_pct", 0, 100)
payload = bytearray(8)
payload[3] = 0xFF
payload[4] = position
return [self._classic(0x4F5, payload, feedback_ids=(0x41A,), expected=f"shade_position:{position}")]
raise NativeCanCompileError(f"CAN_DEV does not implement {capability}")
@staticmethod
def _classic(
frame_id: int,
payload: bytes | bytearray,
repeat: int = 1,
period_ms: int = 0,
feedback_ids: tuple[int, ...] = (),
expected: str = "",
) -> NativeFrameSpec:
return NativeFrameSpec(
frame_id,
bytes(payload),
repeat_count=repeat,
period_ms=period_ms,
feedback_ids=feedback_ids,
expected_feedback=expected,
)
@staticmethod
def _fd(
frame_id: int,
payload: bytes | bytearray,
repeat: int = 1,
period_ms: int = 0,
feedback_ids: tuple[int, ...] = (),
expected: str = "",
) -> NativeFrameSpec:
return NativeFrameSpec(
frame_id,
bytes(payload),
is_fd=True,
bitrate_switch=True,
repeat_count=repeat,
period_ms=period_ms,
feedback_ids=feedback_ids,
expected_feedback=expected,
)
@staticmethod
def _enum(parameters: dict[str, Any], name: str, values: dict[str, int]) -> int:
value = str(parameters.get(name))
try:
return values[value]
except KeyError as exc:
raise NativeCanCompileError(
f"{name} must be one of {', '.join(values)}"
) from exc
@staticmethod
def _integer(
parameters: dict[str, Any], name: str, minimum: int, maximum: int
) -> int:
value = parameters.get(name)
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise NativeCanCompileError(f"{name} must be an integer")
integer = int(value)
if integer != value or not minimum <= integer <= maximum:
raise NativeCanCompileError(
f"{name} must be between {minimum} and {maximum}"
)
return integer
@staticmethod
def _window_payload(target: str, motion: int) -> bytes:
masks = {
"front_left": 0b0001,
"rear_left": 0b0010,
"front_right": 0b0100,
"rear_right": 0b1000,
"front": 0b0101,
"rear": 0b1010,
"all": 0b1111,
}
try:
mask = masks[target]
except KeyError as exc:
raise NativeCanCompileError(f"unsupported window target {target}") from exc
starts = (48, 56, 51, 59)
payload = bytearray(8)
for index, start in enumerate(starts):
if mask & (1 << index):
_set_bits(payload, start, 3, motion)
return bytes(payload)
def _outlet_frame(
self, capability: str, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
ranges = self.calibration["air_outlet"]["ranges"]
if target not in ranges:
raise NativeCanCompileError(f"unsupported outlet target {target}")
if capability == "outlet.set_position_percent":
horizontal_pct = self._integer(parameters, "horizontal_pct", 0, 100)
vertical_pct = self._integer(parameters, "vertical_pct", 0, 100)
horizontal_range = ranges[target]["horizontal"]
vertical_range = ranges[target]["vertical"]
horizontal = horizontal_range[0] + (
(horizontal_range[1] - horizontal_range[0]) * horizontal_pct + 50
) // 100
vertical = vertical_range[0] + (
(vertical_range[1] - vertical_range[0]) * vertical_pct + 50
) // 100
else:
horizontal = self._integer(parameters, "horizontal_value", -128, 126)
vertical = self._integer(parameters, "vertical_value", -128, 126)
horizontal_range = ranges[target]["horizontal"]
vertical_range = ranges[target]["vertical"]
if not horizontal_range[0] <= horizontal <= horizontal_range[1]:
raise NativeCanCompileError("horizontal_value exceeds the calibrated range")
if not vertical_range[0] <= vertical <= vertical_range[1]:
raise NativeCanCompileError("vertical_value exceeds the calibrated range")
side, position = target.split("_", 1)
sub_id = 0x02 if side == "driver" else 0x03
starts = {
"left": (24, 8),
"right": (32, 16),
}
horizontal_start, vertical_start = starts[position]
payload = bytearray((sub_id, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0))
if sub_id == 0x02:
payload[6] = 0xFF
payload[7] = 0xFF
_set_bits(payload, horizontal_start, 8, horizontal + 128)
_set_bits(payload, vertical_start, 8, vertical + 128)
return self._fd(
0x4EF,
payload,
3,
100,
(0x2F5, 0x37B),
f"outlet:{target}:{horizontal}:{vertical}",
)
def _body_seat_frame(
self, capability: str, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
indexes = {"driver": 0, "passenger": 1, "left_rear": 2, "right_rear": 3}
if target not in indexes:
raise NativeCanCompileError(f"unsupported body seat target {target}")
state = str(parameters.get("state"))
if state not in {"on", "off"}:
raise NativeCanCompileError("state must be on or off")
feature = "heating" if capability.endswith("heating") else "ventilation"
index = indexes[target]
payload = bytearray(8)
if state == "off":
payload[0] = 0x20
starts = (12, 20, 28, 36) if feature == "heating" else (8, 16, 24, 32)
_set_bits(payload, starts[index], 2, 2)
else:
level = self._integer(parameters, "level", 1, 5)
payload[0] = 0x21
starts = (12, 20, 28, 36) if feature == "heating" else (8, 16, 24, 32)
_set_bits(payload, starts[index], 4, level)
return self._classic(0x4C1, payload, feedback_ids=(0x42A,), expected=f"body_seat:{target}:{feature}:{state}")
def _reading_light_frame(
self, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
state = self._enum(parameters, "state", {"off": 1, "on": 2})
if target == "all":
payload = bytearray((0, 0, 3, 0xFF, 0xFF, 0, 0, 0))
_set_bits(payload, 50, 2, state)
return self._fd(0x4F5, payload, 3, 100, (0x42E,), f"reading_all:{parameters['state']}")
starts = {
"front_left": 0,
"front_right": 2,
"middle_left": 4,
"middle_right": 6,
"middle_left_rotary": 8,
"middle_right_rotary": 10,
"rear_left": 12,
"rear_right": 14,
}
try:
start = starts[target]
except KeyError as exc:
raise NativeCanCompileError(f"unsupported reading-light target {target}") from exc
payload = bytearray(8)
_set_bits(payload, start, 2, state)
return self._fd(0x1B2, payload, 3, 100, expected=f"reading:{target}:{parameters['state']}")
class NativeCanExecutor:
"""Submit compiled vehicle frames and capture post-command feedback."""
def __init__(
self,
bus_factory: Callable[[], CanBus],
*,
feedback_timeout_ms: int = 1_200,
):
self.bus_factory = bus_factory
self.feedback_timeout_ms = feedback_timeout_ms
def execute(
self,
runtime: NativeCanRuntime,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
started_at = _now()
operations = {
operation["id"]: operation
for operation in function_config["spec"]["operations"]
}
results: list[dict[str, Any]] = []
bus = self.bus_factory()
try:
for command in command_plan["spec"]["commands"]:
operation = operations[command["operation_ref"]]
try:
if command["signals"].get("special_handler") == "window_position":
sent_frames, feedback = self._execute_window_position(bus, command)
else:
sent_frames = self._send_command(bus, command)
feedback = self._wait_feedback(bus, command)
feedback_match = self._feedback_matches(command, feedback)
feedback_required = bool(
str(command["signals"].get("feedback_ids", "")).strip()
)
verified = (
(not feedback_required or feedback is not None)
and feedback_match is not False
)
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "succeeded" if verified else "failed",
"message": (
"CAN 盒已提交车端控制帧并观察到匹配反馈。"
if feedback_match is True
else "CAN 盒已提交车端控制帧并观察到后续状态帧。"
if feedback is not None
else "CAN 盒已提交车端控制帧;当前反馈窗口未观察到对应状态帧。"
),
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": runtime.command_frame_public(command),
"sent_frames": [frame.public() for frame in sent_frames],
"feedback_frame": feedback.public() if feedback else None,
"feedback_observed": feedback is not None,
"feedback_match": feedback_match,
"feedback_required": feedback_required,
"channel": 1,
"simulated": False,
},
}
)
except Exception as exc: # noqa: BLE001 - normalize adapter faults
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "failed",
"message": str(exc),
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": runtime.command_frame_public(command),
"channel": 1,
"simulated": False,
},
}
)
break
finally:
bus.close()
return runtime._report(function_config, results, started_at, "vehicle")
@staticmethod
def _send_command(bus: CanBus, command: dict[str, Any]) -> list[CanFrame]:
signals = command["signals"]
frame_id = int(str(signals["frame_id"]), 0)
payload = bytes.fromhex(str(signals["data_hex"]))
repeat = command.get("repeat", {"count": 1, "period_ms": 0})
sent: list[CanFrame] = []
for index in range(int(repeat["count"])):
sent.append(
bus.send(
frame_id,
payload,
is_fd=bool(signals["is_fd"]),
bitrate_switch=bool(signals["bitrate_switch"]),
)
)
if index + 1 < int(repeat["count"]):
time.sleep(int(repeat["period_ms"]) / 1000)
return sent
def _wait_feedback(
self, bus: CanBus, command: dict[str, Any]
) -> CanFrame | None:
raw_ids = str(command["signals"].get("feedback_ids", ""))
feedback_ids = {
int(value, 0) for value in raw_ids.split(",") if value.strip()
}
if not feedback_ids:
return None
deadline = time.monotonic() + self.feedback_timeout_ms / 1000
while time.monotonic() < deadline:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1000))
frame = bus.receive(min(remaining_ms, 100), include_fd=True)
if frame is not None and frame.arbitration_id in feedback_ids:
return frame
return None
def _execute_window_position(
self, bus: CanBus, command: dict[str, Any]
) -> tuple[list[CanFrame], CanFrame | None]:
parameters = json.loads(command["signals"]["special_parameters"])
target = str(parameters["target"])
goal = int(parameters["position_pct"])
indexes = {"front_left": 0, "rear_left": 1, "front_right": 2, "rear_right": 3}
index = indexes[target]
before = self._wait_for_id(bus, 0x388, 2_500)
if before is None or len(before.data) < 6 or before.data[2 + index] > 100:
raise RuntimeError("未收到有效且新鲜的 0x388 车窗位置,拒绝启动机构")
current = int(before.data[2 + index])
if abs(current - goal) <= 3:
return [], before
motion = 1 if goal > current else 2
start_payload = NativeCanRuntime._window_payload(target, motion)
stop_payload = NativeCanRuntime._window_payload(target, 3)
sent: list[CanFrame] = []
for attempt in range(3):
sent.append(bus.send(0x431, start_payload, is_fd=True, bitrate_switch=True))
if attempt < 2:
time.sleep(0.1)
deadline = time.monotonic() + 12
reached: CanFrame | None = None
try:
while time.monotonic() < deadline:
frame = self._wait_for_id(bus, 0x388, 200)
if frame is None or len(frame.data) < 6 or frame.data[2 + index] > 100:
continue
if abs(int(frame.data[2 + index]) - goal) <= 3:
reached = frame
break
finally:
for attempt in range(3):
sent.append(bus.send(0x431, stop_payload, is_fd=True, bitrate_switch=True))
if attempt < 2:
time.sleep(0.1)
if reached is None:
raise RuntimeError("车窗在 12 秒内未到达目标,已发送停止序列")
return sent, reached
@staticmethod
def _wait_for_id(bus: CanBus, frame_id: int, timeout_ms: int) -> CanFrame | None:
deadline = time.monotonic() + timeout_ms / 1000
while time.monotonic() < deadline:
remaining = max(1, int((deadline - time.monotonic()) * 1000))
frame = bus.receive(min(remaining, 100), include_fd=True)
if frame is not None and frame.arbitration_id == frame_id:
return frame
return None
@staticmethod
def _feedback_matches(
command: dict[str, Any], frame: CanFrame | None
) -> bool | None:
if frame is None:
return None
expected = str(command["signals"].get("expected_feedback", ""))
if not expected or frame.arbitration_id != 0x404 or len(frame.data) < 6:
return None
parts = expected.split(":")
if parts[0] == "climate_power":
actual = frame.data[1] & 0x03
return actual == (1 if parts[1] == "on" else 2)
if parts[0] == "climate_control":
return ((frame.data[1] >> 2) & 0x03) == int(parts[1])
if parts[0] == "climate_mode":
return (frame.data[2] & 0x0F) == int(parts[1])
if parts[0] == "climate_fan":
return ((frame.data[2] >> 4) & 0x0F) == int(parts[1])
if parts[0] == "climate_temperature":
byte_index = {"driver": 4, "passenger": 5}.get(parts[1])
return None if byte_index is None else frame.data[byte_index] == int(parts[2])
return None
+438 -48
View File
@@ -8,6 +8,7 @@ import re
import sys import sys
import time import time
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy from copy import deepcopy
from datetime import UTC, datetime from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
@@ -46,10 +47,16 @@ from cognitive_agent.application.mode_profile import ( # noqa: E402
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402 from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
ModelPlanAdapter, ModelPlanAdapter,
APPLY_PRESET_CAPABILITY, APPLY_PRESET_CAPABILITY,
DYNAMIC_MODE_STOP_CAPABILITY,
SAVE_PRESET_CAPABILITY, SAVE_PRESET_CAPABILITY,
SEAT_CAPABILITIES, SEAT_CAPABILITIES,
SEAT_MOTION_CAPABILITIES, SEAT_MOTION_CAPABILITIES,
) )
from cognitive_agent.application.native_can_intent import merge_native_can_intent # noqa: E402
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402 from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
from cognitive_agent.application.vehicle_profile import ( # noqa: E402 from cognitive_agent.application.vehicle_profile import ( # noqa: E402
VehicleProfile, VehicleProfile,
@@ -63,7 +70,9 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareSafetyError, HardwareSafetyError,
PHYSICAL_BENCH_COORDINATOR, PHYSICAL_BENCH_COORDINATOR,
SeatHardwareSettings, SeatHardwareSettings,
VehicleCanSettings,
) )
from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig # noqa: E402
from cognitive_agent.infrastructure.can.access import ( # noqa: E402 from cognitive_agent.infrastructure.can.access import ( # noqa: E402
PHYSICAL_BENCH_ACCESS, PHYSICAL_BENCH_ACCESS,
) )
@@ -112,6 +121,18 @@ SEAT_AXIS_TO_CAPABILITY = {
SEAT_FUNCTION_TO_CAPABILITY = { SEAT_FUNCTION_TO_CAPABILITY = {
function: capability for capability, function in SEAT_CAPABILITIES.items() function: capability for capability, function in SEAT_CAPABILITIES.items()
} }
DYNAMIC_MODE_STOP_WORDS = ("停止", "关闭", "结束", "取消", "停下", "退出")
DYNAMIC_MODE_WORDS = (
"动态模式",
"动态座椅",
"木马模式",
"木马",
"摇摇马",
"骑木马",
"rocking horse",
"动态摇篮",
"摇篮模式",
)
class SeatDemoService: class SeatDemoService:
@@ -142,6 +163,8 @@ class SeatDemoService:
repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml", repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
) )
self.hardware_settings = SeatHardwareSettings.from_env(repository_root) self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
self.vehicle_can_settings = VehicleCanSettings.from_env(repository_root)
self.native_can_runtime = NativeCanRuntime(repository_root)
self.hardware_execution = PHYSICAL_BENCH_COORDINATOR self.hardware_execution = PHYSICAL_BENCH_COORDINATOR
self.hardware_access = PHYSICAL_BENCH_ACCESS self.hardware_access = PHYSICAL_BENCH_ACCESS
# Shared by the short-lived hardware executor instances. A process must # Shared by the short-lived hardware executor instances. A process must
@@ -192,8 +215,18 @@ class SeatDemoService:
"platform": "reference_vehicle", "platform": "reference_vehicle",
"execution_mode": "dry_run", "execution_mode": "dry_run",
"execution_modes": ["dry_run", "simulated_hardware", "hardware"], "execution_modes": ["dry_run", "simulated_hardware", "hardware"],
"hardware_execution_allowed": self.hardware_settings.configured, "hardware_execution_allowed": (
self.hardware_settings.configured
and self.vehicle_can_settings.configured
),
"hardware": hardware_info, "hardware": hardware_info,
"vehicle_can": self.vehicle_can_settings.public_info(),
"multi_channel_can": {
"adapter_ownership": "single_process",
"seat_channel": self.hardware_settings.channel,
"vehicle_channel": self.vehicle_can_settings.channel,
"channels_must_be_distinct": True,
},
"seat_position_memory_synchronized": bool( "seat_position_memory_synchronized": bool(
self._seat_position_memory_state.get("synchronized", False) self._seat_position_memory_state.get("synchronized", False)
), ),
@@ -298,10 +331,13 @@ class SeatDemoService:
"actuator_state": context["devices"], "actuator_state": context["devices"],
} }
def _new_hardware_executor(self) -> HardwareSeatExecutor: def _new_hardware_executor(
self, bus_factory: Any | None = None
) -> HardwareSeatExecutor:
resolved_bus_factory = bus_factory or self.hardware_settings.bus_factory()
return HardwareSeatExecutor( return HardwareSeatExecutor(
self.hardware_matrix, self.hardware_matrix,
self.hardware_settings.bus_factory(), resolved_bus_factory,
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms, thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
ack_timeout_ms=self.hardware_settings.ack_timeout_ms, ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
axis_maximum_x100={ axis_maximum_x100={
@@ -319,7 +355,7 @@ class SeatDemoService:
), ),
auto_home=self.hardware_settings.auto_home_enabled, auto_home=self.hardware_settings.auto_home_enabled,
home_timeout_s=self.hardware_settings.home_timeout_s, home_timeout_s=self.hardware_settings.home_timeout_s,
firmware_guard=self.hardware_settings.firmware_guard(), firmware_guard=self.hardware_settings.firmware_guard(resolved_bus_factory),
synchronize_position_before_first_motion=( synchronize_position_before_first_motion=(
self.hardware_settings.position_sync_enabled self.hardware_settings.position_sync_enabled
), ),
@@ -414,8 +450,6 @@ class SeatDemoService:
or mode_update is not None or mode_update is not None
) )
hardware_request = execution_mode == "hardware" hardware_request = execution_mode == "hardware"
if hardware_request and not control_plane_request:
self.hardware_settings.authorize(approval_token)
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}" conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id): if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens") raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
@@ -512,7 +546,40 @@ class SeatDemoService:
context, context,
vehicle_profile, vehicle_profile,
) )
parsed = self._apply_mode_profiles(parsed, matched_modes, context, vehicle_profile) if self._is_dynamic_mode_stop_request(utterance):
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
else:
parsed = self._apply_mode_profiles(
parsed, matched_modes, context, vehicle_profile
)
parsed = merge_native_can_intent(parsed, utterance)
parsed = self._partition_native_actions(
parsed, promote_supported=hardware_request
)
if hardware_request and not control_plane_request:
if parsed.get("actions"):
self.hardware_settings.authorize(approval_token)
if parsed.get("native_can_actions"):
self.vehicle_can_settings.authorize(approval_token)
if not parsed.get("actions") and not parsed.get("native_can_actions"):
raise HardwareSafetyError(
"hardware mode requires at least one verified seat or vehicle CAN operation"
)
if parsed.get("actions") and parsed.get("native_can_actions"):
if self.hardware_settings.backend != "zlgcan":
raise HardwareSafetyError(
"linked seat/vehicle execution requires the shared zlgcan backend"
)
try:
seat_channel = int(self.hardware_settings.channel, 0)
except ValueError as exc:
raise HardwareSafetyError(
"ZLG seat CAN channel must be an integer"
) from exc
if seat_channel == self.vehicle_can_settings.channel:
raise HardwareSafetyError(
"seat and vehicle CAN must use different adapter channels"
)
self._trace( self._trace(
traces, traces,
request_id, request_id,
@@ -580,14 +647,9 @@ class SeatDemoService:
}, },
) )
if parsed.get("domain_actions") or any( if parsed.get("domain_actions") or parsed.get("native_can_actions") or any(
domain != "seat" for domain in detected_domains domain != "seat" for domain in detected_domains
): ):
if hardware_request and not parsed.get("actions"):
raise HardwareSafetyError(
"hardware mode requires at least one passenger seat operation; "
"non-seat-only requests stay dry-run"
)
return self._process_orchestrated( return self._process_orchestrated(
utterance, utterance,
request_id, request_id,
@@ -1405,6 +1467,63 @@ class SeatDemoService:
) )
return merged return merged
@staticmethod
def _is_dynamic_mode_stop_request(utterance: str) -> bool:
compact = re.sub(r"\s+", "", utterance)
if any(negation in compact for negation in ("不要停止", "无需停止", "别停止")):
return False
return any(word in compact for word in DYNAMIC_MODE_STOP_WORDS) and any(
word in compact for word in DYNAMIC_MODE_WORDS
)
def _apply_dynamic_mode_stop_request(
self,
parsed: dict[str, Any],
utterance: str,
) -> dict[str, Any]:
"""Make explicit stop language authoritative over a matched start profile."""
stopped = deepcopy(parsed)
stopped["actions"] = [
action
for action in stopped.get("actions", [])
if action.get("action_type") not in {"dynamic_mode", "dynamic_mode_stop"}
]
stopped["actions"].append(
{"action_type": "dynamic_mode_stop", "target": "passenger"}
)
stopped["model_requests"] = [
request
for request in stopped.get("model_requests", [])
if request.get("capability")
not in {"seat.start_dynamic_mode", DYNAMIC_MODE_STOP_CAPABILITY}
]
stopped["model_requests"].append(
{
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": "passenger",
"operation": "deactivate",
"parameters": [],
"reason": "用户明确要求停止动态座椅循环。",
"source_span": utterance,
}
)
stopped["ambiguities"] = [
item
for item in stopped.get("ambiguities", [])
if item.get("field") != "capability"
]
stopped["detected_domains"] = self._merge_domains(
["seat"], stopped.get("detected_domains", [])
)
stopped["decision_summary"] = [
"识别到停止动态座椅模式指令;不会重新启动同名长期模式。",
*stopped.get("decision_summary", []),
]
stopped["confidence"] = max(float(stopped.get("confidence", 0)), 0.99)
return stopped
@staticmethod @staticmethod
def _contains_seat_preset_request(parsed: dict[str, Any]) -> bool: def _contains_seat_preset_request(parsed: dict[str, Any]) -> bool:
capabilities = { capabilities = {
@@ -1454,6 +1573,19 @@ class SeatDemoService:
action.get("target"), action.get("target"),
action.get("axis"), action.get("axis"),
) )
if action.get("action_type") == "dynamic_mode_stop":
return (
"seat",
"dynamic_mode_stop",
action.get("target"),
)
if action.get("action_type") == "dynamic_mode":
return (
"seat",
"dynamic_mode",
action.get("target"),
action.get("mode_id"),
)
return ( return (
"seat", "seat",
"function", "function",
@@ -2136,6 +2268,24 @@ class SeatDemoService:
"parameters": parameters, "parameters": parameters,
"reason": "用户固化的座椅位置模式动作。", "reason": "用户固化的座椅位置模式动作。",
} }
if action.get("action_type") == "dynamic_mode_stop":
return {
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": action["target"],
"operation": "deactivate",
"parameters": {},
"reason": "用户固化的动态座椅停止动作。",
}
if action.get("action_type") == "dynamic_mode":
return {
"domain": "seat",
"capability": "seat.start_dynamic_mode",
"target_scope": action["target"],
"operation": "set",
"parameters": {"mode_id": int(action["mode_id"])},
"reason": "用户固化的固件原生动态座椅模式。",
}
function = action.get("function") function = action.get("function")
capability = SEAT_FUNCTION_TO_CAPABILITY.get(str(function)) capability = SEAT_FUNCTION_TO_CAPABILITY.get(str(function))
if capability is None: if capability is None:
@@ -2152,6 +2302,71 @@ class SeatDemoService:
"reason": "用户固化的座椅舒适功能模式动作。", "reason": "用户固化的座椅舒适功能模式动作。",
} }
def _partition_native_actions(
self, parsed: dict[str, Any], *, promote_supported: bool = False
) -> dict[str, Any]:
"""Promote verified CAN_DEV capabilities and leave unsupported domains virtual."""
result = deepcopy(parsed)
native_actions = list(result.get("native_can_actions", []))
native_signatures = {
(
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
for action in native_actions
}
virtual_actions: list[dict[str, Any]] = []
for action in result.get("domain_actions", []):
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature in native_signatures:
continue
if promote_supported and self.native_can_runtime.supports(action):
promoted = dict(action)
promoted["execution_backend"] = "native_can"
native_actions.append(promoted)
else:
virtual_actions.append(action)
deduplicated: list[dict[str, Any]] = []
signatures: set[tuple[str, str, str, str]] = set()
for action in native_actions:
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature not in signatures:
signatures.add(signature)
deduplicated.append(action)
result["domain_actions"] = virtual_actions
result["native_can_actions"] = deduplicated
detected_domains = list(result.get("detected_domains", []))
if result.get("actions") and "seat" not in detected_domains:
detected_domains.insert(0, "seat")
for action in [*virtual_actions, *deduplicated]:
if action["domain"] not in detected_domains:
detected_domains.append(action["domain"])
result["detected_domains"] = detected_domains
result["deferred_requests"] = [
item
for item in result.get("deferred_requests", [])
if not (
item.get("execution_backend") == "native_can"
and self.native_can_runtime.supports(item)
)
]
return result
def _process_orchestrated( def _process_orchestrated(
self, self,
utterance: str, utterance: str,
@@ -2172,10 +2387,17 @@ class SeatDemoService:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default() vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
generation_started = time.perf_counter() generation_started = time.perf_counter()
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used) intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
for action in parsed.get("domain_actions", []): intent_actions = [
capability = self.domain_runtimes[action["domain"]].capabilities[ *parsed.get("domain_actions", []),
action["capability"] *parsed.get("native_can_actions", []),
] ]
for action in intent_actions:
if action in parsed.get("native_can_actions", []):
capability = self.native_can_runtime.capabilities[action["capability"]]
else:
capability = self.domain_runtimes[action["domain"]].capabilities[
action["capability"]
]
intent_ir["spec"]["requests"].append( intent_ir["spec"]["requests"].append(
{ {
"domain": action["domain"], "domain": action["domain"],
@@ -2212,6 +2434,20 @@ class SeatDemoService:
provider_used, provider_used,
) )
native_config_keys: set[str] = set()
native_actions_by_domain: dict[str, list[dict[str, Any]]] = {}
for action in parsed.get("native_can_actions", []):
native_actions_by_domain.setdefault(action["domain"], []).append(action)
for domain, actions in native_actions_by_domain.items():
key = f"native_{domain}"
native_config_keys.add(key)
configs[key] = self.native_can_runtime.build_function_config(
domain,
actions,
request_id,
provider_used,
)
result["function_configs"] = configs result["function_configs"] = configs
result["function_config"] = ( result["function_config"] = (
configs.get("seat") or next(iter(configs.values()), None) configs.get("seat") or next(iter(configs.values()), None)
@@ -2252,7 +2488,8 @@ class SeatDemoService:
"message": ambiguity["question"], "message": ambiguity["question"],
} }
) )
validation.append( if not native_config_keys:
validation.append(
{ {
"severity": "warning", "severity": "warning",
"code": "VIRTUAL_EXECUTION_ONLY", "code": "VIRTUAL_EXECUTION_ONLY",
@@ -2269,8 +2506,8 @@ class SeatDemoService:
"code": "NO_EXECUTABLE_OPERATION", "code": "NO_EXECUTABLE_OPERATION",
"path": "spec.operations", "path": "spec.operations",
"message": "模型没有生成任何可执行领域操作。", "message": "模型没有生成任何可执行领域操作。",
} }
) )
if hardware_request: if hardware_request:
self._adapt_validation_for_hardware(validation) self._adapt_validation_for_hardware(validation)
result["validation"] = validation result["validation"] = validation
@@ -2310,6 +2547,8 @@ class SeatDemoService:
) )
else: else:
plans[domain] = self.compiler.compile(config) plans[domain] = self.compiler.compile(config)
elif domain in native_config_keys:
plans[domain] = self.native_can_runtime.compile(config)
else: else:
runtime = self.domain_runtimes[domain] runtime = self.domain_runtimes[domain]
plans[domain] = runtime.compile(config) plans[domain] = runtime.compile(config)
@@ -2388,37 +2627,47 @@ class SeatDemoService:
execute_started = time.perf_counter() execute_started = time.perf_counter()
execution_started_at = _now() execution_started_at = _now()
reports: dict[str, dict[str, Any]] = {} reports = self._execute_orchestrated_reports(
for domain, config in configs.items(): configs,
if domain == "seat": plans,
if hardware_request: native_config_keys,
reports[domain] = self._new_hardware_executor().execute( context,
plans[domain], config, context["devices"] hardware_request=hardware_request,
) simulated_hardware_request=simulated_hardware_request,
elif simulated_hardware_request: simulation_fault=simulation_fault,
reports[domain] = self.simulated_hardware_executor.execute( )
plans[domain],
config,
context["devices"],
fault_scenario=simulation_fault,
)
else:
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
)
else:
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
report_results = [ report_results = [
item item
for domain_report in reports.values() for domain_report in reports.values()
for item in domain_report["spec"]["results"] for item in domain_report["spec"]["results"]
] ]
operation_results = [
item
for item in report_results
if item.get("ref") != "seat-controller-firmware"
]
operation_domains = {
str(item.get("domain")) for item in operation_results
}
successful_domains = {
domain
for domain in operation_domains
if all(
item.get("status") == "succeeded"
for item in operation_results
if str(item.get("domain")) == domain
)
}
failed_domains = {
str(item.get("domain"))
for item in operation_results
if item.get("status") == "failed"
}
execution_succeeded = all( execution_succeeded = all(
domain_report["spec"]["status"] == "succeeded" domain_report["spec"]["status"] == "succeeded"
for domain_report in reports.values() for domain_report in reports.values()
) and all(item.get("status") == "succeeded" for item in report_results) ) and all(item.get("status") == "succeeded" for item in report_results)
execution_partially_succeeded = bool(successful_domains and failed_domains)
physical_state = ( physical_state = (
reports.get("seat", {}).get("spec", {}).get("physical_state") reports.get("seat", {}).get("spec", {}).get("physical_state")
if hardware_request if hardware_request
@@ -2477,6 +2726,8 @@ class SeatDemoService:
result["validation"] = validation result["validation"] = validation
if report_issues: if report_issues:
result["status"] = "execution_report_invalid" result["status"] = "execution_report_invalid"
elif execution_partially_succeeded:
result["status"] = "partially_succeeded"
elif not execution_succeeded: elif not execution_succeeded:
result["status"] = "execution_failed" result["status"] = "execution_failed"
elif parsed.get("deferred_requests"): elif parsed.get("deferred_requests"):
@@ -2496,7 +2747,14 @@ class SeatDemoService:
"副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。" "副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。"
if hardware_request and execution_succeeded if hardware_request and execution_succeeded
else ( else (
f"副驾座椅真实执行失败:{failed_execution['message']}" (
"跨域指令部分执行成功;成功域="
f"{','.join(sorted(successful_domains))},失败域="
f"{','.join(sorted(failed_domains))}:"
f"{failed_execution['message']}"
)
if execution_partially_succeeded and failed_execution
else f"真实 CAN 执行失败:{failed_execution['message']}"
if hardware_request and failed_execution if hardware_request and failed_execution
else ( else (
f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。" f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。"
@@ -2523,19 +2781,40 @@ class SeatDemoService:
"failed_domain": ( "failed_domain": (
failed_execution.get("domain") if failed_execution else None failed_execution.get("domain") if failed_execution else None
), ),
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
"partially_succeeded": execution_partially_succeeded,
}, },
domain="orchestration", domain="orchestration",
) )
if execution_succeeded and not report_issues: if (execution_succeeded or execution_partially_succeeded) and not report_issues:
seat_actions = parsed.get("actions", []) seat_actions = (
if hardware_request: parsed.get("actions", [])
if execution_succeeded or "seat" in successful_domains
else []
)
if hardware_request and seat_actions and "seat" in reports:
seat_actions = self._hardware_feedback_actions( seat_actions = self._hardware_feedback_actions(
seat_actions, seat_actions,
configs["seat"], configs["seat"],
reports["seat"], reports["seat"],
) )
all_actions = [*seat_actions, *parsed.get("domain_actions", [])] all_actions = [
*seat_actions,
*[
action
for action in parsed.get("domain_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
*[
action
for action in parsed.get("native_can_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
]
updated_context = self.conversations.apply_successful_actions( updated_context = self.conversations.apply_successful_actions(
conversation_id, conversation_id,
utterance, utterance,
@@ -2559,11 +2838,17 @@ class SeatDemoService:
request_id, request_id,
"state_update", "state_update",
"succeeded", "succeeded",
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。", (
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。"
if execution_succeeded
else "仅成功设备已写入会话状态;失败设备保持执行前状态。"
),
time.perf_counter(), time.perf_counter(),
{ {
"seat_state": updated_context["devices"], "seat_state": updated_context["devices"],
"domain_state": updated_context["domain_devices"], "domain_state": updated_context["domain_devices"],
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
}, },
domain="orchestration", domain="orchestration",
) )
@@ -2595,6 +2880,111 @@ class SeatDemoService:
self._persist(result) self._persist(result)
return result return result
def _execute_orchestrated_reports(
self,
configs: dict[str, dict[str, Any]],
plans: dict[str, dict[str, Any]],
native_config_keys: set[str],
context: dict[str, Any],
*,
hardware_request: bool,
simulated_hardware_request: bool,
simulation_fault: str,
) -> dict[str, dict[str, Any]]:
reports: dict[str, dict[str, Any]] = {}
bridge = None
seat_bus_factory = None
native_bus_factory = None
if hardware_request and native_config_keys:
channel_configs = [self.vehicle_can_settings.channel_config()]
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
channel_configs.insert(
0,
ZlgChannelConfig(
channel=seat_channel,
arbitration_bitrate=self.hardware_settings.bitrate,
data_bitrate=self.vehicle_can_settings.data_bitrate,
can_fd_iso=True,
),
)
bridge = self.vehicle_can_settings.bridge(channel_configs)
native_bus_factory = lambda: bridge.channel_bus(
self.vehicle_can_settings.channel
)
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
try:
def execute_native_configs() -> dict[str, dict[str, Any]]:
native_reports: dict[str, dict[str, Any]] = {}
for key in sorted(native_config_keys):
if not hardware_request:
native_reports[key] = self.native_can_runtime.execute_virtual(
plans[key], configs[key]
)
continue
executor = NativeCanExecutor(
native_bus_factory,
feedback_timeout_ms=(
self.vehicle_can_settings.feedback_timeout_ms
),
)
native_reports[key] = executor.execute(
self.native_can_runtime,
plans[key],
configs[key],
)
return native_reports
def execute_seat_config() -> dict[str, Any]:
config = configs["seat"]
if hardware_request:
seat_executor = (
self._new_hardware_executor(seat_bus_factory)
if seat_bus_factory is not None
else self._new_hardware_executor()
)
return seat_executor.execute(
plans["seat"], config, context["devices"]
)
if simulated_hardware_request:
return self.simulated_hardware_executor.execute(
plans["seat"],
config,
context["devices"],
fault_scenario=simulation_fault,
)
return self.executor.execute(
plans["seat"], config, context["devices"]
)
if hardware_request and native_config_keys and "seat" in configs:
# Both channel views share one adapter owner; executor threads let
# the two channels start within normal USB scheduling jitter.
with ThreadPoolExecutor(max_workers=2) as pool:
native_future = pool.submit(execute_native_configs)
seat_future = pool.submit(execute_seat_config)
reports.update(native_future.result())
reports["seat"] = seat_future.result()
else:
reports.update(execute_native_configs())
if "seat" in configs:
reports["seat"] = execute_seat_config()
for domain, config in configs.items():
if domain in native_config_keys or domain == "seat":
continue
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
finally:
if bridge is not None:
bridge.close()
return reports
@staticmethod @staticmethod
def _build_scene_config( def _build_scene_config(
request_id: str, request_id: str,
+181 -39
View File
@@ -5,6 +5,7 @@ from __future__ import annotations
import json import json
import subprocess import subprocess
import threading import threading
import time
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Any, Protocol from typing import Any, Protocol
@@ -19,6 +20,9 @@ class CanFrame:
arbitration_id: int arbitration_id: int
data: bytes data: bytes
timestamp: float | None = None timestamp: float | None = None
channel: int | None = None
is_fd: bool = False
bitrate_switch: bool = False
def public(self) -> dict[str, Any]: def public(self) -> dict[str, Any]:
return { return {
@@ -26,13 +30,25 @@ class CanFrame:
"payload_size": len(self.data), "payload_size": len(self.data),
"data_hex": " ".join(f"{byte:02X}" for byte in self.data), "data_hex": " ".join(f"{byte:02X}" for byte in self.data),
"timestamp": self.timestamp, "timestamp": self.timestamp,
"channel": self.channel,
"is_fd": self.is_fd,
"bitrate_switch": self.bitrate_switch,
} }
class CanBus(Protocol): class CanBus(Protocol):
def send(self, arbitration_id: int, data: bytes) -> CanFrame: ... def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame: ...
def receive(self, timeout_ms: int) -> CanFrame | None: ... def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None: ...
def close(self) -> None: ... def close(self) -> None: ...
@@ -58,7 +74,16 @@ class PythonCanBus:
except Exception as exc: # noqa: BLE001 - normalize vendor exceptions except Exception as exc: # noqa: BLE001 - normalize vendor exceptions
raise CanGatewayError(f"cannot open {interface} CAN channel {channel}: {exc}") from exc raise CanGatewayError(f"cannot open {interface} CAN channel {channel}: {exc}") from exc
def send(self, arbitration_id: int, data: bytes) -> CanFrame: def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
if is_fd:
raise CanGatewayError("PythonCanBus was opened for classic CAN only")
if len(data) > 8: if len(data) > 8:
raise CanGatewayError("the T2G bench accepts classic CAN frames of at most 8 bytes") raise CanGatewayError("the T2G bench accepts classic CAN frames of at most 8 bytes")
message = self._can.Message( message = self._can.Message(
@@ -71,9 +96,11 @@ class PythonCanBus:
self._bus.send(message, timeout=0.5) self._bus.send(message, timeout=0.5)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
raise CanGatewayError(f"CAN transmit failed: {exc}") from exc raise CanGatewayError(f"CAN transmit failed: {exc}") from exc
return CanFrame(arbitration_id, bytes(data)) return CanFrame(arbitration_id, bytes(data), is_fd=False)
def receive(self, timeout_ms: int) -> CanFrame | None: def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
try: try:
message = self._bus.recv(max(0, timeout_ms) / 1000) message = self._bus.recv(max(0, timeout_ms) / 1000)
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -84,14 +111,36 @@ class PythonCanBus:
int(message.arbitration_id), int(message.arbitration_id),
bytes(message.data), bytes(message.data),
float(message.timestamp) if message.timestamp is not None else None, float(message.timestamp) if message.timestamp is not None else None,
is_fd=bool(getattr(message, "is_fd", False)),
bitrate_switch=bool(getattr(message, "bitrate_switch", False)),
) )
def close(self) -> None: def close(self) -> None:
self._bus.shutdown() self._bus.shutdown()
class ZlgCanBridgeBus: @dataclass(frozen=True)
"""Use the installed 32-bit ZLG driver from the 64-bit application process.""" class ZlgChannelConfig:
"""Configuration for one channel on a shared ZLG CAN FD adapter."""
channel: int
arbitration_bitrate: int = 500_000
data_bitrate: int = 2_000_000
can_fd_iso: bool = True
termination_enabled: bool = True
def public(self) -> dict[str, Any]:
return {
"channel": self.channel,
"arbitration_bitrate": self.arbitration_bitrate,
"data_bitrate": self.data_bitrate,
"can_fd_iso": self.can_fd_iso,
"termination_enabled": self.termination_enabled,
}
class ZlgCanBridgeProcess:
"""Own one device handle and expose several independently addressed channels."""
def __init__( def __init__(
self, self,
@@ -100,9 +149,15 @@ class ZlgCanBridgeBus:
dll_path: Path, dll_path: Path,
device_type: int, device_type: int,
device_index: int, device_index: int,
channel: int, channels: list[ZlgChannelConfig],
bitrate: int, transmit_type: int = 0,
): ):
if not channels:
raise CanGatewayError("at least one ZLG channel must be configured")
channel_ids = [item.channel for item in channels]
if len(channel_ids) != len(set(channel_ids)):
raise CanGatewayError("ZLG channel numbers must be unique")
self._channels = frozenset(channel_ids)
command = [ command = [
str(python_executable), str(python_executable),
str(bridge_script), str(bridge_script),
@@ -112,10 +167,10 @@ class ZlgCanBridgeBus:
str(device_type), str(device_type),
"--device-index", "--device-index",
str(device_index), str(device_index),
"--channel", "--channels-json",
str(channel), json.dumps([item.public() for item in channels], separators=(",", ":")),
"--bitrate", "--transmit-type",
str(bitrate), str(transmit_type),
] ]
try: try:
self._process = subprocess.Popen( # noqa: S603 - paths are trusted server config self._process = subprocess.Popen( # noqa: S603 - paths are trusted server config
@@ -135,6 +190,11 @@ class ZlgCanBridgeBus:
self.close() self.close()
raise CanGatewayError(str(ready.get("message", "ZLG CAN bridge did not start"))) raise CanGatewayError(str(ready.get("message", "ZLG CAN bridge did not start")))
def channel_bus(self, channel: int) -> "ZlgCanChannelBus":
if channel not in self._channels:
raise CanGatewayError(f"ZLG channel {channel} was not initialized")
return ZlgCanChannelBus(self, channel)
def _read_response(self) -> dict[str, Any]: def _read_response(self) -> dict[str, Any]:
if self._process.stdout is None: if self._process.stdout is None:
raise CanGatewayError("ZLG CAN bridge stdout is unavailable") raise CanGatewayError("ZLG CAN bridge stdout is unavailable")
@@ -160,32 +220,6 @@ class ZlgCanBridgeBus:
self._process.stdin.flush() self._process.stdin.flush()
return self._read_response() return self._read_response()
def send(self, arbitration_id: int, data: bytes) -> CanFrame:
response = self._request(
{
"operation": "send",
"arbitration_id": arbitration_id,
"data_hex": data.hex(),
}
)
return CanFrame(
int(response["arbitration_id"]),
bytes.fromhex(response["data_hex"]),
)
def receive(self, timeout_ms: int) -> CanFrame | None:
response = self._request(
{"operation": "receive", "timeout_ms": max(0, int(timeout_ms))}
)
frame = response.get("frame")
if frame is None:
return None
return CanFrame(
int(frame["arbitration_id"]),
bytes.fromhex(frame["data_hex"]),
float(frame["timestamp_us"]) / 1_000_000,
)
def close(self) -> None: def close(self) -> None:
process = getattr(self, "_process", None) process = getattr(self, "_process", None)
if process is None or process.poll() is not None: if process is None or process.poll() is not None:
@@ -202,6 +236,114 @@ class ZlgCanBridgeBus:
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
process.kill() process.kill()
def __enter__(self) -> "ZlgCanBridgeProcess":
return self
def __exit__(self, *_: object) -> None:
self.close()
class ZlgCanChannelBus:
"""Channel-scoped view over a shared :class:`ZlgCanBridgeProcess`."""
def __init__(self, bridge: ZlgCanBridgeProcess, channel: int):
self._bridge = bridge
self.channel = channel
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
response = self._bridge._request(
{
"operation": "send",
"channel": self.channel,
"arbitration_id": arbitration_id,
"data_hex": data.hex(),
"is_fd": is_fd,
"bitrate_switch": bitrate_switch,
}
)
return CanFrame(
int(response["arbitration_id"]),
bytes.fromhex(response["data_hex"]),
channel=self.channel,
is_fd=bool(response.get("is_fd", False)),
bitrate_switch=bool(response.get("bitrate_switch", False)),
)
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
requested_ms = max(0, int(timeout_ms))
deadline = time.monotonic() + requested_ms / 1000
while True:
remaining_ms = max(0, int((deadline - time.monotonic()) * 1000))
response = self._bridge._request(
{
"operation": "receive",
"channel": self.channel,
# Short slices let another channel transmit or receive while
# this synchronous JSON bridge is waiting for feedback.
"timeout_ms": min(20, remaining_ms),
"include_fd": include_fd,
}
)
frame = response.get("frame")
if frame is not None:
return CanFrame(
int(frame["arbitration_id"]),
bytes.fromhex(frame["data_hex"]),
float(frame["timestamp_us"]) / 1_000_000,
channel=self.channel,
is_fd=bool(frame.get("is_fd", False)),
bitrate_switch=bool(frame.get("bitrate_switch", False)),
)
if requested_ms == 0 or time.monotonic() >= deadline:
return None
def close(self) -> None:
# The owner closes all channel handles together. Executors may safely close views.
return None
class ZlgCanBridgeBus(ZlgCanChannelBus):
"""Backward-compatible single-channel ZLG bus."""
def __init__(
self,
python_executable: Path,
bridge_script: Path,
dll_path: Path,
device_type: int,
device_index: int,
channel: int,
bitrate: int,
termination_enabled: bool = True,
):
self._owner = ZlgCanBridgeProcess(
python_executable=python_executable,
bridge_script=bridge_script,
dll_path=dll_path,
device_type=device_type,
device_index=device_index,
channels=[
ZlgChannelConfig(
channel=channel,
arbitration_bitrate=bitrate,
termination_enabled=termination_enabled,
)
],
)
super().__init__(self._owner, channel)
def close(self) -> None:
self._owner.close()
def __enter__(self) -> "ZlgCanBridgeBus": def __enter__(self) -> "ZlgCanBridgeBus":
return self return self
@@ -19,7 +19,14 @@ from cognitive_agent.infrastructure.firmware import (
T2GSeatProgrammer, T2GSeatProgrammer,
) )
from .gateway import CanBus, CanGatewayError, PythonCanBus, ZlgCanBridgeBus from .gateway import (
CanBus,
CanGatewayError,
PythonCanBus,
ZlgCanBridgeBus,
ZlgCanBridgeProcess,
ZlgChannelConfig,
)
TRUE_VALUES = {"1", "true", "yes", "on"} TRUE_VALUES = {"1", "true", "yes", "on"}
@@ -32,6 +39,8 @@ CAPABILITY_PROFILE = {
"seat.set_cushion_tilt": "combined", "seat.set_cushion_tilt": "combined",
"seat.set_backrest_recline": "combined", "seat.set_backrest_recline": "combined",
"seat.set_legrest_height": "combined", "seat.set_legrest_height": "combined",
"seat.start_dynamic_mode": "combined",
"seat.stop_dynamic_mode": "combined",
} }
@@ -132,6 +141,7 @@ class SeatHardwareSettings:
profiles: frozenset[str] profiles: frozenset[str]
backend: str backend: str
bitrate: int bitrate: int
termination_enabled: bool
channel: str channel: str
interface: str interface: str
device_type: int device_type: int
@@ -196,6 +206,9 @@ class SeatHardwareSettings:
profiles=_profiles_from_env(), profiles=_profiles_from_env(),
backend=backend, backend=backend,
bitrate=_env_int("COGNITIVE_AGENT_SEAT_CAN_BITRATE", 500_000), bitrate=_env_int("COGNITIVE_AGENT_SEAT_CAN_BITRATE", 500_000),
termination_enabled=_env_bool(
"COGNITIVE_AGENT_SEAT_CAN_TERMINATION_ENABLED", default=True
),
channel=os.getenv("COGNITIVE_AGENT_SEAT_CAN_CHANNEL", "0").strip(), channel=os.getenv("COGNITIVE_AGENT_SEAT_CAN_CHANNEL", "0").strip(),
interface=os.getenv("COGNITIVE_AGENT_SEAT_CAN_INTERFACE", "socketcan").strip(), interface=os.getenv("COGNITIVE_AGENT_SEAT_CAN_INTERFACE", "socketcan").strip(),
device_type=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE", 41), device_type=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE", 41),
@@ -234,7 +247,7 @@ class SeatHardwareSettings:
"COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S", 120 "COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S", 120
), ),
firmware_probe_timeout_ms=_env_int( firmware_probe_timeout_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 1_200 "COGNITIVE_AGENT_SEAT_FIRMWARE_PROBE_TIMEOUT_MS", 3_500
), ),
firmware_boot_wait_ms=_env_int( firmware_boot_wait_ms=_env_int(
"COGNITIVE_AGENT_SEAT_FIRMWARE_BOOT_WAIT_MS", 1_500 "COGNITIVE_AGENT_SEAT_FIRMWARE_BOOT_WAIT_MS", 1_500
@@ -368,6 +381,7 @@ class SeatHardwareSettings:
device_index=self.device_index, device_index=self.device_index,
channel=numeric_channel, channel=numeric_channel,
bitrate=self.bitrate, bitrate=self.bitrate,
termination_enabled=self.termination_enabled,
) )
return create_zlg_bus return create_zlg_bus
@@ -390,9 +404,11 @@ class SeatHardwareSettings:
timeout_ms=self.firmware_program_timeout_ms, timeout_ms=self.firmware_program_timeout_ms,
) )
def firmware_guard(self) -> T2GFirmwareGuard: def firmware_guard(
self, bus_factory: Callable[[], CanBus] | None = None
) -> T2GFirmwareGuard:
return T2GFirmwareGuard( return T2GFirmwareGuard(
self.bus_factory(), bus_factory or self.bus_factory(),
self._programmer() if self.auto_flash_enabled else None, self._programmer() if self.auto_flash_enabled else None,
auto_flash=self.auto_flash_enabled, auto_flash=self.auto_flash_enabled,
probe_timeout_ms=self.firmware_probe_timeout_ms, probe_timeout_ms=self.firmware_probe_timeout_ms,
@@ -420,6 +436,7 @@ class SeatHardwareSettings:
"adapter_configured": self.adapter_configured, "adapter_configured": self.adapter_configured,
"channel": self.channel, "channel": self.channel,
"bitrate": self.bitrate, "bitrate": self.bitrate,
"termination_enabled": self.termination_enabled,
"frame_format": "classic_can_11_bit", "frame_format": "classic_can_11_bit",
"firmware_profiles": sorted(self.profiles), "firmware_profiles": sorted(self.profiles),
"firmware_images": images, "firmware_images": images,
@@ -450,3 +467,209 @@ class SeatHardwareSettings:
"current diagnostics use conservative bench thresholds and do not replace external power limiting", "current diagnostics use conservative bench thresholds and do not replace external power limiting",
], ],
} }
@dataclass(frozen=True)
class VehicleCanSettings:
"""Fail-closed channel/timing contract imported from the verified CAN_DEV branch."""
repository_root: Path
enabled: bool
approval_required: bool
approval_token: str
backend: str
channel: int
arbitration_bitrate: int
data_bitrate: int
termination_enabled: bool
clock_hz: int
arbitration_tseg1: int
arbitration_tseg2: int
arbitration_sjw: int
arbitration_brp: int
data_tseg1: int
data_tseg2: int
data_sjw: int
data_brp: int
feedback_timeout_ms: int
device_type: int
device_index: int
zlg_python: Path
zlg_dll: Path
bridge_script: Path
@classmethod
def from_env(cls, repository_root: Path) -> "VehicleCanSettings":
return cls(
repository_root=repository_root,
enabled=_env_bool(
"COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
default=_env_bool("COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED"),
),
approval_required=_env_bool(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED", default=True
),
approval_token=os.getenv("COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN", ""),
backend=os.getenv(
"COGNITIVE_AGENT_VEHICLE_CAN_BACKEND", "zlgcan"
).strip().lower(),
channel=_env_int("COGNITIVE_AGENT_VEHICLE_CAN_CHANNEL", 1),
arbitration_bitrate=_env_int(
"COGNITIVE_AGENT_VEHICLE_CAN_ARBITRATION_BITRATE", 500_000
),
data_bitrate=_env_int(
"COGNITIVE_AGENT_VEHICLE_CAN_DATA_BITRATE", 2_000_000
),
termination_enabled=_env_bool(
"COGNITIVE_AGENT_VEHICLE_CAN_TERMINATION_ENABLED", default=False
),
clock_hz=60_000_000,
arbitration_tseg1=14,
arbitration_tseg2=3,
arbitration_sjw=2,
arbitration_brp=5,
data_tseg1=10,
data_tseg2=2,
data_sjw=2,
data_brp=1,
feedback_timeout_ms=_env_int(
"COGNITIVE_AGENT_VEHICLE_CAN_FEEDBACK_TIMEOUT_MS", 1_500
),
device_type=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_TYPE", 41),
device_index=_env_int("COGNITIVE_AGENT_ZLGCAN_DEVICE_INDEX", 0),
zlg_python=Path(
os.getenv(
"COGNITIVE_AGENT_ZLGCAN_PYTHON",
r"C:\Program Files (x86)\ZCANPRO\python38\python.exe",
)
),
zlg_dll=Path(
os.getenv(
"COGNITIVE_AGENT_ZLGCAN_DLL",
r"C:\Program Files (x86)\ZCANPRO\zlgcan.dll",
)
),
bridge_script=repository_root
/ "src"
/ "cognitive_agent"
/ "infrastructure"
/ "can"
/ "zlgcan_bridge.py",
)
@property
def adapter_configured(self) -> bool:
return bool(
self.backend == "zlgcan"
and self.zlg_python.is_file()
and self.zlg_dll.is_file()
and self.bridge_script.is_file()
)
@property
def configured(self) -> bool:
return bool(
self.enabled
and (not self.approval_required or self.approval_token)
and self.adapter_configured
and self.timing_is_verified
)
@property
def timing_is_verified(self) -> bool:
return (
self.arbitration_bitrate == 500_000
and self.data_bitrate == 2_000_000
and 60_000_000
// ((self.arbitration_brp + 1) * (self.arbitration_tseg1 + self.arbitration_tseg2 + 3))
== 500_000
and 60_000_000
// ((self.data_brp + 1) * (self.data_tseg1 + self.data_tseg2 + 3))
== 2_000_000
)
def authorize(self, supplied_token: str | None) -> None:
if not self.enabled:
raise HardwareSafetyError(
"vehicle CAN execution is disabled; set COGNITIVE_AGENT_VEHICLE_CAN_ENABLED=true"
)
if self.approval_required:
if not self.approval_token:
raise HardwareSafetyError(
"COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN is not configured"
)
if not supplied_token or not hmac.compare_digest(
supplied_token, self.approval_token
):
raise HardwareSafetyError("hardware approval token is invalid")
if self.backend != "zlgcan":
raise HardwareSafetyError(
"vehicle CAN FD currently requires the zlgcan multi-channel backend"
)
if self.channel < 0:
raise HardwareSafetyError("vehicle CAN channel cannot be negative")
if not self.timing_is_verified:
raise HardwareSafetyError(
"vehicle CAN timing must remain ISO CAN FD 500 kbit/s / 2 Mbit/s at 80% sample point"
)
if not 100 <= self.feedback_timeout_ms <= 10_000:
raise HardwareSafetyError(
"COGNITIVE_AGENT_VEHICLE_CAN_FEEDBACK_TIMEOUT_MS must be between 100 and 10000"
)
if not self.adapter_configured:
raise HardwareSafetyError("the configured ZLG CAN FD runtime is not ready")
def channel_config(self) -> ZlgChannelConfig:
return ZlgChannelConfig(
channel=self.channel,
arbitration_bitrate=self.arbitration_bitrate,
data_bitrate=self.data_bitrate,
can_fd_iso=True,
termination_enabled=self.termination_enabled,
)
def bridge(self, channels: list[ZlgChannelConfig]) -> ZlgCanBridgeProcess:
return ZlgCanBridgeProcess(
python_executable=self.zlg_python,
bridge_script=self.bridge_script,
dll_path=self.zlg_dll,
device_type=self.device_type,
device_index=self.device_index,
channels=channels,
)
def public_info(self) -> dict[str, Any]:
return {
"enabled": self.enabled,
"configured": self.configured,
"backend": self.backend,
"platform": "t2g_cabin_bench",
"channel": self.channel,
"mode": 0,
"work_mode": "normal",
"can_fd_standard": "ISO",
"termination_enabled": self.termination_enabled,
"clock_hz": self.clock_hz,
"arbitration": {
"bitrate": self.arbitration_bitrate,
"tseg1": self.arbitration_tseg1,
"tseg2": self.arbitration_tseg2,
"sjw": self.arbitration_sjw,
"smp": 0,
"brp": self.arbitration_brp,
"total_tq": 20,
"sample_point_percent": 80,
},
"data": {
"bitrate": self.data_bitrate,
"tseg1": self.data_tseg1,
"tseg2": self.data_tseg2,
"sjw": self.data_sjw,
"smp": 0,
"brp": self.data_brp,
"total_tq": 15,
"sample_point_percent": 80,
},
"source_branch": "project-code/CAN_DEV",
"source_commit": "6e7ed04a468dfcde7efda9e30bc15144bf02faf9",
}
@@ -1,7 +1,9 @@
"""32-bit JSON-line bridge for the ZLG zlgcan.dll API. """32-bit JSON-line bridge for a multi-channel ZLG CAN FD adapter.
This module deliberately has no project or third-party imports so it can run under The module deliberately has no project or third-party imports. It runs under
the 32-bit Python bundled with ZCANPRO while the web service uses 64-bit Python. the 32-bit Python bundled with ZCANPRO while the application uses 64-bit Python.
One bridge owns the physical adapter once and initializes every requested
channel, which avoids two processes racing to open the same USB device.
""" """
from __future__ import annotations from __future__ import annotations
@@ -10,10 +12,12 @@ import argparse
import ctypes import ctypes
import json import json
import sys import sys
import time
ZCAN_TYPE_CANFD = 1 ZCAN_TYPE_CANFD = 1
ZCAN_STATUS_OK = 1 ZCAN_STATUS_OK = 1
CANFD_BRS = 0x01
class ZcanClassicInit(ctypes.Structure): class ZcanClassicInit(ctypes.Structure):
@@ -62,6 +66,17 @@ class ZcanFrame(ctypes.Structure):
] ]
class ZcanFdFrame(ctypes.Structure):
_fields_ = [
("can_id", ctypes.c_uint32),
("len", ctypes.c_uint8),
("flags", ctypes.c_uint8),
("reserved0", ctypes.c_uint8),
("reserved1", ctypes.c_uint8),
("data", ctypes.c_uint8 * 64),
]
class ZcanTransmitData(ctypes.Structure): class ZcanTransmitData(ctypes.Structure):
_fields_ = [("frame", ZcanFrame), ("transmit_type", ctypes.c_uint32)] _fields_ = [("frame", ZcanFrame), ("transmit_type", ctypes.c_uint32)]
@@ -70,6 +85,14 @@ class ZcanReceiveData(ctypes.Structure):
_fields_ = [("frame", ZcanFrame), ("timestamp", ctypes.c_uint64)] _fields_ = [("frame", ZcanFrame), ("timestamp", ctypes.c_uint64)]
class ZcanTransmitFdData(ctypes.Structure):
_fields_ = [("frame", ZcanFdFrame), ("transmit_type", ctypes.c_uint32)]
class ZcanReceiveFdData(ctypes.Structure):
_fields_ = [("frame", ZcanFdFrame), ("timestamp", ctypes.c_uint64)]
class ZcanChannelErrorInfo(ctypes.Structure): class ZcanChannelErrorInfo(ctypes.Structure):
_fields_ = [ _fields_ = [
("error_code", ctypes.c_uint32), ("error_code", ctypes.c_uint32),
@@ -87,45 +110,85 @@ class Driver:
self.transmit_type = args.transmit_type self.transmit_type = args.transmit_type
self.dll = ctypes.WinDLL(args.dll) self.dll = ctypes.WinDLL(args.dll)
self._declare_functions() self._declare_functions()
self.device = self.dll.ZCAN_OpenDevice( self.device = self.dll.ZCAN_OpenDevice(args.device_type, args.device_index, 0)
args.device_type, args.device_index, 0
)
if not self.device: if not self.device:
raise RuntimeError( raise RuntimeError(
f"ZCAN_OpenDevice failed for device_type={args.device_type}, " "ZCAN_OpenDevice failed for device_type={}, device_index={}; "
f"device_index={args.device_index}; connect the adapter and close " "connect the adapter and close ZCANPRO first".format(
"ZCANPRO first" args.device_type, args.device_index
)
) )
self.channel = None self.channels = {}
try: try:
bitrate = str(args.bitrate).encode("ascii") for config in self._channel_configs(args):
channel_prefix = str(args.channel) self._initialize_channel(config)
path = (channel_prefix + "/canfd_abit_baud_rate").encode("ascii")
if self.dll.ZCAN_SetValue(self.device, path, bitrate) != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_SetValue(canfd_abit_baud_rate) failed")
data_path = (channel_prefix + "/canfd_dbit_baud_rate").encode("ascii")
if self.dll.ZCAN_SetValue(self.device, data_path, b"2000000") != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_SetValue(canfd_dbit_baud_rate) failed")
init = ZcanChannelInit()
# USBCANFD devices require a CANFD channel handle even when the
# application only transmits classic CAN frames through ZCAN_Transmit.
init.can_type = ZCAN_TYPE_CANFD
init.canfd.acc_code = 0
init.canfd.acc_mask = 0xFFFFFFFF
init.canfd.filter = 0
init.canfd.mode = 0
self.channel = self.dll.ZCAN_InitCAN(
self.device, args.channel, ctypes.byref(init)
)
if not self.channel:
raise RuntimeError("ZCAN_InitCAN failed")
if self.dll.ZCAN_StartCAN(self.channel) != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_StartCAN failed")
self.dll.ZCAN_ClearBuffer(self.channel)
except Exception: except Exception:
self.close() self.close()
raise raise
@staticmethod
def _channel_configs(args):
if args.channels_json:
configs = json.loads(args.channels_json)
else:
configs = [
{
"channel": args.channel,
"arbitration_bitrate": args.bitrate,
"data_bitrate": args.data_bitrate,
"can_fd_iso": True,
"termination_enabled": True,
}
]
if not isinstance(configs, list) or not configs:
raise ValueError("channels-json must be a non-empty JSON array")
channel_ids = [int(item["channel"]) for item in configs]
if len(channel_ids) != len(set(channel_ids)):
raise ValueError("channel numbers must be unique")
return configs
def _set_value(self, path, value):
result = self.dll.ZCAN_SetValue(
self.device, path.encode("ascii"), str(value).encode("ascii")
)
if result != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_SetValue({}) failed".format(path))
def _initialize_channel(self, config):
channel_id = int(config["channel"])
arbitration_bitrate = int(config.get("arbitration_bitrate", 500000))
data_bitrate = int(config.get("data_bitrate", 2000000))
can_fd_iso = bool(config.get("can_fd_iso", True))
if channel_id < 0:
raise ValueError("channel number cannot be negative")
if arbitration_bitrate <= 0 or data_bitrate <= 0:
raise ValueError("CAN bitrates must be positive")
prefix = str(channel_id)
self._set_value(prefix + "/protocol", 1)
self._set_value(prefix + "/canfd_standard", 0 if can_fd_iso else 1)
self._set_value(prefix + "/canfd_exp", 1)
self._set_value(prefix + "/canfd_abit_baud_rate", arbitration_bitrate)
self._set_value(prefix + "/canfd_dbit_baud_rate", data_bitrate)
init = ZcanChannelInit()
init.can_type = ZCAN_TYPE_CANFD
init.canfd.acc_code = 0
init.canfd.acc_mask = 0xFFFFFFFF
init.canfd.filter = 0
init.canfd.mode = 0
handle = self.dll.ZCAN_InitCAN(self.device, channel_id, ctypes.byref(init))
if not handle:
raise RuntimeError("ZCAN_InitCAN failed for channel {}".format(channel_id))
self.channels[channel_id] = handle
self._set_value(
prefix + "/initenal_resistance",
1 if bool(config.get("termination_enabled", True)) else 0,
)
if self.dll.ZCAN_StartCAN(handle) != ZCAN_STATUS_OK:
raise RuntimeError("ZCAN_StartCAN failed for channel {}".format(channel_id))
self.dll.ZCAN_ClearBuffer(handle)
def _declare_functions(self): def _declare_functions(self):
handle = ctypes.c_void_p handle = ctypes.c_void_p
self.dll.ZCAN_OpenDevice.argtypes = [ self.dll.ZCAN_OpenDevice.argtypes = [
@@ -168,10 +231,60 @@ class Driver:
ctypes.c_int32, ctypes.c_int32,
] ]
self.dll.ZCAN_Receive.restype = ctypes.c_uint32 self.dll.ZCAN_Receive.restype = ctypes.c_uint32
self.dll.ZCAN_TransmitFD.argtypes = [
handle,
ctypes.POINTER(ZcanTransmitFdData),
ctypes.c_uint32,
]
self.dll.ZCAN_TransmitFD.restype = ctypes.c_uint32
self.dll.ZCAN_ReceiveFD.argtypes = [
handle,
ctypes.POINTER(ZcanReceiveFdData),
ctypes.c_uint32,
ctypes.c_int32,
]
self.dll.ZCAN_ReceiveFD.restype = ctypes.c_uint32
def send(self, arbitration_id, data): def _handle(self, channel):
channel_id = int(channel)
try:
return self.channels[channel_id]
except KeyError:
raise ValueError("channel {} was not initialized".format(channel_id))
def _transmit_error(self, handle, operation):
error = ZcanChannelErrorInfo()
read_ok = self.dll.ZCAN_ReadChannelErrInfo(handle, ctypes.byref(error))
detail = (
"; channel_error=0x{:08X}".format(error.error_code)
if read_ok == ZCAN_STATUS_OK
else "; channel error info unavailable"
)
raise RuntimeError(operation + " did not accept the frame" + detail)
def send(self, channel, arbitration_id, data, is_fd=False, bitrate_switch=False):
if not 0 <= arbitration_id <= 0x7FF: if not 0 <= arbitration_id <= 0x7FF:
raise ValueError("only 11-bit standard CAN identifiers are allowed") raise ValueError("only 11-bit standard CAN identifiers are allowed")
handle = self._handle(channel)
if is_fd:
valid_lengths = set(range(9)) | {12, 16, 20, 24, 32, 48, 64}
if len(data) not in valid_lengths:
raise ValueError(
"CAN FD payload length must be 0-8, 12, 16, 20, 24, 32, 48 or 64 bytes"
)
transmit = ZcanTransmitFdData()
transmit.frame.can_id = arbitration_id
transmit.frame.len = len(data)
transmit.frame.flags = CANFD_BRS if bitrate_switch else 0
for index, byte in enumerate(data):
transmit.frame.data[index] = byte
transmit.transmit_type = self.transmit_type
if self.dll.ZCAN_TransmitFD(handle, ctypes.byref(transmit), 1) != 1:
self._transmit_error(handle, "ZCAN_TransmitFD")
return
if bitrate_switch:
raise ValueError("bitrate switching is only valid for CAN FD frames")
if len(data) > 8: if len(data) > 8:
raise ValueError("classic CAN payload exceeds 8 bytes") raise ValueError("classic CAN payload exceeds 8 bytes")
transmit = ZcanTransmitData() transmit = ZcanTransmitData()
@@ -180,38 +293,63 @@ class Driver:
for index, byte in enumerate(data): for index, byte in enumerate(data):
transmit.frame.data[index] = byte transmit.frame.data[index] = byte
transmit.transmit_type = self.transmit_type transmit.transmit_type = self.transmit_type
if self.dll.ZCAN_Transmit(self.channel, ctypes.byref(transmit), 1) != 1: if self.dll.ZCAN_Transmit(handle, ctypes.byref(transmit), 1) != 1:
error = ZcanChannelErrorInfo() self._transmit_error(handle, "ZCAN_Transmit")
read_ok = self.dll.ZCAN_ReadChannelErrInfo(
self.channel, ctypes.byref(error)
)
detail = (
f"; channel_error=0x{error.error_code:08X}"
if read_ok == ZCAN_STATUS_OK
else "; channel error info unavailable"
)
raise RuntimeError("ZCAN_Transmit did not accept the frame" + detail)
def receive(self, timeout_ms): @staticmethod
receive = ZcanReceiveData() def _classic_public(receive):
count = self.dll.ZCAN_Receive(
self.channel, ctypes.byref(receive), 1, int(timeout_ms)
)
if count == 0:
return None
if count != 1:
raise RuntimeError("ZCAN_Receive returned an unexpected frame count")
frame = receive.frame frame = receive.frame
return { return {
"arbitration_id": int(frame.can_id & 0x1FFFFFFF), "arbitration_id": int(frame.can_id & 0x1FFFFFFF),
"data_hex": bytes(frame.data[: frame.can_dlc]).hex(), "data_hex": bytes(frame.data[: frame.can_dlc]).hex(),
"timestamp_us": int(receive.timestamp), "timestamp_us": int(receive.timestamp),
"is_fd": False,
"bitrate_switch": False,
} }
@staticmethod
def _fd_public(receive):
frame = receive.frame
return {
"arbitration_id": int(frame.can_id & 0x1FFFFFFF),
"data_hex": bytes(frame.data[: frame.len]).hex(),
"timestamp_us": int(receive.timestamp),
"is_fd": True,
"bitrate_switch": bool(frame.flags & CANFD_BRS),
}
def receive(self, channel, timeout_ms, include_fd=False):
handle = self._handle(channel)
deadline = time.monotonic() + max(0, int(timeout_ms)) / 1000.0
while True:
classic = ZcanReceiveData()
count = self.dll.ZCAN_Receive(handle, ctypes.byref(classic), 1, 0)
if count == 1:
return self._classic_public(classic)
if count != 0:
raise RuntimeError("ZCAN_Receive returned an unexpected frame count")
if include_fd:
fd_receive = ZcanReceiveFdData()
count = self.dll.ZCAN_ReceiveFD(
handle, ctypes.byref(fd_receive), 1, 0
)
if count == 1:
return self._fd_public(fd_receive)
if count != 0:
raise RuntimeError(
"ZCAN_ReceiveFD returned an unexpected frame count"
)
remaining = deadline - time.monotonic()
if remaining <= 0:
return None
time.sleep(min(0.002, remaining))
def close(self): def close(self):
if self.channel: for channel_id in sorted(self.channels, reverse=True):
self.dll.ZCAN_ResetCAN(self.channel) handle = self.channels.pop(channel_id)
self.channel = None self.dll.ZCAN_ResetCAN(handle)
if self.device: if self.device:
self.dll.ZCAN_CloseDevice(self.device) self.dll.ZCAN_CloseDevice(self.device)
self.device = None self.device = None
@@ -222,8 +360,10 @@ def parse_args():
parser.add_argument("--dll", required=True) parser.add_argument("--dll", required=True)
parser.add_argument("--device-type", type=int, required=True) parser.add_argument("--device-type", type=int, required=True)
parser.add_argument("--device-index", type=int, default=0) parser.add_argument("--device-index", type=int, default=0)
parser.add_argument("--channels-json")
parser.add_argument("--channel", type=int, default=0) parser.add_argument("--channel", type=int, default=0)
parser.add_argument("--bitrate", type=int, default=500000) parser.add_argument("--bitrate", type=int, default=500000)
parser.add_argument("--data-bitrate", type=int, default=2000000)
parser.add_argument( parser.add_argument(
"--transmit-type", "--transmit-type",
type=int, type=int,
@@ -247,6 +387,8 @@ def main():
"ZCAN_StartCAN", "ZCAN_StartCAN",
"ZCAN_Transmit", "ZCAN_Transmit",
"ZCAN_Receive", "ZCAN_Receive",
"ZCAN_TransmitFD",
"ZCAN_ReceiveFD",
) )
missing = [name for name in required if not hasattr(dll, name)] missing = [name for name in required if not hasattr(dll, name)]
emit( emit(
@@ -256,9 +398,12 @@ def main():
"missing_exports": missing, "missing_exports": missing,
"structure_sizes": { "structure_sizes": {
"init": ctypes.sizeof(ZcanChannelInit), "init": ctypes.sizeof(ZcanChannelInit),
"frame": ctypes.sizeof(ZcanFrame), "classic_frame": ctypes.sizeof(ZcanFrame),
"tx": ctypes.sizeof(ZcanTransmitData), "classic_tx": ctypes.sizeof(ZcanTransmitData),
"rx": ctypes.sizeof(ZcanReceiveData), "classic_rx": ctypes.sizeof(ZcanReceiveData),
"fd_frame": ctypes.sizeof(ZcanFdFrame),
"fd_tx": ctypes.sizeof(ZcanTransmitFdData),
"fd_rx": ctypes.sizeof(ZcanReceiveFdData),
"error": ctypes.sizeof(ZcanChannelErrorInfo), "error": ctypes.sizeof(ZcanChannelErrorInfo),
}, },
} }
@@ -268,27 +413,43 @@ def main():
driver = None driver = None
try: try:
driver = Driver(args) driver = Driver(args)
emit({"ok": True, "event": "ready"}) emit({"ok": True, "event": "ready", "channels": sorted(driver.channels)})
for line in sys.stdin: for line in sys.stdin:
try: try:
request = json.loads(line) request = json.loads(line)
operation = request.get("operation") operation = request.get("operation")
if operation == "send": if operation == "send":
channel = int(request.get("channel", args.channel))
arbitration_id = int(request["arbitration_id"]) arbitration_id = int(request["arbitration_id"])
data = bytes.fromhex(request["data_hex"]) data = bytes.fromhex(request["data_hex"])
driver.send(arbitration_id, data) is_fd = bool(request.get("is_fd", False))
bitrate_switch = bool(request.get("bitrate_switch", False))
driver.send(
channel,
arbitration_id,
data,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
emit( emit(
{ {
"ok": True, "ok": True,
"channel": channel,
"arbitration_id": arbitration_id, "arbitration_id": arbitration_id,
"data_hex": data.hex(), "data_hex": data.hex(),
"is_fd": is_fd,
"bitrate_switch": bitrate_switch,
} }
) )
elif operation == "receive": elif operation == "receive":
emit( emit(
{ {
"ok": True, "ok": True,
"frame": driver.receive(int(request.get("timeout_ms", 0))), "frame": driver.receive(
int(request.get("channel", args.channel)),
int(request.get("timeout_ms", 0)),
include_fd=bool(request.get("include_fd", False)),
),
} }
) )
elif operation == "close": elif operation == "close":
@@ -12,9 +12,11 @@ from typing import Any, Callable
from cognitive_agent.infrastructure.can.gateway import CanBus from cognitive_agent.infrastructure.can.gateway import CanBus
FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-current-diagnostics-v6" FIRMWARE_PROTOCOL_SIGNATURE = "seat-controller-smv8-dynamic-modes-v13"
FIRMWARE_PROTOCOL_FRAME_IDS = frozenset({0x301, 0x302, 0x303, 0x304, 0x305, 0x4FA}) FIRMWARE_PROTOCOL_FRAME_IDS = frozenset(
EXPECTED_FIRMWARE_REVISION = 6 {0x301, 0x302, 0x303, 0x304, 0x305, 0x306, 0x4FA}
)
EXPECTED_FIRMWARE_REVISION = 13
EXPECTED_MOTOR_MAX_HALL = (480, 374, 917, 647) EXPECTED_MOTOR_MAX_HALL = (480, 374, 917, 647)
@@ -168,9 +170,79 @@ class T2GFirmwareGuard:
self.auto_flash = auto_flash self.auto_flash = auto_flash
self.probe_timeout_ms = probe_timeout_ms self.probe_timeout_ms = probe_timeout_ms
self.boot_wait_ms = boot_wait_ms self.boot_wait_ms = boot_wait_ms
self._last_probe_diagnostics: dict[str, Any] = {}
def _record_probe_diagnostics(
self,
*,
observed_ids: set[int],
motor_status_ids: set[int],
motor_diagnostic: dict[int, int],
motor_current_ids: set[int],
firmware_revision: int | None,
) -> None:
expected_motors = {0, 1, 2, 3}
max_hall_counts = [motor_diagnostic.get(motor) for motor in range(4)]
mismatched_max_hall = [
motor
for motor, expected in enumerate(EXPECTED_MOTOR_MAX_HALL)
if motor_diagnostic.get(motor) is not None
and abs(int(motor_diagnostic[motor]) - expected)
> max(4, round(expected * 0.05))
]
self._last_probe_diagnostics = {
"observed_frame_ids": [
f"0x{item:03X}" for item in sorted(observed_ids)
],
"missing_frame_ids": [
f"0x{item:03X}"
for item in sorted(FIRMWARE_PROTOCOL_FRAME_IDS - observed_ids)
],
"firmware_revision": firmware_revision,
"expected_firmware_revision": EXPECTED_FIRMWARE_REVISION,
"observed_motor_status_ids": sorted(motor_status_ids),
"missing_motor_status_ids": sorted(expected_motors - motor_status_ids),
"observed_motor_diagnostic_ids": sorted(motor_diagnostic),
"missing_motor_diagnostic_ids": sorted(
expected_motors - set(motor_diagnostic)
),
"observed_motor_current_ids": sorted(motor_current_ids),
"missing_motor_current_ids": sorted(expected_motors - motor_current_ids),
"max_hall_counts": max_hall_counts,
"expected_max_hall_counts": list(EXPECTED_MOTOR_MAX_HALL),
"mismatched_max_hall_ids": mismatched_max_hall,
}
def _probe_failure_detail(self) -> str:
diagnostics = self._last_probe_diagnostics
if not diagnostics:
return "no qualifying Seat_Controller_demo protocol frames were observed"
def joined(name: str) -> str:
values = diagnostics.get(name) or []
return ",".join(str(item) for item in values) or "none"
revision = diagnostics.get("firmware_revision")
revision_text = "unknown" if revision is None else str(revision)
max_hall = diagnostics.get("max_hall_counts") or []
max_hall_text = ",".join(
"?" if value is None else str(value) for value in max_hall
)
return (
f"observed_frame_ids={joined('observed_frame_ids')}; "
f"missing_frame_ids={joined('missing_frame_ids')}; "
f"firmware_revision={revision_text} "
f"(expected {EXPECTED_FIRMWARE_REVISION}); "
f"missing_motor_status_ids={joined('missing_motor_status_ids')}; "
"missing_motor_diagnostic_ids="
f"{joined('missing_motor_diagnostic_ids')}; "
f"missing_motor_current_ids={joined('missing_motor_current_ids')}; "
f"max_hall_counts={max_hall_text}; "
f"mismatched_max_hall_ids={joined('mismatched_max_hall_ids')}"
)
def _probe(self, bus: CanBus) -> dict[str, Any] | None: def _probe(self, bus: CanBus) -> dict[str, Any] | None:
"""Observe the cyclic 0x301..0x305/0x4FA contract without transmitting. """Observe the cyclic 0x301..0x306/0x4FA contract without transmitting.
The 2026-07-30 firmware uses 0x304 and 0x305 for current diagnostics, The 2026-07-30 firmware uses 0x304 and 0x305 for current diagnostics,
so the previous request/response identity handshake would collide with so the previous request/response identity handshake would collide with
@@ -185,12 +257,27 @@ class T2GFirmwareGuard:
motor_diagnostic: dict[int, int] = {} motor_diagnostic: dict[int, int] = {}
motor_current_ids: set[int] = set() motor_current_ids: set[int] = set()
firmware_revision: int | None = None firmware_revision: int | None = None
self._last_probe_diagnostics = {}
while True: while True:
remaining_ms = round((deadline - time.monotonic()) * 1000) remaining_ms = round((deadline - time.monotonic()) * 1000)
if remaining_ms <= 0: if remaining_ms <= 0:
self._record_probe_diagnostics(
observed_ids=observed_ids,
motor_status_ids=motor_status_ids,
motor_diagnostic=motor_diagnostic,
motor_current_ids=motor_current_ids,
firmware_revision=firmware_revision,
)
return None return None
frame = bus.receive(remaining_ms) frame = bus.receive(remaining_ms)
if frame is None: if frame is None:
self._record_probe_diagnostics(
observed_ids=observed_ids,
motor_status_ids=motor_status_ids,
motor_diagnostic=motor_diagnostic,
motor_current_ids=motor_current_ids,
firmware_revision=firmware_revision,
)
return None return None
arbitration_id = int(frame.arbitration_id) arbitration_id = int(frame.arbitration_id)
if arbitration_id not in FIRMWARE_PROTOCOL_FRAME_IDS or len(frame.data) < 8: if arbitration_id not in FIRMWARE_PROTOCOL_FRAME_IDS or len(frame.data) < 8:
@@ -206,6 +293,14 @@ class T2GFirmwareGuard:
elif arbitration_id == 0x304 and data[0] < 4: elif arbitration_id == 0x304 and data[0] < 4:
motor_current_ids.add(data[0]) motor_current_ids.add(data[0])
self._record_probe_diagnostics(
observed_ids=observed_ids,
motor_status_ids=motor_status_ids,
motor_diagnostic=motor_diagnostic,
motor_current_ids=motor_current_ids,
firmware_revision=firmware_revision,
)
if ( if (
observed_ids == FIRMWARE_PROTOCOL_FRAME_IDS observed_ids == FIRMWARE_PROTOCOL_FRAME_IDS
and firmware_revision == EXPECTED_FIRMWARE_REVISION and firmware_revision == EXPECTED_FIRMWARE_REVISION
@@ -254,6 +349,7 @@ class T2GFirmwareGuard:
) from probe_error ) from probe_error
raise FirmwareProgrammingError( raise FirmwareProgrammingError(
"matching Seat_Controller_demo passive protocol signature was not detected; " "matching Seat_Controller_demo passive protocol signature was not detected; "
f"{self._probe_failure_detail()}; "
"automatic programming is disabled" "automatic programming is disabled"
) )
if self.programmer is None: if self.programmer is None:
@@ -268,7 +364,8 @@ class T2GFirmwareGuard:
verified = self._probe(verified_bus) verified = self._probe(verified_bus)
if verified is None: if verified is None:
raise FirmwareProgrammingError( raise FirmwareProgrammingError(
"programming completed but passive firmware protocol verification failed" "programming completed but passive firmware protocol verification failed; "
+ self._probe_failure_detail()
) )
except Exception as exc: except Exception as exc:
verified_bus.close() verified_bus.close()
+2 -2
View File
@@ -111,7 +111,7 @@ class SeatWebApiTests(unittest.TestCase):
def test_static_console_and_runtime_endpoint(self) -> None: def test_static_console_and_runtime_endpoint(self) -> None:
with self.opener.open(f"{self.base_url}/", timeout=3) as response: with self.opener.open(f"{self.base_url}/", timeout=3) as response:
html = response.read().decode("utf-8") html = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-9", timeout=3) as response: with self.opener.open(f"{self.base_url}/app.js?v=seat-hil-13", timeout=3) as response:
app_script = response.read().decode("utf-8") app_script = response.read().decode("utf-8")
with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response: with self.opener.open(f"{self.base_url}/api/runtime", timeout=3) as response:
runtime = json.loads(response.read().decode("utf-8")) runtime = json.loads(response.read().decode("utf-8"))
@@ -121,7 +121,7 @@ class SeatWebApiTests(unittest.TestCase):
self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html) self.assertIn('data-vehicle-model="byd-tang-2024-reference"', html)
self.assertIn("整车孪生界面", html) self.assertIn("整车孪生界面", html)
self.assertIn("透明车体 · 实体设备", html) self.assertIn("透明车体 · 实体设备", html)
self.assertIn('src="/app.js?v=seat-hil-9"', html) self.assertIn('src="/app.js?v=seat-hil-13"', html)
self.assertIn('id="hardwareAccessField"', html) self.assertIn('id="hardwareAccessField"', html)
self.assertIn("hardware_access_required", app_script) self.assertIn("hardware_access_required", app_script)
self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script) self.assertIn("HARDWARE_REQUEST_TIMEOUT_MS = 15 * 60 * 1000", app_script)
@@ -538,7 +538,7 @@ class MultiDomainServiceTests(unittest.TestCase):
vehicle_id="denza_n7", vehicle_id="denza_n7",
) )
self.assertEqual(result["status"], "execution_failed") self.assertEqual(result["status"], "partially_succeeded")
self.assertEqual(result["execution_report"]["spec"]["status"], "failed") self.assertEqual(result["execution_report"]["spec"]["status"], "failed")
self.assertEqual( self.assertEqual(
result["actuator_state"]["passenger"]["position"]["longitudinal"][ result["actuator_state"]["passenger"]["position"]["longitudinal"][
@@ -553,6 +553,10 @@ class MultiDomainServiceTests(unittest.TestCase):
"physical_can", "physical_can",
) )
self.assertEqual( self.assertEqual(
result["domain_state"]["roof"]["transmittance_pct"],
20,
)
self.assertNotEqual(
result["domain_state"]["roof"]["transmittance_pct"], result["domain_state"]["roof"]["transmittance_pct"],
initial_roof, initial_roof,
) )
+100
View File
@@ -69,6 +69,8 @@ class ModeProfileRegistryTests(unittest.TestCase):
"electric_beach_chair", "electric_beach_chair",
"queen_passenger_driving", "queen_passenger_driving",
"calf_decompression_float", "calf_decompression_float",
"rocking_horse",
"dynamic_cradle",
} }
denza_modes = registry.public_modes(vehicle_id="denza_n7") denza_modes = registry.public_modes(vehicle_id="denza_n7")
denza_ids = {mode["id"] for mode in denza_modes} denza_ids = {mode["id"] for mode in denza_modes}
@@ -88,6 +90,104 @@ class ModeProfileRegistryTests(unittest.TestCase):
"calf_decompression_float", "calf_decompression_float",
) )
def test_dynamic_modes_preserve_exact_timelines_and_native_mode_ids(self) -> None:
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
profiles = {
profile.id: profile
for profile in registry.profiles()
if profile.id in {"rocking_horse", "dynamic_cradle"}
}
horse = profiles["rocking_horse"]
self.assertIsNotNone(horse.timeline)
assert horse.timeline is not None
self.assertEqual(horse.timeline.repeat, "until_stopped_or_timeout")
self.assertEqual(horse.timeline.max_runtime_ms, 600_000)
self.assertIsNone(horse.timeline.cycle_duration_ms)
self.assertEqual(
[(phase.id, phase.duration_ms) for phase in horse.timeline.phases],
[("rock_forward", None), ("rock_backward", None)],
)
self.assertEqual(
horse.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
320,
)
self.assertEqual(
[phase.completion for phase in horse.timeline.phases],
["all_targets_reached", "all_targets_reached"],
)
self.assertEqual([phase.timeout_ms for phase in horse.timeline.phases], [15_000, 15_000])
self.assertEqual(horse.timeline.phases[0].motion["final_approach_hall_pulses"], 8)
self.assertTrue(horse.timeline.phases[0].motion["continuous_four_axis_cruise"])
self.assertEqual(
horse.timeline.phases[0].targets["cushion_tilt"]["hall_pulse"],
250,
)
self.assertEqual(
horse.timeline.phases[1].targets["cushion_tilt"]["hall_pulse"],
130,
)
self.assertEqual(
horse.timeline.phases[0].motion["cruise_duty_percent"]["cushion_tilt"],
90,
)
self.assertEqual(
horse.timeline.phases[0].targets["longitudinal"]["hall_pulse"],
270,
)
self.assertEqual(
horse.timeline.phases[1].targets["longitudinal"]["hall_pulse"],
370,
)
self.assertEqual(
horse.timeline.phases[0].motion["cruise_duty_percent"]["longitudinal"],
90,
)
cradle = profiles["dynamic_cradle"]
self.assertIsNotNone(cradle.timeline)
assert cradle.timeline is not None
self.assertEqual(cradle.timeline.repeat, "until_stopped_or_timeout")
self.assertEqual(cradle.timeline.max_runtime_ms, 600_000)
self.assertIsNone(cradle.timeline.cycle_duration_ms)
self.assertEqual(
[(phase.id, phase.duration_ms) for phase in cradle.timeline.phases],
[("forward", None), ("return", None)],
)
self.assertEqual(
cradle.timeline.entry["targets"]["longitudinal"]["hall_pulse"],
391,
)
self.assertEqual(
[phase.completion for phase in cradle.timeline.phases],
["all_targets_reached", "all_targets_reached"],
)
self.assertEqual(
[phase.timeout_ms for phase in cradle.timeline.phases],
[30_000, 30_000],
)
self.assertEqual(
cradle.timeline.phases[0].motion["final_approach_hall_pulses"],
12,
)
self.assertTrue(
cradle.timeline.phases[0].motion["continuous_four_axis_cruise"]
)
self.assertNotIn("clearance_lead", cradle.timeline.phases[0].motion)
requests = {
profile.id: registry.requests_for_profiles([profile])[0]
for profile in (horse, cradle)
}
self.assertEqual(
requests["rocking_horse"]["parameters"],
[{"name": "mode_id", "value": 1}],
)
self.assertEqual(
requests["dynamic_cradle"]["parameters"],
[{"name": "mode_id", "value": 2}],
)
def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None: def test_denza_mode_positions_use_corrected_n7_zero_points(self) -> None:
registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes") registry = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes")
expected_positions = { expected_positions = {
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
import unittest
from pathlib import Path
from unittest.mock import patch
from cognitive_agent.application.native_can_runtime import (
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
from cognitive_agent.infrastructure.can.gateway import CanFrame
from cognitive_agent.infrastructure.can.hardware import VehicleCanSettings
class _FakeBus:
def __init__(self, received: list[CanFrame] | None = None) -> None:
self.received = list(received or [])
self.sent: list[CanFrame] = []
self.closed = False
def send(
self,
arbitration_id: int,
data: bytes,
*,
is_fd: bool = False,
bitrate_switch: bool = False,
) -> CanFrame:
frame = CanFrame(
arbitration_id,
data,
channel=1,
is_fd=is_fd,
bitrate_switch=bitrate_switch,
)
self.sent.append(frame)
return frame
def receive(
self, timeout_ms: int, *, include_fd: bool = False
) -> CanFrame | None:
return self.received.pop(0) if self.received else None
def close(self) -> None:
self.closed = True
class NativeCanIntegrationTests(unittest.TestCase):
def setUp(self) -> None:
self.runtime = NativeCanRuntime(REPOSITORY_ROOT)
def _compile(self, domain: str, actions: list[dict]) -> tuple[dict, dict]:
config = self.runtime.build_function_config(
domain, actions, "req-native-test", "local_rules"
)
return config, self.runtime.compile(config)
def test_climate_frames_match_can_dev_layout_and_repeat_policy(self) -> None:
actions = [
{
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
},
{
"domain": "climate",
"capability": "climate.set_temperature",
"target": "driver",
"operation": "set",
"parameters": {"temperature_c": 24},
},
{
"domain": "climate",
"capability": "climate.set_fan_level",
"target": "vehicle",
"operation": "set",
"parameters": {"level": 3},
},
]
_, plan = self._compile("climate", actions)
commands = plan["spec"]["commands"]
self.assertEqual([item["signals"]["frame_id"] for item in commands], ["0x1DE"] * 3)
self.assertEqual(commands[0]["signals"]["data_hex"], "00 00 00 10 00 00 00 00")
self.assertEqual(commands[1]["signals"]["data_hex"], "00 00 00 00 18 00 00 00")
self.assertEqual(commands[2]["signals"]["data_hex"], "30 00 00 00 00 00 00 00")
self.assertEqual(commands[0]["repeat"], {"count": 3, "period_ms": 100})
self.assertFalse(commands[0]["signals"]["is_fd"])
def test_window_motion_uses_can_fd_with_brs(self) -> None:
_, plan = self._compile(
"window",
[
{
"domain": "window",
"capability": "window.set_motion",
"target": "front_left",
"operation": "set",
"parameters": {"motion": "open"},
}
],
)
command = plan["spec"]["commands"][0]
self.assertEqual(command["signals"]["frame_id"], "0x431")
self.assertEqual(command["signals"]["data_hex"], "00 00 00 00 00 00 01 00")
self.assertTrue(command["signals"]["is_fd"])
self.assertTrue(command["signals"]["bitrate_switch"])
def test_executor_requires_configured_feedback_for_vehicle_success(self) -> None:
config, plan = self._compile(
"climate",
[
{
"domain": "climate",
"capability": "climate.set_power",
"target": "vehicle",
"operation": "set",
"parameters": {"state": "on"},
}
],
)
feedback = CanFrame(0x404, bytes((0, 1, 0, 0, 0, 0, 0, 0)), channel=1)
bus = _FakeBus([feedback])
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(lambda: bus, feedback_timeout_ms=1).execute(
self.runtime, plan, config
)
self.assertEqual(report["spec"]["status"], "succeeded")
self.assertEqual(len(bus.sent), 3)
self.assertTrue(bus.closed)
no_feedback_bus = _FakeBus()
with patch("cognitive_agent.application.native_can_runtime.time.sleep"):
report = NativeCanExecutor(
lambda: no_feedback_bus, feedback_timeout_ms=1
).execute(self.runtime, plan, config)
self.assertEqual(report["spec"]["status"], "failed")
def test_one_sentence_builds_seat_and_vehicle_channel_plans(self) -> None:
service = SeatDemoService(REPOSITORY_ROOT)
result = service.process(
"副驾座椅通风3档,同时打开空调,温度设为24度,风量3档",
provider="local",
)
self.assertEqual(result["status"], "succeeded")
self.assertEqual(set(result["function_configs"]), {"seat", "native_climate"})
self.assertEqual(set(result["command_plans"]), {"seat", "native_climate"})
body_commands = result["command_plans"]["native_climate"]["spec"]["commands"]
self.assertTrue(body_commands)
self.assertTrue(all(item["bus"] == "body_can" for item in body_commands))
self.assertFalse(result["deferred_requests"])
def test_vehicle_timing_contract_matches_requested_60mhz_values(self) -> None:
settings = VehicleCanSettings.from_env(Path(REPOSITORY_ROOT))
self.assertTrue(settings.timing_is_verified)
info = settings.public_info()
self.assertEqual(info["arbitration"]["bitrate"], 500_000)
self.assertEqual(info["arbitration"]["sample_point_percent"], 80)
self.assertEqual(info["data"]["bitrate"], 2_000_000)
self.assertEqual(info["data"]["sample_point_percent"], 80)
if __name__ == "__main__":
unittest.main()