feat: update CAN integration and T2G seat firmware
This commit is contained in:
@@ -4,4 +4,10 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
artifacts/runs/
|
||||
artifacts/server/
|
||||
artifacts/tmp/
|
||||
tmp/*.log
|
||||
.codex_tmp/
|
||||
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/
|
||||
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/
|
||||
firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem
|
||||
|
||||
@@ -79,7 +79,7 @@ artifacts/runs/<request_id>/
|
||||
|
||||
当前已接入四个相互隔离的可执行领域:
|
||||
|
||||
- 座椅:基于 `座椅部分功能CAN矩阵表.xlsx` 的主/副驾通风和加热,以及明确标识为 synthetic 的五轴位置电机演示。
|
||||
- 座椅:所有热管理、位置移动和姿态预设能力仅面向副驾;未指定座位时默认副驾,显式主驾请求拒绝执行。
|
||||
- 空调:基于 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的电源、18–32℃ 分区温度、0–7 档风量、A/C、内外循环、风向和前除霜。
|
||||
- 天幕:基于同一矩阵的 0–100% 透光率、后遮阳帘绝对位置和自动调光。
|
||||
- 氛围灯:基于同一矩阵的总开关、24-bit RGB、0–100% 亮度和 ECU 场景模式。
|
||||
@@ -92,7 +92,7 @@ make test
|
||||
make dev
|
||||
```
|
||||
|
||||
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器仅是未配置 GPT 或 `auto` 模式调用失败时的显式降级路径。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
|
||||
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器用于未配置 GPT,或 GPT/`auto` 模式因 API Key、模型权限、网络或结构化响应异常而失败时的安全降级;结果会明确标记 `local_rules_fallback`。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
|
||||
|
||||
### 一句话、快捷模式与手势控制
|
||||
|
||||
@@ -134,4 +134,4 @@ PYTHONPATH=src python -m cognitive_agent.auth generate-secret
|
||||
|
||||
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
|
||||
|
||||
已验证的 T2G 加热、通风和四电机能力已合并到仓库内 `firmware/t2g/Seat_Controller_demo`,并提供一条默认锁定的真实 HIL 路径。主驾与副驾目前映射到同一物理座椅;硬件请求会先做固件身份握手,可在明确启用后通过 IAR C-SPY 自动烧录,再发送 CAN 并等待真实回读。固件拓扑、IAR 构建、ZLG 32 位 bridge、CAN 接线、安全门禁和上电步骤见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.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)。
|
||||
|
||||
+113
-33
@@ -20,6 +20,7 @@ const state = {
|
||||
progressTimer: null,
|
||||
seatTwin: null,
|
||||
seatActuatorState: {},
|
||||
seatFeedbackSource: "simulated",
|
||||
availableSeatAxes: null,
|
||||
};
|
||||
|
||||
@@ -41,7 +42,7 @@ const TWIN_DEFAULT_POSITIONS = {
|
||||
};
|
||||
const TWIN_ANGLE_RANGES = {
|
||||
backrest_recline: [95, 150],
|
||||
legrest_height: [95, 163],
|
||||
legrest_height: [95, 167],
|
||||
};
|
||||
|
||||
function initJointTwinLayout() {
|
||||
@@ -104,13 +105,13 @@ function twinPostureAngles(device) {
|
||||
device?.position?.[axis]?.angle_deg
|
||||
?? twinAngleForPosition(axis, position(axis, fallback)),
|
||||
);
|
||||
const cushion = 2 + position("cushion_tilt", 50) * 0.09;
|
||||
const cushion = position("cushion_tilt", 50) * 0.09;
|
||||
return {
|
||||
cushion_tilt: cushion,
|
||||
backrest_recline: included("backrest_recline", 20) - 90 + cushion,
|
||||
legrest_height: Math.abs(
|
||||
180 - cushion - included("legrest_height", 0),
|
||||
),
|
||||
backrest_recline: included("backrest_recline", 20)
|
||||
- TWIN_ANGLE_RANGES.backrest_recline[0],
|
||||
legrest_height: included("legrest_height", 0)
|
||||
- TWIN_ANGLE_RANGES.legrest_height[0],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1492,15 +1493,15 @@ function renderTwinCalloutValues(device) {
|
||||
Number.isInteger(number) ? String(number) : number.toFixed(1)
|
||||
);
|
||||
const values = {
|
||||
longitudinal: [`${longitudinal}%`, longitudinal < 45 ? "后移区" : longitudinal > 55 ? "前移区" : "中位区"],
|
||||
longitudinal: [`${longitudinal}%`, longitudinal < 45 ? "前移区" : longitudinal > 55 ? "后移区" : "中位区"],
|
||||
cushion_tilt: [`${formatAngle(postureAngles.cushion_tilt)}°`, `${cushion}%`],
|
||||
backrest_recline: [
|
||||
`${backrestEstimated ? "≈" : ""}${formatAngle(postureAngles.backrest_recline)}°`,
|
||||
`${backrestEstimated ? "估算 " : ""}${backrest}% · 相对竖直`,
|
||||
`${backrestEstimated ? "估算 " : ""}${backrest}% · 相对归零位`,
|
||||
],
|
||||
legrest_height: [
|
||||
`${legrestEstimated ? "≈" : ""}${formatAngle(postureAngles.legrest_height)}°`,
|
||||
`${legrestEstimated ? "估算 " : ""}${legrest}% · 相对地板`,
|
||||
`${legrestEstimated ? "估算 " : ""}${legrest}% · 相对垂直零位`,
|
||||
],
|
||||
legrest_extension: [`${extension}%`, "伸缩行程"],
|
||||
backrest_included_angle: [
|
||||
@@ -1522,10 +1523,10 @@ function renderTwinCalloutValues(device) {
|
||||
function renderTwinReadout() {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
const seat = twin.selected;
|
||||
const device = twin.actuatorState[seat];
|
||||
$("#twinSeatName").textContent = seat === "driver" ? "主驾" : "副驾";
|
||||
$("#twinSeatZone").textContent = seat === "driver" ? "front_left" : "front_right";
|
||||
const seat = "passenger";
|
||||
const device = twin.actuatorState.passenger;
|
||||
$("#twinSeatName").textContent = "副驾";
|
||||
$("#twinSeatZone").textContent = "front_right";
|
||||
const postureAngles = twinPostureAngles(device);
|
||||
Object.entries(TWIN_DEFAULT_POSITIONS).forEach(([axis, fallback]) => {
|
||||
const motion = device?.position?.[axis];
|
||||
@@ -1554,7 +1555,7 @@ function renderTwinReadout() {
|
||||
function updateTwinExplicitAngles(result) {
|
||||
const twin = state.seatTwin;
|
||||
if (!twin) return;
|
||||
const targets = { driver: {}, passenger: {} };
|
||||
const targets = { passenger: {} };
|
||||
const requestedSeats = new Set();
|
||||
const capabilityAxes = {
|
||||
"seat.set_backrest_recline": "backrest_recline",
|
||||
@@ -2245,7 +2246,7 @@ function initSeatTwin() {
|
||||
vehicleSystems,
|
||||
selected: "passenger",
|
||||
actuatorState: { passenger: initialDevice() },
|
||||
explicitAngleTargets: { driver: {}, passenger: {} },
|
||||
explicitAngleTargets: { passenger: {} },
|
||||
view: { yaw: -0.62, pitch: 0.31, distance: 10.2, targetX: 0 },
|
||||
dragging: false,
|
||||
pointer: { x: 0, y: 0 },
|
||||
@@ -2429,7 +2430,7 @@ function initSeatTwin() {
|
||||
model.angles[axis] += Math.abs(delta) < 0.03 ? delta : delta * 0.085;
|
||||
});
|
||||
model.slide.position.z = (model.positions.longitudinal - 50) * 0.012;
|
||||
const cushionAngle = 2 + model.positions.cushion_tilt * 0.09;
|
||||
const cushionAngle = model.positions.cushion_tilt * 0.09;
|
||||
model.cushionPivot.rotation.x = degrees(-cushionAngle);
|
||||
model.backrestPivot.rotation.x = (
|
||||
model.cushionPivot.rotation.x
|
||||
@@ -2503,6 +2504,26 @@ function selectProvider(provider) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateSeatFeedbackModeLabel() {
|
||||
const label = $("#seatFeedbackModeLabel");
|
||||
if (!label) return;
|
||||
if (state.seatFeedbackSource === "physical_can") {
|
||||
label.textContent = "PHYSICAL CAN · BENCH SYNCHRONIZED";
|
||||
return;
|
||||
}
|
||||
if (state.seatFeedbackSource === "physical_partial") {
|
||||
label.textContent = "PHYSICAL CAN · PARTIAL FEEDBACK";
|
||||
return;
|
||||
}
|
||||
if (state.seatFeedbackSource === "unavailable") {
|
||||
label.textContent = "PHYSICAL CAN · SYNC UNAVAILABLE";
|
||||
return;
|
||||
}
|
||||
label.textContent = state.executionMode === "hardware"
|
||||
? "PHYSICAL CAN FEEDBACK"
|
||||
: "SIMULATED FEEDBACK";
|
||||
}
|
||||
|
||||
function selectExecutionMode(mode) {
|
||||
if (mode === "hardware" && !state.runtime?.hardware?.configured) return;
|
||||
state.executionMode = mode;
|
||||
@@ -2510,7 +2531,8 @@ function selectExecutionMode(mode) {
|
||||
button.classList.toggle("active", button.dataset.executionMode === mode);
|
||||
});
|
||||
const hardware = mode === "hardware";
|
||||
$("#approvalField").hidden = !hardware;
|
||||
const approvalRequired = Boolean(state.runtime?.hardware?.approval_required);
|
||||
$("#approvalField").hidden = !hardware || !approvalRequired;
|
||||
$("#executionBadge").className = `status-badge ${hardware ? "" : "neutral"}`.trim();
|
||||
$("#executionBadge").innerHTML = hardware
|
||||
? '<i data-lucide="radio-tower"></i>HIL · 真实 CAN'
|
||||
@@ -2523,9 +2545,13 @@ function selectExecutionMode(mode) {
|
||||
$("#feedbackModeLabel").textContent = hardware
|
||||
? "T2G BENCH · PHYSICAL CAN FEEDBACK"
|
||||
: "MATRIX-BACKED · SIMULATED FEEDBACK";
|
||||
$("#seatFeedbackModeLabel").textContent = hardware
|
||||
? "PHYSICAL CAN FEEDBACK"
|
||||
: "SIMULATED FEEDBACK";
|
||||
const runButtonLabel = $("#runButton span");
|
||||
if (runButtonLabel) {
|
||||
runButtonLabel.textContent = hardware
|
||||
? (approvalRequired ? "授权并真实执行" : "真实执行")
|
||||
: "生成并执行";
|
||||
}
|
||||
updateSeatFeedbackModeLabel();
|
||||
refreshIcons();
|
||||
}
|
||||
|
||||
@@ -2715,7 +2741,7 @@ async function loadRuntime() {
|
||||
if (state.runtime.openai_available) {
|
||||
badge.classList.remove("danger");
|
||||
badge.innerHTML = `<i data-lucide="sparkles"></i>${state.runtime.openai_model}`;
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 结构化输出`;
|
||||
$("#providerNote").textContent = `${state.runtime.openai_model} · Responses API · 失败时自动降级本地规则`;
|
||||
selectProvider("openai");
|
||||
} else {
|
||||
badge.classList.add("danger");
|
||||
@@ -2730,18 +2756,21 @@ async function loadRuntime() {
|
||||
hardwareButton.disabled = !hardware.configured;
|
||||
const profileText = (hardware.firmware_profiles || []).join(" / ") || "未选择 profile";
|
||||
const flashText = hardware.auto_flash_enabled ? "自动烧录已启用" : "自动烧录未启用";
|
||||
const approvalText = hardware.approval_required ? "需授权令牌" : "调试免令牌";
|
||||
$("#hardwareNote").textContent = hardware.configured
|
||||
? `${hardware.backend} · ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText}`
|
||||
: `真实执行已锁定:${profileText};请完成服务端启用、令牌、适配器和固件镜像配置`;
|
||||
? `${hardware.backend} · ${hardware.bitrate / 1000} kbit/s · ${profileText} · ${flashText} · ${approvalText}`
|
||||
: `真实执行已锁定:${profileText};请完成服务端启用、适配器、固件镜像${hardware.approval_required ? "和令牌" : ""}配置`;
|
||||
$("#hardwareAliasNote").textContent = "副驾座椅 → t2g_primary_seat(统一电机、加热和通风执行器)";
|
||||
$("#autoFlashNote").textContent = hardware.auto_flash_enabled
|
||||
? "未检测到匹配固件时,将在发送座椅控制帧前通过 IAR C-SPY 自动烧录并重新验证。"
|
||||
: "仅接受已烧录且身份匹配的统一固件;需要自动烧录时请启用服务端开关。";
|
||||
: "仅接受周期反馈与四轴量程签名匹配的统一固件;需要自动烧录时请启用服务端开关。";
|
||||
$("#hardwareBadge").className = `status-badge ${hardware.configured ? "" : "danger"}`.trim();
|
||||
$("#hardwareBadge").innerHTML = hardware.configured
|
||||
? '<i data-lucide="shield-check"></i>硬件已配置'
|
||||
: '<i data-lucide="shield-off"></i>硬件锁定';
|
||||
$("#hardwareCoverage").textContent = hardware.configured ? "可选 · 需授权" : "已阻断";
|
||||
$("#hardwareCoverage").textContent = hardware.configured
|
||||
? `可选 · ${approvalText}`
|
||||
: "已阻断";
|
||||
$("#hardwareCoverage").className = hardware.configured ? "available" : "blocked";
|
||||
selectExecutionMode("dry_run");
|
||||
refreshIcons();
|
||||
@@ -2753,6 +2782,37 @@ async function loadRuntime() {
|
||||
}
|
||||
}
|
||||
|
||||
async function synchronizePhysicalSeatState() {
|
||||
if (!state.runtime?.hardware?.configured) return false;
|
||||
const query = new URLSearchParams({
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime.default_vehicle_id || "",
|
||||
});
|
||||
try {
|
||||
const response = await fetch(`/api/hardware/seat-state?${query}`, {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (response.status === 401) {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const payload = await response.json();
|
||||
renderSeatState(payload.actuator_state);
|
||||
state.seatFeedbackSource = payload.complete
|
||||
? "physical_can"
|
||||
: "physical_partial";
|
||||
updateSeatFeedbackModeLabel();
|
||||
return true;
|
||||
} catch (_error) {
|
||||
if (!state.seatFeedbackSource.startsWith("physical")) {
|
||||
state.seatFeedbackSource = "unavailable";
|
||||
updateSeatFeedbackModeLabel();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuthStatus() {
|
||||
const response = await fetch("/api/auth/status", { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
@@ -3413,15 +3473,17 @@ function renderExecution(result) {
|
||||
: `${executedDomains.join(" / ")} 共 ${report.spec.results.length} 个操作已完成 CAN 编码与虚拟执行`;
|
||||
if (motionCount) executionDetail += `,含 ${motionCount} 个座椅位置电机反馈`;
|
||||
if (firmwareResult?.telemetry?.programmed_this_request) executionDetail += ";本次已自动烧录并校验统一固件";
|
||||
else if (firmwareResult) executionDetail += ";板上统一固件身份已校验";
|
||||
else if (firmwareResult) executionDetail += ";板上统一固件协议签名已校验";
|
||||
if (result.status === "partially_succeeded") {
|
||||
executionDetail += ";未接入域请求已保留为 deferred request";
|
||||
}
|
||||
if (!reportSucceeded) {
|
||||
const failure = report.spec.results.find((item) => item.status === "failed");
|
||||
const failureTelemetry = failure?.telemetry || {};
|
||||
executionDetail = failureTelemetry.error_type === "FirmwareProgrammingError"
|
||||
? "固件身份验证或自动烧录失败,座椅控制帧未发送。"
|
||||
executionDetail = failureTelemetry.error_type === "CanGatewayError"
|
||||
? `CAN 适配器或总线访问失败:${failure?.message || "无法打开或访问 CAN 适配器。"}`
|
||||
: failureTelemetry.error_type === "FirmwareProgrammingError"
|
||||
? `固件协议签名验证或自动烧录失败,座椅控制帧未发送:${failure?.message || "请检查 CMSIS-DAP 探针和 IAR 配置。"}`
|
||||
: failureTelemetry.calibration_required
|
||||
? `大幅相对移动已转换为绝对目标,但 Motor${failureTelemetry.motor_selector} 的 Position_Valid=0;未发送绝对移动命令,请先在台架清空并备好急停后执行显式端点标定。`
|
||||
: failureTelemetry.failure_category === "stall_no_hall_activity"
|
||||
@@ -3432,7 +3494,7 @@ function renderExecution(result) {
|
||||
? `Motor${failureTelemetry.motor_selector} ${failureTelemetry.motor_result_name},板端事件 ${failureTelemetry.last_motor_event_name || "UNAVAILABLE"};${failureTelemetry.stop_all_attempted ? "已尝试发送 STOP_ALL。" : "请检查硬件回读。"}`
|
||||
: failureTelemetry.stop_all_attempted
|
||||
? "真实台架未返回满足目标的反馈,移动链路已尝试发送 STOP_ALL。"
|
||||
: "真实台架未返回满足目标的物理反馈。";
|
||||
: failure?.message || "真实台架未返回满足目标的物理反馈。";
|
||||
}
|
||||
summary.innerHTML = `<i data-lucide="${reportSucceeded ? ((presetSave || modeSave) ? "bookmark-check" : "circle-check-big") : "circle-x"}"></i><div><strong>${reportSucceeded ? (modeSave ? "长期模式已保存" : presetSave ? "姿态预设已保存" : (physical ? "真实台架执行成功" : "虚拟执行成功")) : "真实台架执行失败"}</strong><span>${executionDetail}</span></div>`;
|
||||
$("#resultNavMeta").textContent = reportSucceeded
|
||||
@@ -3464,6 +3526,14 @@ function renderExecution(result) {
|
||||
|
||||
function renderResult(result) {
|
||||
state.result = result;
|
||||
const reportMode = result.execution_report?.spec?.mode;
|
||||
if (reportMode === "hil") {
|
||||
state.seatFeedbackSource = result.execution_report?.spec?.physical_state?.complete
|
||||
? "physical_can"
|
||||
: "physical_partial";
|
||||
} else if (result.function_configs?.seat) {
|
||||
state.seatFeedbackSource = "simulated";
|
||||
}
|
||||
const executionSucceeded = ["succeeded", "partially_succeeded"].includes(result.status);
|
||||
const domains = Object.keys(result.function_configs || {});
|
||||
const domainLabels = {
|
||||
@@ -3493,6 +3563,7 @@ function renderResult(result) {
|
||||
renderTrace(result.trace_events || []);
|
||||
renderValidation(result.validation || []);
|
||||
renderSeatState(result.actuator_state);
|
||||
updateSeatFeedbackModeLabel();
|
||||
renderDomainState(result.domain_state);
|
||||
updateTwinExplicitAngles(result);
|
||||
renderSavedPresets(result);
|
||||
@@ -3518,8 +3589,10 @@ async function submitRequest(event) {
|
||||
$("#resultTitle").textContent = "正在执行车舱编排";
|
||||
startProgress();
|
||||
const approvalTokenInput = $("#approvalToken");
|
||||
const approvalToken = state.executionMode === "hardware"
|
||||
? approvalTokenInput.value
|
||||
const approvalRequired = state.executionMode === "hardware"
|
||||
&& Boolean(state.runtime?.hardware?.approval_required);
|
||||
const approvalToken = approvalRequired
|
||||
? approvalTokenInput?.value || ""
|
||||
: null;
|
||||
if (approvalTokenInput) approvalTokenInput.value = "";
|
||||
|
||||
@@ -3533,7 +3606,7 @@ async function submitRequest(event) {
|
||||
conversation_id: state.conversationId,
|
||||
vehicle_id: state.vehicleId || state.runtime?.default_vehicle_id,
|
||||
execution_mode: state.executionMode,
|
||||
...(state.executionMode === "hardware"
|
||||
...(approvalRequired
|
||||
? { approval_token: approvalToken }
|
||||
: {}),
|
||||
}),
|
||||
@@ -3545,6 +3618,9 @@ async function submitRequest(event) {
|
||||
}
|
||||
if (!response.ok) throw new Error(payload.message || payload.error || `HTTP ${response.status}`);
|
||||
renderResult(payload);
|
||||
if (state.executionMode === "hardware") {
|
||||
await synchronizePhysicalSeatState();
|
||||
}
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
showToast(completed
|
||||
? (payload.execution_report?.spec?.mode === "hil" ? "真实台架执行与反馈确认已完成" : "可执行动作的 dry-run 已完成")
|
||||
@@ -3558,7 +3634,10 @@ async function submitRequest(event) {
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.classList.remove("loading");
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? "授权并真实执行" : "生成并执行"}</span>`;
|
||||
const hardwareLabel = state.runtime?.hardware?.approval_required
|
||||
? "授权并真实执行"
|
||||
: "真实执行";
|
||||
button.innerHTML = `<i data-lucide="play"></i><span>${state.executionMode === "hardware" ? hardwareLabel : "生成并执行"}</span>`;
|
||||
refreshIcons();
|
||||
}
|
||||
}
|
||||
@@ -3684,6 +3763,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
$$('[data-download]').forEach((button) => button.addEventListener("click", () => downloadArtifact(button.dataset.download)));
|
||||
await loadAuthStatus();
|
||||
await loadRuntime();
|
||||
await synchronizePhysicalSeatState();
|
||||
if (new URLSearchParams(window.location.search).get("autorun") === "1") {
|
||||
$("#requestForm").requestSubmit();
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-22" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-23" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -92,7 +92,7 @@
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">主驾和副驾暂时共享同一组实体电机、加热与通风执行器</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">所有座椅能力仅控制副驾实体电机、加热与通风执行器</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
@@ -108,8 +108,8 @@
|
||||
|
||||
<div class="field-group approval-field" id="approvalField" hidden>
|
||||
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
|
||||
<input id="approvalToken" name="approval_token" type="password" inputmode="numeric" maxlength="6" autocomplete="off" spellcheck="false" placeholder="请输入 123456" />
|
||||
<p class="provider-note">演示授权令牌为 123456,仅用于本次安全校验,不会写入日志、会话或执行制品。</p>
|
||||
<input id="approvalToken" name="approval_token" type="password" maxlength="128" autocomplete="off" spellcheck="false" placeholder="请输入服务端配置令牌" />
|
||||
<p class="provider-note">仅在服务端启用令牌门禁时显示;令牌不会写入日志、会话或执行制品。</p>
|
||||
<p class="provider-note" id="autoFlashNote">正在检查自动烧录配置</p>
|
||||
</div>
|
||||
|
||||
@@ -298,9 +298,9 @@
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾动态座椅</span><small>ACTIVE · FRONT RIGHT</small></div>
|
||||
<div class="twin-callout-layer" aria-hidden="true">
|
||||
<div class="twin-callout" data-callout="longitudinal"><span>水平位置</span><b>50%</b><small>导轨</small></div>
|
||||
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>7°</b><small>50%</small></div>
|
||||
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>20.5°</b><small>20% · 相对竖直</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>78.5°</b><small>0% · 相对地板</small></div>
|
||||
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>4.5°</b><small>50%</small></div>
|
||||
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>13.6°</b><small>20% · 相对归零位</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>0°</b><small>0% · 相对垂直零位</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_extension"><span>腿托伸缩</span><b>0%</b><small>行程</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="backrest_included_angle" hidden><span>靠背-坐垫夹角</span><b>104°</b><small>本轮指定夹角</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="legrest_included_angle" hidden><span>坐垫-腿托夹角</span><b>95°</b><small>本轮指定夹角</small></div>
|
||||
@@ -338,18 +338,10 @@
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><span class="section-kicker">ACTUATOR STATE</span><h3>前排座椅状态</h3></div>
|
||||
<div><span class="section-kicker">ACTUATOR STATE</span><h3>副驾座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾</strong><span>front_left</span></div></div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<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="thermal-states">
|
||||
|
||||
@@ -3,9 +3,11 @@ platform: t2g_seat_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
|
||||
allowed_domain: seat
|
||||
allowed_target_zones: [front_left, front_right]
|
||||
allowed_target_zones: [front_right]
|
||||
allowed_seat_instances: [passenger]
|
||||
physical_target_alias: t2g_primary_seat
|
||||
can:
|
||||
backend: zlgcan
|
||||
@@ -17,13 +19,16 @@ firmware:
|
||||
valid_profiles: [combined]
|
||||
topology: one_board_unified_seat_controller
|
||||
repository_project: firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp
|
||||
identity_request: {frame_id: 0x304, data_hex: "53 45 41 54 01 00 00 00"}
|
||||
identity_response: {frame_id: 0x305, data_hex: "53 45 41 54 01 01 00 3F"}
|
||||
repository_vendor_dependencies: [firmware/t2g/common, firmware/t2g/tviibh8m]
|
||||
verification: passive_301_302_303_304_305_4FA_and_max_hall
|
||||
expected_max_hall: [480, 374, 917, 647]
|
||||
auto_flash_enabled_env: COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED
|
||||
auto_flash_default_when_hardware_enabled: true
|
||||
programmer: iar_cspy_cmsisdap
|
||||
safety:
|
||||
stop_all_on_motion_error: true
|
||||
require_physical_feedback: true
|
||||
longitudinal_motor3_enabled: false
|
||||
cushion_tilt_enabled: false
|
||||
shared_physical_target: true
|
||||
passenger_only: true
|
||||
auto_home_enabled_env: COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED
|
||||
auto_home_default: false
|
||||
home_timeout_seconds_env: COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S
|
||||
|
||||
@@ -36,6 +36,6 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 92
|
||||
position_pct: 100
|
||||
angle_deg: 167
|
||||
reason: 小腿解压悬浮模式腿托电机目标 72 度,按腾势 N7 显示夹角换算。
|
||||
|
||||
@@ -35,6 +35,6 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 49
|
||||
position_pct: 53
|
||||
angle_deg: 133
|
||||
reason: 巡航沙发模式腿托电机目标 38 度,按腾势 N7 显示夹角换算。
|
||||
|
||||
@@ -36,7 +36,7 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 55
|
||||
position_pct: 60
|
||||
angle_deg: 138
|
||||
reason: 电感沙滩椅腿托电机目标 43 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
|
||||
@@ -35,7 +35,7 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 71
|
||||
position_pct: 76
|
||||
angle_deg: 150
|
||||
reason: 休闲躺椅模式腿托电机目标 55 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
|
||||
@@ -36,7 +36,7 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 54
|
||||
position_pct: 58
|
||||
angle_deg: 137
|
||||
reason: 冥想空间模式腿托电机目标 42 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
|
||||
@@ -13,30 +13,30 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
position_pct: 100
|
||||
reason: 小憩模式水平电机目标 0mm,按腾势 N7 215mm 行程换算。
|
||||
- domain: seat
|
||||
capability: seat.set_cushion_tilt
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
position_pct: 100
|
||||
reason: 小憩模式坐盆电机目标。
|
||||
- domain: seat
|
||||
capability: seat.set_backrest_recline
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
angle_deg: 95
|
||||
position_pct: 81
|
||||
angle_deg: 150
|
||||
reason: 小憩模式靠背电机目标 0 度,按腾势 N7 显示夹角换算。
|
||||
- domain: seat
|
||||
capability: seat.set_legrest_height
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 0
|
||||
angle_deg: 95
|
||||
position_pct: 94
|
||||
angle_deg: 163
|
||||
reason: 小憩模式腿托电机目标 0 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
capability: climate.set_temperature
|
||||
|
||||
@@ -35,7 +35,7 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 19
|
||||
position_pct: 21
|
||||
angle_deg: 110
|
||||
reason: 办公模式腿托电机目标 15 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
|
||||
@@ -36,7 +36,7 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 28
|
||||
position_pct: 31
|
||||
angle_deg: 117
|
||||
reason: 女王副驾腿托电机目标 22 度,按腾势 N7 显示夹角换算。
|
||||
- domain: climate
|
||||
|
||||
@@ -38,6 +38,6 @@ operations:
|
||||
target_scope: passenger
|
||||
operation: set
|
||||
parameters:
|
||||
position_pct: 87
|
||||
position_pct: 94
|
||||
angle_deg: 163
|
||||
reason: 零重力模式腿托电机目标 68 度,按腾势 N7 显示夹角换算。
|
||||
|
||||
@@ -27,16 +27,16 @@ seat:
|
||||
kind: included_angle
|
||||
zero_deg: 95
|
||||
max_deg: 163
|
||||
provisional: true
|
||||
provisional: false
|
||||
legrest_height:
|
||||
name: 腿托
|
||||
unit: degree
|
||||
range: 78
|
||||
range: 72
|
||||
reference_zero: retracted
|
||||
positive_direction: raise
|
||||
hall_total_pulses: 647
|
||||
display_angle:
|
||||
kind: included_angle
|
||||
zero_deg: 95
|
||||
max_deg: 173
|
||||
provisional: true
|
||||
max_deg: 167
|
||||
provisional: false
|
||||
|
||||
Executable → Regular
+6
-7
@@ -1,24 +1,23 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionConfig
|
||||
metadata:
|
||||
id: seat-driver-ventilation-on
|
||||
name: Driver seat ventilation on
|
||||
id: seat-passenger-ventilation-on
|
||||
name: Front passenger seat ventilation on
|
||||
version: 1.0.0
|
||||
request_id: req-demo-001
|
||||
description: Executable semantic target derived from the partial seat CAN matrix.
|
||||
description: Executable passenger-only semantic target derived from the partial seat CAN matrix.
|
||||
spec:
|
||||
domain: seat
|
||||
operations:
|
||||
- id: driver_ventilation_on
|
||||
- id: passenger_ventilation_on
|
||||
capability: seat.set_ventilation
|
||||
target:
|
||||
device: seat
|
||||
zone: front_left
|
||||
instance: driver
|
||||
zone: front_right
|
||||
instance: passenger
|
||||
parameters:
|
||||
state: "on"
|
||||
timeout_ms: 3000
|
||||
failure_policy: abort
|
||||
assumptions:
|
||||
- Execution is virtual because frame metadata and hardware safety inputs are incomplete.
|
||||
|
||||
@@ -18,6 +18,10 @@
|
||||
"status": {"type": "string", "enum": ["succeeded", "partially_succeeded", "failed", "cancelled"]},
|
||||
"started_at": {"type": "string", "format": "date-time"},
|
||||
"finished_at": {"type": "string", "format": "date-time"},
|
||||
"physical_state": {
|
||||
"type": "object",
|
||||
"description": "Authoritative physical device state captured from hardware feedback."
|
||||
},
|
||||
"results": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# T2G 座椅前端能力测试集
|
||||
|
||||
本测试集验证从前端自然语言输入、意图计划、`FunctionConfig`、CAN 编译到执行反馈展示的完整链路。机器自动化测试固定使用 `dry_run`,不会连接台架;真实台架部分必须由现场人员逐条执行并目视确认。
|
||||
|
||||
可执行用例源位于 `tests/fixtures/seat_frontend_capabilities.json`,HTTP 回归测试位于 `tests/e2e/test_seat_web_api.py`。
|
||||
|
||||
## 覆盖范围
|
||||
|
||||
| 用例 | 前端输入 | 重点验证 |
|
||||
| --- | --- | --- |
|
||||
| THERM-01/02 | 副驾加热3档、关闭加热 | 加热开关、档位和 `0x4FA` 回读 |
|
||||
| THERM-03/04 | 副驾通风2档、关闭通风 | 通风开关、档位和 `0x4FA` 回读 |
|
||||
| MOTION-01 | 副驾水平位置25% | M0、`0x302≈2500`、COMPLETE/OK |
|
||||
| MOTION-02 | 副驾坐盆倾角35% | M1、`0x302≈3500`、COMPLETE/OK |
|
||||
| MOTION-03 | 副驾靠背130度 | M2、角度标定和真实位置反馈 |
|
||||
| MOTION-04 | 副驾腿托145度 | M3、角度标定和真实位置反馈 |
|
||||
| COMBINED-01 | 一句话同时设置热管理和四轴 | 多操作编排、逐项反馈与失败传播 |
|
||||
| DEFAULT-01 | 未指定座位的通风请求 | 自动使用副驾,不生成主驾操作 |
|
||||
| SAFE-01 | 显式主驾通风请求 | 返回仅支持副驾,不得重定向或发 CAN |
|
||||
| SAFE-02 | 独立腿托伸缩 | 当前台架不支持,打开CAN前拒绝 |
|
||||
| SAFE-03 | 生产默认配置下错误/缺失授权令牌 | HTTP 403,CAN 不得打开;本地显式设置 `COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false` 时不适用 |
|
||||
| RECOVERY-01 | 未烧写或固件不匹配后请求加热 | 自动烧写、重新验证、继续原动作 |
|
||||
|
||||
## 自动化执行
|
||||
|
||||
```powershell
|
||||
$env:PYTHONDONTWRITEBYTECODE='1'
|
||||
$env:PYTHONPATH='src;domains/seat/src'
|
||||
python -m unittest tests.e2e.test_seat_web_api
|
||||
```
|
||||
|
||||
自动测试会逐条向 `/api/requests` 提交测试集中的自然语言输入,验证所有六项真实台架能力均生成副驾目标和正确参数,并确认执行模式保持为 `dry_run`。领域集成测试另行验证未指定座位时默认副驾,以及本地解析和模型输出中的显式主驾目标都被拒绝。
|
||||
|
||||
## 真实台架执行顺序
|
||||
|
||||
1. 固定座椅、清空行程、准备急停和限流电源;保持 `COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED=false`。
|
||||
2. 先执行 RECOVERY-01。可使用未烧写控制器,或仅在确认允许覆盖当前固件后进行;前端结果必须显示 `programmed_this_request=true`、`firmware_verified=true`,随后才出现动作反馈。
|
||||
3. 依次执行 THERM-01~04,确认开、关和档位回读。
|
||||
4. 让四轴处于有效位置后,依次执行 MOTION-01~04。每次只观察一个物理轴,映射或方向错误立即急停。
|
||||
5. 单项全部通过后再执行 COMBINED-01;逐项核对 ACK、TaskState、位置、Hall、电流诊断及最终报告。
|
||||
6. 最后执行 SAFE-01~03,确认所有安全用例均未产生 CAN 动作。
|
||||
|
||||
普通一句话请求绝不触发全行程标定。若 `PositionValid=0`,保持自动归零关闭时动作应安全失败;只有确认台架无遮挡且允许回最近端后,才能单独启用已知量程归零。
|
||||
@@ -22,8 +22,6 @@
|
||||
|
||||
| capability | target | parameter | 0x42A signal |
|
||||
| --- | --- | --- | --- |
|
||||
| `seat.set_ventilation` | `front_left` | `state: on/off` | `Driver_Seat_Ventilation_S` |
|
||||
| `seat.set_heating` | `front_left` | `state: on/off` | `Driver_Seat_Heating_S` |
|
||||
| `seat.set_ventilation` | `front_right` | `state: on/off` | `Front_Passenger_Seat_Ventilation_S` |
|
||||
| `seat.set_heating` | `front_right` | `state: on/off` | `Front_Passenger_Seat_Heating_S` |
|
||||
|
||||
@@ -39,7 +37,7 @@
|
||||
| `seat.set_legrest_height` | 完全降下 | 完全升起 |
|
||||
| `seat.set_legrest_extension` | 完全收回 | 完全伸出 |
|
||||
|
||||
五轴均支持主副驾。模型必须基于会话中的当前位置将相对语句转换为绝对 `position_pct`;适配器不会自行解释“往后、躺一点、再伸长”。能力范围参考了厂商公开车主手册中常见的前后、坐垫倾角、靠背和腿部支撑调节,但 CAN 定义并非来自这些厂商。
|
||||
五轴只支持副驾。模型必须基于会话中的当前位置将相对语句转换为绝对 `position_pct`;适配器不会自行解释“往后、躺一点、再伸长”。能力范围参考了厂商公开车主手册中常见的前后、坐垫倾角、靠背和腿部支撑调节,但 CAN 定义并非来自这些厂商。
|
||||
|
||||
靠背和腿托同时接受精确夹角语义:
|
||||
|
||||
@@ -48,7 +46,7 @@
|
||||
|
||||
GPT 保留用户指定的原始 `angle_deg`,适配器使用线性演示标定确定性转换为电机 `position_pct`;`FunctionConfig`、模拟反馈和孪生模型同时保留百分比与夹角。该换算只用于验证认知编排和 UI,不代表真实座椅运动学。
|
||||
|
||||
`seat-motion-virtual-matrix.yaml` 使用虚构标准帧 `0x550/0x551` 控制主/副驾,用 `0x558/0x559` 反馈。每个命令包含绝对目标、轴使能掩码、计划内命令序号、模式和虚构 checksum;反馈包含实际位置、moving mask、fault mask 和 position-valid mask。`CommandPlan.completion` 要求位置在 `±1%` 内到位,虚拟执行报告保留起点、中间采样和终点。
|
||||
虚拟链路只使用标准帧 `0x551` 控制副驾,并用 `0x559` 反馈。每个命令包含绝对目标、轴使能掩码、计划内命令序号、模式和虚构 checksum;反馈包含实际位置、moving mask、fault mask 和 position-valid mask。`CommandPlan.completion` 要求位置在 `±1%` 内到位,虚拟执行报告保留起点、中间采样和终点。
|
||||
|
||||
## 用户姿态预设
|
||||
|
||||
@@ -68,10 +66,10 @@ GPT 保留用户指定的原始 `angle_deg`,适配器使用线性演示标定
|
||||
|
||||
`domains/seat/can/seat-thermal-matrix.yaml` 保存 6 个报文、23 个信号:
|
||||
|
||||
- `0x42A`:主/副驾通风、加热开关候选控制信号。
|
||||
- `0x4FA`:主/副驾通风、加热工作状态。
|
||||
- `0x29E`:主驾开关、模式和 1–5 档状态。
|
||||
- `0x3CE`:主驾通风/加热控制模式。
|
||||
- `0x42A`:原始矩阵中的主/副驾通风、加热开关候选控制信号;当前应用只编译副驾映射。
|
||||
- `0x4FA`:原始矩阵中的主/副驾通风、加热工作状态;当前应用只消费副驾状态。
|
||||
- `0x29E`:板端既有 `Driver_Seat_*` 命名字段,不代表应用目标为主驾。
|
||||
- `0x3CE`:板端既有 `Driver_Seat_*` 通风/加热控制字段,不代表应用目标为主驾。
|
||||
- `0x3D3`:副驾开关、模式和 1–5 档状态。
|
||||
- `0x2AA / sub-id 0x01`:主/副驾档位配置状态。
|
||||
|
||||
@@ -80,8 +78,8 @@ GPT 保留用户指定的原始 `angle_deg`,适配器使用线性演示标定
|
||||
因此以下值是演示结果,不是实车发送结论:
|
||||
|
||||
```text
|
||||
打开主驾通风 + 关闭副驾加热
|
||||
0x42A minimum payload: 01 80
|
||||
打开副驾通风 + 关闭副驾加热
|
||||
0x42A minimum payload: 00 90
|
||||
```
|
||||
|
||||
## 认知档位与主动阻断项
|
||||
@@ -90,7 +88,7 @@ GPT 保留用户指定的原始 `angle_deg`,适配器使用线性演示标定
|
||||
- 4–5 档目标:超出当前自动舒适策略和 `0x4FA` 虚拟回读范围,主动阻断。
|
||||
- 零重力/姿态:可用虚构五轴、演示夹角标定和用户预设验证模型分解,但没有真实运动学或车型安全标定。
|
||||
- 硬件发送:平台、矩阵和映射均未达到 HIL/vehicle verified。
|
||||
- 显式控制语句缺目标或开关状态:进入 `needs_confirmation`;舒适反馈可依据会话目标和默认强度策略补全。
|
||||
- 显式控制语句缺少座位目标时默认副驾;显式主驾、主副驾或前排目标进入 `needs_confirmation`,不得重定向。缺少开关状态时仍需确认;舒适反馈可依据副驾会话状态和默认强度策略补全。
|
||||
|
||||
## GPT 边界
|
||||
|
||||
@@ -98,7 +96,7 @@ GPT 返回受动态 JSON Schema 约束的 `domain/capability/target_scope/operat
|
||||
|
||||
当 `planning_environment=dry_run_candidate_synthesis` 时,命名 preset 不是执行前提。对“休憩、休息、阅读”等可理解的模式目标,GPT 应使用已注册的原子能力生成保守的绝对位置候选方案,并在 `decision_summary` 中记录推断。“没有同名预设”本身不构成歧义;只有目标座位、期望结果或所需能力确实无法判断时才要求确认。这类百分比是模型推断的 dry-run 候选值,不是人机工程或车型标定结论。
|
||||
|
||||
模型结果返回后,本地层只执行能力注册表一致性检查、参数校验、路由与域适配,不使用本地领域规则重新解释或改写 GPT 动作。未安装执行适配器的能力保留为 `deferred_requests`:纯未接入域请求返回 `capability_unavailable`,混合请求则先执行已接入子请求并返回 `partially_succeeded`。本地解析器只作为未配置 GPT 或 `auto` 模式调用失败时的显式降级。UI 展示 `decision_summary`、`model_requests` 和确定性阶段事件,不保存或展示模型私有 chain-of-thought。
|
||||
模型结果返回后,本地层只执行能力注册表一致性检查、参数校验、路由与域适配,不使用本地领域规则重新解释或改写 GPT 动作。未安装执行适配器的能力保留为 `deferred_requests`:纯未接入域请求返回 `capability_unavailable`,混合请求则先执行已接入子请求并返回 `partially_succeeded`。本地解析器作为未配置 GPT,或 GPT/`auto` 模式调用失败时的显式安全降级,并以 `local_rules_fallback` 标记。UI 展示 `decision_summary`、`model_requests` 和确定性阶段事件,不保存或展示模型私有 chain-of-thought。
|
||||
|
||||
## 实车前置条件
|
||||
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
# T2G 座椅真实硬件台架接入
|
||||
|
||||
本链路把一句话生成的 `FunctionConfig` 确定性编译成统一 IAR 固件的真实经典 CAN 协议,并以开发板反馈作为成功依据。默认仍是 `dry_run`;只有服务端显式启用、请求提供正确授权令牌、固件 profile 匹配且目标通过配置门禁时,才会打开 CAN 适配器。绝对位置还会在发命令前实时检查板端 `Position_Valid`;未标定时仅允许显式、小幅相对指令使用受限定时/Hall 路径。主驾和副驾当前都是 `t2g_primary_seat` 的逻辑别名,两种指令驱动同一组实体电机、加热和通风。
|
||||
本链路把前端一句话生成的 `FunctionConfig` 确定性编译为统一 IAR 固件的经典 CAN 协议,并以开发板真实反馈作为成功依据。默认仍为 `dry_run`;只有服务端启用硬件且目标通过门禁时才会打开 CAN 适配器。生产/公网环境默认还要求每次请求携带正确授权令牌;本地受控调试可显式关闭该令牌门禁。
|
||||
|
||||
## 固件与拓扑
|
||||
## 自包含固件
|
||||
|
||||
仓库内的固件接口工程为:
|
||||
仓库已经包含完整构建输入:
|
||||
|
||||
```text
|
||||
firmware\t2g\Seat_Controller_demo\IAR\Seat_Controller_demo.ewp
|
||||
firmware/t2g/
|
||||
├── CYPRESS_SDK_LICENSE.txt
|
||||
├── common/
|
||||
├── tviibh8m/
|
||||
└── Seat_Controller_demo/
|
||||
├── App/ Bsp/ Config/ Driver/
|
||||
├── Docs/ Tests/
|
||||
└── IAR/Seat_Controller_demo.ewp
|
||||
```
|
||||
|
||||
厂商 SDK 只作为外部编译依赖,默认根目录为:
|
||||
|
||||
```text
|
||||
C:\Users\D30042075206\Desktop\T2G_Sample_Driver_Library_7.9.0
|
||||
```
|
||||
|
||||
三个已验证 demo 已合并为一个单板统一固件;原工程继续作为能力来源和回归基线:
|
||||
|
||||
| 来源 | IAR 工程 | 能力 | CAN |
|
||||
| --- | --- | --- | --- |
|
||||
| 加热基线 | `Seat_Backrest_Heater_demo` | 主驾靠背加热 | RX `0x3CE/0x29E`,TX `0x4FA` |
|
||||
| 通风基线 | `Seat_Fan_demo` | 主驾通风 | RX `0x29E`,TX `0x4FA` |
|
||||
| 移动/安全骨架 | `Seat_Move_demo` | 腿托伸缩/升降、靠背;Motor3 暂锁定 | RX `0x300`,TX `0x301/0x302/0x303` |
|
||||
| 仓库内单板交付 | `firmware/t2g/Seat_Controller_demo` | 上述能力、共享 CAN4/IRQ/WDT、固件身份握手 | 全部上述报文及 `0x304/0x305` |
|
||||
|
||||
真实执行只接受仓库内 `combined` profile 和 `Seat_Controller_demo.hex/.out`。该工程用一个 CAN4 实例接收热管理与移动命令,`0x4FA` 同时填充通风 bits0..2 和加热 bits3..5;移动 ACK/反馈保持 `0x301/0x302/0x303`。三个原始 demo 镜像不再作为 Web 硬件执行入口。
|
||||
|
||||
每次真实请求在控制帧发送前先执行不驱动执行器的身份握手:主机向 `0x304` 发送 `53 45 41 54 01 00 00 00`,固件必须在 `0x305` 返回 `53 45 41 54 01 01 00 3F`。该响应表示协议 1、固件 1.1.0 和统一能力掩码。响应缺失或不匹配时,后端不会发送加热、通风或电机命令。
|
||||
|
||||
## 构建(不烧录)
|
||||
因此迁移仓库后不再依赖原电脑桌面的 `T2G_Sample_Driver_Library_7.9.0`。IAR、CMSIS-DAP 与 ZLG 驱动属于本机工具链/硬件驱动,仍须在台架电脑安装。
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\build_seat_firmware.ps1 -Profile combined
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\build_seat_firmware.ps1
|
||||
```
|
||||
|
||||
脚本把仓库内工程临时放到与 SDK `common/tviibh8m` 相同的相对拓扑中,调用 IAR EWARM 9.2 的 `iarbuild.exe` 构建 `rev_d`,再把 HEX/OUT 回写到仓库并输出 HEX SHA-256。脚本不包含下载、烧录或电机启动操作。
|
||||
构建脚本只使用仓库内 `common/tviibh8m`,在临时目录编译并回写 HEX/OUT,不连接探针、不烧录、不启动执行器。非标准 IAR 路径可通过 `COGNITIVE_AGENT_IAR_BUILD` 或 `-IarBuild` 指定。
|
||||
|
||||
后端自动烧录与构建脚本相互独立:只有真实台架请求已通过授权、`COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED=true`、身份握手失败且 IAR/C-SPY 依赖完整时,才会关闭 CAN,使用 CMSIS-DAP 和专用 `Seat_Controller_program.mac` 执行一次限时 Download-and-Debug,并以 `--leave_target_running` 把目标交还给新固件,随后重新打开 CAN 并再次验证 `0x305`。不能使用 `--download_only`,因为 IAR 会在下载后保持 CM0+ 停止。该宏在下载前强制关闭电机、风扇和加热输出并清除 program-only 调试标记;新固件从正常复位入口启动。二次验证失败时控制帧仍被阻断;交互调试仍使用原 `Seat_Controller_demo.mac` 的调试脱离故障锁存。
|
||||
## 四轴与量程
|
||||
|
||||
## 物理链路检查
|
||||
统一坐标为最近端 `0 Hall = 0%`,朝最远端 Hall 增加,`0x302.Value_X100` 范围为 0–10000。
|
||||
|
||||
1. 开发板 CAN4 使用 `RX=P3.7`、`TX=P3.6`,`P3.1` 为板载 CAN 收发器使能,高电平有效。
|
||||
2. ZLG 适配器、各开发板和执行器控制硬件必须共地;CANH 对 CANH、CANL 对 CANL,禁止反接。
|
||||
3. 总线两端各使用一个 120 Ω 终端;断电测量 CANH 与 CANL 之间应约为 60 Ω。短台架也只能保留总线两端的两个终端。
|
||||
4. 使用经典标准 CAN、11-bit ID、DLC 8、500 kbit/s;不要把旧版 32-byte CAN-FD 表用于当前 `Seat_Move_demo`。
|
||||
5. 先确认急停/断电手段、机械行程、夹伤区域、执行器电源能力和 Hall 接线,再上电。不要依赖 Web 软件替代物理急停。
|
||||
6. ZCANPRO 与本服务不能同时独占同一个适配器。启动服务前关闭 ZCANPRO 的在线通道。
|
||||
| MotorID | 前端能力 | 物理执行器 | 最近端 → 最远端 | MaxHall | 全量程 |
|
||||
| ---: | --- | --- | --- | ---: | ---: |
|
||||
| M0 | `seat.set_longitudinal_position` | 水平前后 | 最前 → 最后 | 480 | 215 mm |
|
||||
| M1 | `seat.set_cushion_tilt` | 坐盆斜上下 | 最高 → 斜向下 | 374 | 水平 51 mm / 垂直 56 mm |
|
||||
| M2 | `seat.set_backrest_recline` | 靠背 | 竖直 → 后躺 | 917 | 68° |
|
||||
| M3 | `seat.set_legrest_height` | 腿托 | 最下 → 抬起 | 647 | 72° |
|
||||
|
||||
## 服务端启用
|
||||
四轴与 physical Mode 同号,均已纳入硬件映射;台架不实现独立腿托伸缩轴,因此 `seat.set_legrest_extension` 只保留虚拟展示,真实请求会在打开 CAN 前拒绝。
|
||||
|
||||
把 `.env.example` 中相关变量复制到本机 `.env`,至少设置:
|
||||
## CAN 协议与反馈
|
||||
|
||||
- 热管理命令:`0x29E/0x3CE`;档位反馈:`0x4FA`。
|
||||
- 移动命令:`0x300`;系统 ACK:`0x301`;归一化位置:`0x302`;Hall/量程:`0x303`。
|
||||
- 电流诊断:`0x304` 四轴轮询,`0x305` 通风/加热;这些值用于台架可见性,当前保守阈值不能替代外部限流或量产保护。
|
||||
- Opcode 1 的 `ParamA` 为 0–10000 绝对位置,`ParamB=0` 使用各轴固件默认超时。
|
||||
- Opcode 3 的 `ParamA` 为持续时间,`ParamB` 为驱动等级×10(默认 90% 即 900)。
|
||||
- `STOP_ALL` 的专用 ACK 为 4;完成/故障 TaskState 分别为 6/7。
|
||||
|
||||
新版固件把 `0x304/0x305` 用于电流诊断,不能继续发送旧版身份请求。后端在任何控制帧之前被动观察 `0x301–0x305`、`0x4FA`、四个 MotorID 和 MaxHall `480/374/917/647`;真实硬件执行启用后,未观察到完整签名会自动使用仓库内 IAR 镜像烧写,重新被动验证成功后继续原请求。烧写或复验失败时禁止发送座椅控制帧。
|
||||
|
||||
## 启用配置
|
||||
|
||||
```dotenv
|
||||
COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED=true
|
||||
COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED=false
|
||||
COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN=123456
|
||||
COGNITIVE_AGENT_SEAT_FIRMWARE_PROFILES=combined
|
||||
COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED=true
|
||||
@@ -61,42 +61,29 @@ COGNITIVE_AGENT_IAR_CSPYBAT=D:\Program Files\IAR Systems\Embedded Workbench 9.2\
|
||||
COGNITIVE_AGENT_SEAT_CAN_BACKEND=zlgcan
|
||||
COGNITIVE_AGENT_SEAT_CAN_CHANNEL=0
|
||||
COGNITIVE_AGENT_SEAT_CAN_BITRATE=500000
|
||||
COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL=false
|
||||
COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED=false
|
||||
COGNITIVE_AGENT_SEAT_HOME_TIMEOUT_S=120
|
||||
```
|
||||
|
||||
服务运行在 64 位 Python,ZCANPRO 的 `zlgcan.dll` 和内置 Python 是 32 位,因此主进程会启动 `zlgcan_bridge.py` JSON-line 子进程。运行时页面只显示“令牌已配置”和固件哈希,不返回令牌本身。
|
||||
`COGNITIVE_AGENT_HARDWARE_APPROVAL_TOKEN_REQUIRED` 默认为 `true`,生产和公网部署应保持默认值。本地受控台架调试可显式设为 `false`,此时前端隐藏令牌输入且服务端不再逐请求校验令牌;硬件启用、固件、适配器、副驾目标、物理反馈和故障 STOP_ALL 等其他门禁保持不变。
|
||||
|
||||
可先做不打开设备的 DLL/ABI 自检:
|
||||
`COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED` 在真实硬件执行启用时默认为 `true`,上面的显式配置便于台架审计;如需禁止自动覆盖控制器固件,可明确设置为 `false`。此默认值不会影响 `dry_run`。
|
||||
|
||||
```powershell
|
||||
& 'C:\Program Files (x86)\ZCANPRO\python38\python.exe' `
|
||||
'.\src\cognitive_agent\infrastructure\can\zlgcan_bridge.py' `
|
||||
--dll 'C:\Program Files (x86)\ZCANPRO\zlgcan.dll' `
|
||||
--device-type 41 --self-test
|
||||
```
|
||||
当前台架只公布并接受副驾目标 `passenger/front_right`。没有座位词的座椅指令默认副驾;显式主驾、主副驾或前排范围请求会在打开 CAN 前拒绝,不能静默重定向。热管理 CAN 中仍出现的 `Driver_Seat_*` 是板端既有协议字段名,不代表应用层可以控制主驾。
|
||||
|
||||
## 请求与反馈闭环
|
||||
## 绝对位置与已知量程归零
|
||||
|
||||
前端选择“真实台架”,输入本次授权令牌后再提交。硬件路径只接受 `seat` 单域,`front_left/driver` 和 `front_right/passenger` 都映射到 `t2g_primary_seat`:
|
||||
绝对命令发送前必须看到 `Position_Valid=1`。SMV8 clean 记录有效时,一句话可以直接下发绝对目标;位置无效时默认失败关闭,不会静默触发全行程标定。
|
||||
|
||||
- 固件:先执行 `0x304/0x305` 身份握手;必要时自动烧录并二次握手。烧录和身份验证结果写入 `ExecutionReport`,授权令牌不会写入制品。
|
||||
台架无遮挡、急停就绪并且明确允许自动回最近端时,可设置 `COGNITIVE_AGENT_SEAT_AUTO_HOME_ENABLED=true`。后端仅对目标失效轴发送 `HOME_KNOWN_RANGE(0x0B)`,先核对 MaxHall,再等待 `COMPLETE/OK`、Near、Hall≤2 和 PositionValid,随后继续原绝对目标。此流程会产生真实机械运动,但不会访问最远端或重测量程。Opcode `0x0A` 全行程标定不会由普通一句话请求触发。
|
||||
|
||||
- 热/风:发送真实 `0x3CE/0x29E`,必须等到 `0x4FA` 的实际档位匹配。
|
||||
- 绝对移动:打开适配器后先发 `STOP_ALL` 并用 `0x301` 重同步 sequence;发送 Opcode `1` 前读取目标电机 `0x302.Position_Valid`。未标定时不会发送绝对移动帧,而是返回需要显式端点标定的错误。标定有效时必须收到匹配 sequence 的 ACCEPTED ACK,再等 `0x302` 返回 COMPLETE/OK、位置有效且在容差内。
|
||||
- 小幅相对移动:GPT 仍输出解析后的绝对目标,同时 `FunctionConfig.operation.motion` 保留 `increase/decrease`、起点和增量。未标定安全路径编译为 Opcode `3`、50% 定时占空比;时长按仓库内 500 ms 实测 Hall 速度确定,单次增量限制为 15%。成功必须同时满足 ACK、`0x302 COMPLETE/OK` 和固件内部“至少一个原始 Hall 边沿”门禁;后端还记录 `0x303` 的 Hall 前后值并核对可观测方向。
|
||||
- 拒绝、安全故障、Hall 未运动、方向不符、位置无效或超时:报告失败,并立即尽力再次发送 `STOP_ALL`;失败的动作不会写入会话状态。未标定相对移动成功时会话中的绝对百分比/角度明确标记为估计值,不能替代端点标定结果。
|
||||
## 上电与安全检查
|
||||
|
||||
Opcode `10` 的全行程端点标定会主动运动到两个机械端点,普通一句话请求绝不会静默触发。需要精确绝对角度时,应先在台架无遮挡、急停可用的维护流程中显式完成目标电机标定,并保存 clean 记录。
|
||||
1. 固定座椅,清空四轴全行程,准备硬件急停和 12 V 限流电源。
|
||||
2. CAN4:RX=P3.7、TX=P3.6、P3.1 收发器使能;CANH/CANL 正确、共地、总线两端各 120 Ω。
|
||||
3. 使用 Classic CAN、11-bit、DLC 8、500 kbit/s;ZCANPRO 与服务不能同时独占适配器。
|
||||
4. 首次动作只做单轴短行程并目视核对 M0/M1/M2/M3 与方向;任一映射异常立即急停。
|
||||
5. 先验证通风/加热关闭与低档,再执行移动;无示波器时不得声称 PWM、死区或硬实时关断已经验证。
|
||||
6. 每次检查 `ExecutionReport.spec.mode=hil`、协议签名、命令帧、ACK、位置/Hall 回读与失败后的 STOP_ALL。
|
||||
|
||||
如果一句话同时要求主驾和副驾对同一能力使用相同目标,编译器只发送一次实体命令,并把同一真实回读投影给两个逻辑目标;如果两侧对同一实体能力提出冲突目标,请求会在打开 CAN 之前被拒绝。真实执行成功后,两侧会话状态保持同步。
|
||||
|
||||
Motor0(腿托伸缩)、Motor1(腿托升降)和 Motor2(靠背)已验证。Motor3(座椅前后)仍未接线、未实标,默认强制禁止。`seat.set_cushion_tilt` 在当前固件没有物理电机,始终禁止。只有完成 Motor3 接线、端点标定、方向核对和风险评审后,才能显式设置 `COGNITIVE_AGENT_ENABLE_UNVERIFIED_LONGITUDINAL=true`。
|
||||
|
||||
## 建议上电顺序
|
||||
|
||||
1. 保持服务为 `dry_run`,运行完整测试。
|
||||
2. 从仓库构建 `Seat_Controller_demo`。首次联调建议人工烧录并先用 ZCANPRO 观察周期反馈;确认 CMSIS-DAP、复位和供电链路后再启用自动烧录。
|
||||
3. 使用固件文档中的短时测试帧逐电机核对方向、Hall 与急停;Motor3 不参与。
|
||||
4. 关闭 ZCANPRO 在线通道,启用服务端环境变量;需要自动烧录时保持 CMSIS-DAP 连接并显式设置自动烧录开关。
|
||||
5. 前端先执行加热/通风 OFF,再做低档 ON;移动先从小行程、无遮挡状态开始。
|
||||
6. 对每次实际动作核对 `ExecutionReport.spec.mode=hil`、真实反馈帧和固件镜像哈希。
|
||||
完整固件协议、HIL 结果和手工回归工具位于 `firmware/t2g/Seat_Controller_demo/Docs` 与 `Tests/manual_hil`。
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
version: 3.0.0
|
||||
version: 4.0.0
|
||||
source:
|
||||
project: Seat_Move_demo
|
||||
workbook: Seat_Move_demo/Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx
|
||||
test_report: Seat_Move_demo/Docs/TEST_REPORT_2026-07-24.md
|
||||
verified_on: 2026-07-24
|
||||
scope: t2g_driver_seat_four_motor_protocol
|
||||
project: Seat_Controller_demo
|
||||
specification: firmware/t2g/Seat_Controller_demo/Docs/座椅控制器_CAN_测试规范.md
|
||||
test_status: firmware/t2g/Seat_Controller_demo/Docs/HIL_TEST_STATUS_2026-07-30.md
|
||||
verified_on: 2026-07-30
|
||||
scope: t2g_primary_seat_four_motor_protocol
|
||||
hardware_execution_allowed: true
|
||||
bit_numbering: lsb0
|
||||
byte_order: intel_little_endian
|
||||
limitations:
|
||||
- Motor 0 through 2 were hardware verified; Motor 3 is disabled until it is wired and calibrated.
|
||||
- Cushion tilt has no actuator in Seat_Move_demo and is never eligible for hardware transmission.
|
||||
- Mechanical direction labels still require a final visual sign-off on the assembled seat.
|
||||
- M0/M1/M2/M3 are one physical seat's horizontal, seat-pan, backrest and legrest axes.
|
||||
- Absolute position requires a valid SMV8 position; known-range homing is a separately gated maintenance action.
|
||||
- Current diagnostics are calibrated for observability; production over-current thresholds remain unverified.
|
||||
messages:
|
||||
- name: T2G_Seat_Move_Command_300
|
||||
frame_id: 0x300
|
||||
@@ -31,19 +31,19 @@ messages:
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Ack, start_bit: 0, bit_length: 3}
|
||||
- {name: Reject_Reason, start_bit: 3, bit_length: 3}
|
||||
- {name: Shutdown_Inhibit, start_bit: 6, bit_length: 1}
|
||||
- {name: Safety_Inhibit, start_bit: 7, bit_length: 1}
|
||||
- {name: Last_Opcode, start_bit: 8, bit_length: 4}
|
||||
- {name: Active_Mask, start_bit: 12, bit_length: 4}
|
||||
- {name: Ack, start_bit: 0, bit_length: 4}
|
||||
- {name: System_State, start_bit: 4, bit_length: 4}
|
||||
- {name: Reject_Reason, start_bit: 8, bit_length: 8}
|
||||
- {name: Last_Sequence, start_bit: 16, bit_length: 16}
|
||||
- {name: Retry_Mask, start_bit: 32, bit_length: 4}
|
||||
- {name: Record_Clean, start_bit: 36, bit_length: 1}
|
||||
- {name: Watchdog_Reset, start_bit: 37, bit_length: 1}
|
||||
- {name: Record_Valid, start_bit: 38, bit_length: 1}
|
||||
- {name: Safety_Fault, start_bit: 40, bit_length: 8}
|
||||
- {name: Config_Sequence, start_bit: 48, bit_length: 16}
|
||||
- {name: Last_Opcode, start_bit: 32, bit_length: 8}
|
||||
- {name: Active_Mask, start_bit: 40, bit_length: 4}
|
||||
- {name: Safety_Inhibit, start_bit: 44, bit_length: 1}
|
||||
- {name: Calibration_Busy, start_bit: 46, bit_length: 1}
|
||||
- {name: Fault_Summary, start_bit: 47, bit_length: 1}
|
||||
- {name: Faulted_Mask, start_bit: 48, bit_length: 4}
|
||||
- {name: Position_Valid_Mask, start_bit: 52, bit_length: 4}
|
||||
- {name: Alive, start_bit: 56, bit_length: 4}
|
||||
- {name: Firmware_Revision, start_bit: 60, bit_length: 4}
|
||||
- name: T2G_Seat_Move_Motor_Status_302
|
||||
frame_id: 0x302
|
||||
role: feedback
|
||||
@@ -54,8 +54,11 @@ messages:
|
||||
- {name: Result, start_bit: 12, bit_length: 4}
|
||||
- {name: Direction, start_bit: 16, bit_length: 2}
|
||||
- {name: Active, start_bit: 18, bit_length: 1}
|
||||
- {name: Retried, start_bit: 19, bit_length: 1}
|
||||
- {name: Position_Valid, start_bit: 20, bit_length: 1}
|
||||
- {name: Position_Valid, start_bit: 19, bit_length: 1}
|
||||
- {name: Near_Endpoint, start_bit: 20, bit_length: 1}
|
||||
- {name: Far_Endpoint, start_bit: 21, bit_length: 1}
|
||||
- {name: Stall, start_bit: 22, bit_length: 1}
|
||||
- {name: Driver_Fault, start_bit: 23, bit_length: 1}
|
||||
- {name: Motor_Sequence, start_bit: 24, bit_length: 16}
|
||||
- {name: Value_X100, start_bit: 40, bit_length: 16}
|
||||
- {name: Duty_Percent, start_bit: 56, bit_length: 8}
|
||||
@@ -66,6 +69,36 @@ messages:
|
||||
signals:
|
||||
- {name: Motor_Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Last_Motor_Event, start_bit: 8, bit_length: 4}
|
||||
- {name: Fault_Flags, start_bit: 12, bit_length: 4}
|
||||
- {name: Hall_Count, start_bit: 16, bit_length: 16}
|
||||
- {name: Max_Hall_Count, start_bit: 32, bit_length: 16}
|
||||
- {name: Max_Speed_Pps_X100, start_bit: 48, bit_length: 16}
|
||||
- {name: Hall_Speed_Half_Pps, start_bit: 48, bit_length: 8}
|
||||
- {name: Retry_Count, start_bit: 56, bit_length: 4}
|
||||
- {name: Diagnostic_Alive, start_bit: 60, bit_length: 4}
|
||||
- name: T2G_Seat_Move_Current_Diagnostic_304
|
||||
frame_id: 0x304
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Motor_Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Current_Sample_Valid, start_bit: 8, bit_length: 1}
|
||||
- {name: Current_Baseline_Valid, start_bit: 9, bit_length: 1}
|
||||
- {name: High_Current, start_bit: 10, bit_length: 1}
|
||||
- {name: Hall_Activity_Low, start_bit: 11, bit_length: 1}
|
||||
- {name: Current_Calibrating, start_bit: 12, bit_length: 1}
|
||||
- {name: Current_Raw, start_bit: 16, 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: T2G_Seat_Thermal_Current_Diagnostic_305
|
||||
frame_id: 0x305
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Fan_Current_Sample_Valid, start_bit: 0, bit_length: 1}
|
||||
- {name: Heater_Current_Sample_Valid, start_bit: 1, bit_length: 1}
|
||||
- {name: Fan_Running, start_bit: 8, bit_length: 1}
|
||||
- {name: Heater_Level, start_bit: 9, bit_length: 2}
|
||||
- {name: Fan_Current_Raw, start_bit: 16, bit_length: 16}
|
||||
- {name: Heater_Current_Raw, start_bit: 32, bit_length: 16}
|
||||
- {name: Fan_Adc_Error_Count, start_bit: 48, bit_length: 8}
|
||||
- {name: Heater_Adc_Error_Count, start_bit: 56, bit_length: 8}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
version: 1.0.0
|
||||
version: 2.0.0
|
||||
source:
|
||||
projects:
|
||||
- Seat_Backrest_Heater_demo
|
||||
@@ -6,15 +6,16 @@ source:
|
||||
workbooks:
|
||||
- 座椅加热功能CAN矩阵表.xlsx
|
||||
- 座椅通风功能CAN矩阵表.xlsx
|
||||
verified_on: 2026-07-23
|
||||
scope: t2g_driver_seat_heating_and_ventilation
|
||||
verified_on: 2026-07-30
|
||||
scope: t2g_front_passenger_seat_heating_and_ventilation
|
||||
hardware_execution_allowed: true
|
||||
bit_numbering: lsb0
|
||||
byte_order: intel_little_endian
|
||||
limitations:
|
||||
- Only the driver-seat backrest heater and ventilation fan are implemented by the verified demos.
|
||||
- The two demos are standalone firmware images; use one board per profile or a reviewed combined image.
|
||||
- Product deployment still requires command timeout, E2E protection and calibrated thermal/current limits.
|
||||
- The verified physical heater and fan are exposed only as the logical front-passenger seat.
|
||||
- Driver_Seat_* remains the immutable firmware CAN signal name; it is not a frontend target.
|
||||
- Heating and ventilation run together in the repository Seat_Controller_demo image.
|
||||
- Bench ADC diagnostics do not replace production over-current protection or external power limiting.
|
||||
messages:
|
||||
- name: T2G_Seat_Thermal_Control_29E
|
||||
frame_id: 0x29E
|
||||
|
||||
@@ -3,8 +3,8 @@ kind: SignalMapping
|
||||
metadata:
|
||||
id: seat-t2g-bench-hardware-mapping
|
||||
name: Verified T2G seat bench mapping
|
||||
version: 1.1.0
|
||||
description: Mapping implemented by the unified Seat_Controller_demo IAR firmware.
|
||||
version: 3.0.0
|
||||
description: Front-passenger-only mapping implemented by the unified Seat_Controller_demo IAR firmware.
|
||||
spec:
|
||||
domain: seat
|
||||
platform: t2g_seat_bench
|
||||
@@ -12,74 +12,6 @@ spec:
|
||||
matrix_ref: seat-thermal-hardware-matrix.yaml
|
||||
status: hil_verified
|
||||
mappings:
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Ventilation_On, Driver_Seat_Ventilation_Mode, Driver_Seat_Ventilation_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Ventilation_Status
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Heating_On, Driver_Seat_Heating_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Heating_Status
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
@@ -136,7 +68,7 @@ spec:
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
- capability: seat.set_cushion_tilt
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
|
||||
@@ -3,8 +3,8 @@ kind: SignalMapping
|
||||
metadata:
|
||||
id: seat-reference-vehicle-mapping
|
||||
name: Seat mapping for reference vehicle
|
||||
version: 1.1.0
|
||||
description: Supplied thermal matrix plus explicitly synthetic motion mappings; virtual execution only.
|
||||
version: 1.2.0
|
||||
description: Front-passenger-only thermal and explicitly synthetic motion mappings; virtual execution only.
|
||||
spec:
|
||||
domain: seat
|
||||
platform: reference_vehicle
|
||||
@@ -12,20 +12,6 @@ spec:
|
||||
matrix_ref: seat-thermal-matrix.yaml
|
||||
status: draft
|
||||
mappings:
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Driver_Seat_Ventilation_S
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Driver_Seat_Heating_S
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
@@ -40,51 +26,6 @@ spec:
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Front_Passenger_Seat_Heating_S
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Longitudinal_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_cushion_tilt
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Cushion_Tilt_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Backrest_Recline_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Legrest_Height_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Legrest_Extension_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
|
||||
@@ -3,18 +3,18 @@ kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: seat-capabilities
|
||||
name: Seat capabilities
|
||||
version: 1.2.0
|
||||
version: 1.3.0
|
||||
spec:
|
||||
domain: seat
|
||||
capabilities:
|
||||
- name: seat.set_ventilation
|
||||
description: >-
|
||||
Move air through one seat surface to reduce local occupant heat and moisture; this does not
|
||||
control general cabin airflow. Turn it on or off for the driver or front passenger seat.
|
||||
Move air through the front-passenger seat surface to reduce local occupant heat and
|
||||
moisture; this does not control general cabin airflow. The current seat domain controls
|
||||
the front-passenger seat only.
|
||||
target_devices:
|
||||
- seat
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: state
|
||||
@@ -36,12 +36,11 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_heating
|
||||
description: >-
|
||||
Warm one seat surface for local occupant thermal comfort. Turn it on or off for the driver
|
||||
or front passenger seat.
|
||||
Warm the front-passenger seat surface for local occupant thermal comfort. The current seat
|
||||
domain controls the front-passenger seat only.
|
||||
target_devices:
|
||||
- seat
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: state
|
||||
@@ -63,7 +62,7 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_longitudinal_position
|
||||
description: >-
|
||||
Set one front seat's fore-aft rail position. For the selected Denza N7 profile, position_pct
|
||||
Set the front-passenger seat's fore-aft rail position. For the selected Denza N7 profile, position_pct
|
||||
is an absolute target: 0 is the forwardmost mechanical limit and 100 is the rearmost limit.
|
||||
Resolve relative requests such as "move back a little" against the current position supplied
|
||||
in runtime state: forward decreases the value and backward increases it. For a
|
||||
@@ -72,7 +71,6 @@ spec:
|
||||
target_devices:
|
||||
- seat_longitudinal_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
@@ -93,13 +91,12 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_cushion_tilt
|
||||
description: >-
|
||||
Set the seat-cushion pitch for one front seat. For the selected Denza N7 profile,
|
||||
Set the front-passenger seat-cushion pitch. For the selected Denza N7 profile,
|
||||
position_pct is absolute: 0 places the front edge at its highest limit and 100 lowers the
|
||||
front edge to its lowest tilt limit.
|
||||
target_devices:
|
||||
- seat_cushion_tilt_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
@@ -113,7 +110,7 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_backrest_recline
|
||||
description: >-
|
||||
Set the backrest recline of one front seat. Use either position_pct or angle_deg.
|
||||
Set the front-passenger backrest recline. Use either position_pct or angle_deg.
|
||||
position_pct is absolute: 0 is the most upright limit and 100 is the most reclined limit.
|
||||
angle_deg is the included angle between the seat-cushion surface and backrest centreline,
|
||||
from 95 to 163 degrees for the selected Denza N7 profile. "Backrest backward" means a
|
||||
@@ -122,7 +119,6 @@ spec:
|
||||
target_devices:
|
||||
- seat_backrest_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
@@ -150,16 +146,15 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_legrest_height
|
||||
description: >-
|
||||
Raise or lower one front seat's powered leg rest. Use either position_pct or angle_deg.
|
||||
Raise or lower the front-passenger seat's powered leg rest. Use either position_pct or angle_deg.
|
||||
position_pct is absolute: 0 is fully lowered and 100 is fully raised. angle_deg is the
|
||||
included angle between the cushion surface and leg-rest surface, from 95 to 173 degrees for
|
||||
included angle between the cushion surface and leg-rest surface, from 95 to 167 degrees for
|
||||
the selected Denza N7 profile. For relative physical angle requests, angle_delta_deg may be
|
||||
supplied and the runtime converts it to an absolute position_pct before CAN compilation. This
|
||||
axis does not change leg-rest extension.
|
||||
target_devices:
|
||||
- seat_legrest_lift_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
@@ -174,7 +169,7 @@ spec:
|
||||
unit: degree
|
||||
required: false
|
||||
minimum: 95
|
||||
maximum: 173
|
||||
maximum: 167
|
||||
description: Exact included angle between the cushion surface and leg-rest surface.
|
||||
- name: angle_delta_deg
|
||||
type: number
|
||||
@@ -187,12 +182,11 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_legrest_extension
|
||||
description: >-
|
||||
Extend or retract one front seat's powered leg rest. position_pct is absolute: 0 is fully
|
||||
Extend or retract the front-passenger seat's powered leg rest. position_pct is absolute: 0 is fully
|
||||
retracted and 100 is fully extended. This axis does not change leg-rest height.
|
||||
target_devices:
|
||||
- seat_legrest_extension_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
@@ -206,13 +200,12 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.save_posture_preset
|
||||
description: >-
|
||||
Save the current five-axis posture of one front seat as a conversation-owned named preset.
|
||||
Save the current five-axis posture of the front-passenger seat as a conversation-owned named preset.
|
||||
This is a control-plane operation and does not emit a CAN command. Use only when the user
|
||||
explicitly asks to save, remember or overwrite the current posture.
|
||||
target_devices:
|
||||
- seat_preset_store
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: preset_name
|
||||
@@ -223,13 +216,12 @@ spec:
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.apply_posture_preset
|
||||
description: >-
|
||||
Apply a previously saved five-axis posture preset to one front seat. The runtime expands
|
||||
Apply a previously saved five-axis posture preset to the front-passenger seat. The runtime expands
|
||||
the stored FunctionPreset into the registered motion capabilities before CAN compilation.
|
||||
Use this capability only when the named preset exists in runtime conversation context.
|
||||
target_devices:
|
||||
- seat_preset_store
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: preset_name
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Planned Seat Capabilities
|
||||
|
||||
零重力姿态和车型级自定义角度仍不属于实车可执行交付。现有 Excel 只覆盖主/副驾通风和加热相关信号,没有滑轨、靠背、坐垫或腿托控制信号,也没有运动学标定。
|
||||
零重力姿态和车型级自定义角度仍不属于参考车型矩阵的实车可执行交付。当前产品边界只暴露副驾座椅能力;原始 Excel 中的主驾信号仅作为历史源数据保留,不再注册为可执行目标。
|
||||
|
||||
当前已使用 `seat-motion-virtual-matrix.yaml` 提供五轴 `0-100%` 虚构位置能力,用于模型理解和端到端 dry-run 验证。靠背/坐垫 `95–140°` 和坐垫/腿托 `95–160°` 采用显式 synthetic 线性标定,仅用于验证精确夹角语义、配置生成和孪生展示。该文件不是 DBC,不提供真实物理毫米/角度转换,也不具备防夹、电流、占位、车速或挡位互锁。
|
||||
|
||||
|
||||
Executable → Regular
+6
-7
@@ -1,19 +1,19 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: seat-driver-ventilation
|
||||
name: Driver seat ventilation
|
||||
id: seat-passenger-ventilation
|
||||
name: Front passenger seat ventilation
|
||||
version: 1.0.0
|
||||
description: Binary ventilation command supported by the partial CAN matrix.
|
||||
description: Binary ventilation command for the front-passenger-only seat domain.
|
||||
spec:
|
||||
domain: seat
|
||||
operations:
|
||||
- id: driver_ventilation_on
|
||||
- id: passenger_ventilation_on
|
||||
capability: seat.set_ventilation
|
||||
target:
|
||||
device: seat
|
||||
zone: front_left
|
||||
instance: driver
|
||||
zone: front_right
|
||||
instance: passenger
|
||||
parameters:
|
||||
state: "on"
|
||||
timeout_ms: 3000
|
||||
@@ -21,4 +21,3 @@ spec:
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
"required": ["position_pct"],
|
||||
"properties": {
|
||||
"position_pct": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"angle_deg": {"type": "number", "minimum": 95, "maximum": 173}
|
||||
"angle_deg": {"type": "number", "minimum": 95, "maximum": 167}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,7 +19,6 @@ MOTION_AXIS_BITS = {
|
||||
"seat.set_legrest_extension": 4,
|
||||
}
|
||||
MOTION_STATUS_MESSAGES = {
|
||||
"front_left": "Driver_Seat_Motion_Status_558",
|
||||
"front_right": "Passenger_Seat_Motion_Status_559",
|
||||
}
|
||||
|
||||
|
||||
@@ -9,14 +9,10 @@ from .matrix import SeatCanMatrix
|
||||
|
||||
|
||||
COMMAND_TO_FEEDBACK = {
|
||||
"Driver_Seat_Ventilation_S": "Pilot_Seat_Vtilat_Stat_S",
|
||||
"Driver_Seat_Heating_S": "Pilot_Seat_Heat_Stat_S",
|
||||
"Front_Passenger_Seat_Ventilation_S": "Copilot_Seat_Vtilat_Stat_4FA_S",
|
||||
"Front_Passenger_Seat_Heating_S": "Copilot_Seat_Heat_Stat_4FA_S",
|
||||
}
|
||||
OPERATION_TO_COMMAND = {
|
||||
("front_left", "seat.set_ventilation"): "Driver_Seat_Ventilation_S",
|
||||
("front_left", "seat.set_heating"): "Driver_Seat_Heating_S",
|
||||
("front_right", "seat.set_ventilation"): "Front_Passenger_Seat_Ventilation_S",
|
||||
("front_right", "seat.set_heating"): "Front_Passenger_Seat_Heating_S",
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ from typing import Any
|
||||
|
||||
|
||||
TARGETS = {
|
||||
"driver": {"device": "seat", "zone": "front_left", "instance": "driver"},
|
||||
"passenger": {"device": "seat", "zone": "front_right", "instance": "passenger"},
|
||||
}
|
||||
CAPABILITIES = {
|
||||
|
||||
@@ -16,7 +16,7 @@ STATE_VALUES = {"on": 1, "off": 2}
|
||||
class T2GSeatCompiler:
|
||||
"""Deterministic compiler for the HIL-verified T2G seat bench protocol."""
|
||||
|
||||
version = "1.3.0"
|
||||
version = "2.0.0"
|
||||
|
||||
def __init__(self, mapping_path: Path, calibration_path: Path):
|
||||
self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8"))
|
||||
@@ -35,7 +35,7 @@ class T2GSeatCompiler:
|
||||
actuator_state: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
operations = function_config["spec"]["operations"]
|
||||
self._validate_shared_physical_targets(operations)
|
||||
self._validate_passenger_targets(operations)
|
||||
thermal_operations = [
|
||||
item
|
||||
for item in operations
|
||||
@@ -86,7 +86,7 @@ class T2GSeatCompiler:
|
||||
operations: list[dict[str, Any]],
|
||||
actuator_state: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
state = actuator_state.get("driver") or actuator_state.get("passenger", {})
|
||||
state = actuator_state.get("passenger", {})
|
||||
desired = {
|
||||
"ventilation": dict(state.get("ventilation", {"state": "off", "level": 0})),
|
||||
"heating": dict(state.get("heating", {"state": "off", "level": 0})),
|
||||
@@ -159,7 +159,10 @@ class T2GSeatCompiler:
|
||||
raise ValueError(
|
||||
f"T2G IAR firmware has no calibrated actuator for {capability}"
|
||||
) from exc
|
||||
maximum = int(axis["maximum_value_x100"])
|
||||
maximum = int(self.protocol["normalized_position_max"])
|
||||
absolute_timeout_ms = int(
|
||||
self.protocol["absolute_position_timeout_ms"]
|
||||
)
|
||||
position_pct = int(operation["parameters"]["position_pct"])
|
||||
target = round(maximum * position_pct / 100)
|
||||
tolerance = max(1, round(maximum / 100))
|
||||
@@ -195,20 +198,29 @@ class T2GSeatCompiler:
|
||||
"Motor_Selector": int(axis["motor_selector"]),
|
||||
"Opcode": int(self.protocol["absolute_position_opcode"]),
|
||||
"Param_A": target,
|
||||
"Param_B": int(self.protocol["default_duty_percent"]),
|
||||
# Opcode 1 Param_B is a timeout override, not duty.
|
||||
# The explicit calibrated value covers a full M0 stroke;
|
||||
# the legacy per-axis default can stop it at about 79%.
|
||||
"Param_B": absolute_timeout_ms,
|
||||
"Protocol_Version": int(self.protocol["version"]),
|
||||
# Allocated immediately before hardware transmission. Zero keeps
|
||||
# the compiled plan deterministic and is never put on the bus.
|
||||
"Sequence": 0,
|
||||
},
|
||||
"send_offset_ms": (command_offset + index) * 100,
|
||||
"send_offset_ms": (
|
||||
(command_offset + index)
|
||||
* int(self.protocol["inter_axis_settle_ms"])
|
||||
),
|
||||
"completion": {
|
||||
"message": "T2G_Seat_Move_Motor_Status_302",
|
||||
"signal": "Value_X100",
|
||||
"operator": "within",
|
||||
"value": target,
|
||||
"tolerance": tolerance,
|
||||
"timeout_ms": int(operation.get("timeout_ms", 8000)),
|
||||
"timeout_ms": max(
|
||||
int(operation.get("timeout_ms", 0)),
|
||||
absolute_timeout_ms + 3000,
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -261,38 +273,36 @@ class T2GSeatCompiler:
|
||||
"Motor_Selector": int(axis["motor_selector"]),
|
||||
"Opcode": int(self.protocol["timed_duty_opcode"]),
|
||||
"Param_A": duration_ms,
|
||||
"Param_B": int(self.protocol["default_duty_percent"]),
|
||||
"Param_B": int(self.protocol["default_duty_percent"])
|
||||
* int(self.protocol["timed_duty_level_scale"]),
|
||||
"Protocol_Version": int(self.protocol["version"]),
|
||||
"Sequence": 0,
|
||||
},
|
||||
"send_offset_ms": command_index * 100,
|
||||
"send_offset_ms": (
|
||||
command_index * int(self.protocol["inter_axis_settle_ms"])
|
||||
),
|
||||
"completion": {
|
||||
"message": "T2G_Seat_Move_Motor_Status_302",
|
||||
"signal": "Task_State",
|
||||
"operator": "eq",
|
||||
"value": 8,
|
||||
"value": 6,
|
||||
"timeout_ms": timeout_ms,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _validate_shared_physical_targets(
|
||||
def _validate_passenger_targets(
|
||||
operations: list[dict[str, Any]],
|
||||
) -> None:
|
||||
requested: dict[str, dict[str, Any]] = {}
|
||||
for operation in operations:
|
||||
capability = operation["capability"]
|
||||
request = {
|
||||
"parameters": dict(operation.get("parameters", {})),
|
||||
"motion": dict(operation["motion"]) if operation.get("motion") else None,
|
||||
}
|
||||
previous = requested.get(capability)
|
||||
if previous is not None and previous != request:
|
||||
target = operation.get("target", {})
|
||||
if (
|
||||
target.get("instance") != "passenger"
|
||||
or target.get("zone") != "front_right"
|
||||
):
|
||||
raise ValueError(
|
||||
"driver and passenger aliases request conflicting values for "
|
||||
f"the same physical capability {capability}"
|
||||
"T2G hardware accepts only passenger/front_right seat operations"
|
||||
)
|
||||
requested[capability] = request
|
||||
|
||||
def _require_supported(self, operation: dict[str, Any]) -> None:
|
||||
key = (operation["capability"], operation["target"]["zone"])
|
||||
|
||||
@@ -18,13 +18,15 @@ MOTOR_RESULT_NAMES = {
|
||||
0: "NONE",
|
||||
1: "RUNNING",
|
||||
2: "OK",
|
||||
3: "STOPPED",
|
||||
4: "STALL",
|
||||
5: "ENDPOINT",
|
||||
3: "STOPPED_BY_CMD",
|
||||
4: "ENDPOINT_REACHED",
|
||||
5: "STALL",
|
||||
6: "TIMEOUT",
|
||||
7: "DRIVER_FAULT",
|
||||
8: "INVALID",
|
||||
9: "CALIBRATION_FAILED",
|
||||
8: "HALL_FAULT",
|
||||
9: "INVALID_CMD",
|
||||
10: "INTERLOCKED",
|
||||
11: "CALIBRATION_FAILED",
|
||||
}
|
||||
|
||||
MOTOR_EVENT_NAMES = {
|
||||
@@ -39,6 +41,26 @@ MOTOR_EVENT_NAMES = {
|
||||
8: "CALIBRATION_FAILED",
|
||||
}
|
||||
|
||||
ENDPOINT_RECOVERY_PROBE_DURATION_MS = 500
|
||||
ENDPOINT_RECOVERY_PROBE_LEVEL_X10 = 500
|
||||
ENDPOINT_RECOVERY_MAX_STATIONARY_HALL_DELTA = 2
|
||||
ENDPOINT_RECOVERY_MAX_NEAR_HALL = 8
|
||||
ENDPOINT_RECOVERY_MAX_NEAR_HALL_PERCENT = 15
|
||||
|
||||
PHYSICAL_MOTOR_AXES = {
|
||||
0: "longitudinal",
|
||||
1: "cushion_tilt",
|
||||
2: "backrest_recline",
|
||||
3: "legrest_height",
|
||||
}
|
||||
PHYSICAL_AXIS_CAPABILITIES = {
|
||||
axis: capability
|
||||
for capability, axis in MOTION_CAPABILITY_TO_AXIS.items()
|
||||
if axis in PHYSICAL_MOTOR_AXES.values()
|
||||
}
|
||||
MOTION_ACTIVE_TASK_STATES = {1, 2, 3, 4, 5}
|
||||
MOTION_FAULT_RESULTS = {5, 6, 7, 8, 9, 10, 11}
|
||||
|
||||
|
||||
class HardwareExecutionError(RuntimeError):
|
||||
"""Raised for rejected, faulted or timed-out physical operations."""
|
||||
@@ -68,6 +90,10 @@ class HardwareSeatExecutor:
|
||||
thermal_timeout_ms: int = 2_000,
|
||||
ack_timeout_ms: int = 1_500,
|
||||
axis_maximum_x100: dict[str, int] | None = None,
|
||||
motor_max_hall: dict[int, int] | None = None,
|
||||
stop_all_motor_selector: int = 0,
|
||||
auto_home: bool = False,
|
||||
home_timeout_s: int = 120,
|
||||
firmware_guard: T2GFirmwareGuard | None = None,
|
||||
):
|
||||
self.matrix = matrix
|
||||
@@ -75,7 +101,14 @@ class HardwareSeatExecutor:
|
||||
self.thermal_timeout_ms = thermal_timeout_ms
|
||||
self.ack_timeout_ms = ack_timeout_ms
|
||||
self.axis_maximum_x100 = dict(axis_maximum_x100 or {})
|
||||
self.motor_max_hall = dict(motor_max_hall or {})
|
||||
if stop_all_motor_selector not in range(4):
|
||||
raise ValueError("stop_all_motor_selector must be between 0 and 3")
|
||||
self.stop_all_motor_selector = stop_all_motor_selector
|
||||
self.auto_home = auto_home
|
||||
self.home_timeout_s = home_timeout_s
|
||||
self.firmware_guard = firmware_guard
|
||||
self._current_samples: dict[int, list[dict[str, Any]]] = {}
|
||||
|
||||
@classmethod
|
||||
def _next_sequence(cls) -> int:
|
||||
@@ -100,6 +133,8 @@ class HardwareSeatExecutor:
|
||||
has_motion = any(item["message"] == "T2G_Seat_Move_Command_300" for item in commands)
|
||||
bus: CanBus | None = None
|
||||
motion_armed = False
|
||||
physical_state: dict[str, Any] | None = None
|
||||
self._current_samples = {}
|
||||
|
||||
with self._adapter_lock:
|
||||
try:
|
||||
@@ -113,7 +148,7 @@ class HardwareSeatExecutor:
|
||||
"message": (
|
||||
"repository Seat_Controller_demo was programmed and verified"
|
||||
if firmware_telemetry.get("programmed_this_request")
|
||||
else "repository Seat_Controller_demo identity was verified"
|
||||
else "repository Seat_Controller_demo protocol signature was verified"
|
||||
),
|
||||
"telemetry": {
|
||||
**firmware_telemetry,
|
||||
@@ -177,10 +212,23 @@ class HardwareSeatExecutor:
|
||||
)
|
||||
finally:
|
||||
if bus is not None:
|
||||
try:
|
||||
physical_state = self._capture_physical_state(bus, results)
|
||||
except Exception: # noqa: BLE001 - do not mask the command outcome
|
||||
physical_state = self._physical_state_from_results(results)
|
||||
bus.close()
|
||||
|
||||
succeeded = all(item["status"] == "succeeded" for item in results)
|
||||
request_id = function_config["metadata"]["request_id"]
|
||||
spec: dict[str, Any] = {
|
||||
"mode": "hil",
|
||||
"status": "succeeded" if succeeded else "failed",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
}
|
||||
if physical_state is not None:
|
||||
spec["physical_state"] = physical_state
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "ExecutionReport",
|
||||
@@ -191,15 +239,166 @@ class HardwareSeatExecutor:
|
||||
"request_id": request_id,
|
||||
"labels": {"simulation": "false", "feedback": "physical_can"},
|
||||
},
|
||||
"spec": {
|
||||
"mode": "hil",
|
||||
"status": "succeeded" if succeeded else "failed",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
},
|
||||
"spec": spec,
|
||||
}
|
||||
|
||||
def read_physical_state(self) -> dict[str, Any]:
|
||||
"""Read the passenger seat status without transmitting a CAN command."""
|
||||
|
||||
with self._adapter_lock:
|
||||
bus = self.bus_factory()
|
||||
try:
|
||||
state = self._capture_physical_state(bus, [])
|
||||
finally:
|
||||
bus.close()
|
||||
if not state.get("position") and not any(
|
||||
state.get(feature) for feature in ("ventilation", "heating")
|
||||
):
|
||||
raise HardwareExecutionError(
|
||||
"no physical T2G seat status was received from CAN 0x302/0x4FA"
|
||||
)
|
||||
return state
|
||||
|
||||
def _capture_physical_state(
|
||||
self,
|
||||
bus: CanBus,
|
||||
results: list[dict[str, Any]],
|
||||
) -> dict[str, Any]:
|
||||
state = self._physical_state_from_results(results)
|
||||
deadline = time.monotonic() + max(
|
||||
self.thermal_timeout_ms,
|
||||
self.ack_timeout_ms,
|
||||
1_000,
|
||||
) / 1000
|
||||
while not self._physical_state_complete(state):
|
||||
remaining_ms = round((deadline - time.monotonic()) * 1000)
|
||||
if remaining_ms <= 0:
|
||||
break
|
||||
frame = bus.receive(remaining_ms)
|
||||
if frame is None:
|
||||
break
|
||||
self._observe_auxiliary_frame(frame)
|
||||
if frame.arbitration_id == 0x302 and len(frame.data) >= 8:
|
||||
values = self.matrix.decode_by_frame_id(0x302, frame.data)
|
||||
self._record_motion_state(state, values, frame)
|
||||
elif frame.arbitration_id == 0x4FA and len(frame.data) >= 8:
|
||||
values = self.matrix.decode_by_frame_id(0x4FA, frame.data)
|
||||
self._record_thermal_state(state, values, frame)
|
||||
state["captured_at"] = _now()
|
||||
state["source"] = "physical_can"
|
||||
state["target"] = {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger",
|
||||
}
|
||||
state["complete"] = self._physical_state_complete(state)
|
||||
state["missing"] = self._physical_state_missing(state)
|
||||
return state
|
||||
|
||||
def _physical_state_from_results(
|
||||
self, results: list[dict[str, Any]]
|
||||
) -> dict[str, Any]:
|
||||
state: dict[str, Any] = {"position": {}}
|
||||
for item in results:
|
||||
telemetry = item.get("telemetry", {})
|
||||
if item.get("status") != "succeeded":
|
||||
continue
|
||||
axis = telemetry.get("motion_axis")
|
||||
position = telemetry.get("effective_position_pct")
|
||||
if axis in PHYSICAL_AXIS_CAPABILITIES and position is not None:
|
||||
state["position"][str(axis)] = {
|
||||
"position_pct": int(position),
|
||||
"position_valid": bool(telemetry.get("position_valid", True)),
|
||||
"motion_state": "stopped",
|
||||
"fault": False,
|
||||
"feedback_frame": telemetry.get("feedback_frame"),
|
||||
}
|
||||
feature = str(telemetry.get("feedback_signal", ""))
|
||||
raw = telemetry.get("feedback_value")
|
||||
if raw is None:
|
||||
continue
|
||||
if feature == "Driver_Seat_Heating_Status":
|
||||
self._set_thermal_feature(state, "heating", int(raw))
|
||||
elif feature == "Driver_Seat_Ventilation_Status":
|
||||
self._set_thermal_feature(state, "ventilation", int(raw))
|
||||
return state
|
||||
|
||||
def _record_motion_state(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
values: dict[str, int],
|
||||
frame: CanFrame,
|
||||
) -> None:
|
||||
motor = int(values["Motor_Sub_ID"])
|
||||
axis = PHYSICAL_MOTOR_AXES.get(motor)
|
||||
if axis is None:
|
||||
return
|
||||
capability = PHYSICAL_AXIS_CAPABILITIES[axis]
|
||||
maximum = max(1, int(self.axis_maximum_x100.get(capability, 10_000)))
|
||||
task_state = int(values["Task_State"])
|
||||
result = int(values["Result"])
|
||||
position_pct = round(int(values["Value_X100"]) * 100 / maximum)
|
||||
state.setdefault("position", {})[axis] = {
|
||||
"position_pct": max(0, min(100, position_pct)),
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"motion_state": (
|
||||
"moving" if task_state in MOTION_ACTIVE_TASK_STATES else "stopped"
|
||||
),
|
||||
"fault": task_state == 7 or result in MOTION_FAULT_RESULTS,
|
||||
"task_state": task_state,
|
||||
"motor_result": result,
|
||||
"motor_selector": motor,
|
||||
"value_x100": int(values["Value_X100"]),
|
||||
"feedback_frame": frame.public(),
|
||||
}
|
||||
|
||||
def _record_thermal_state(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
values: dict[str, int],
|
||||
frame: CanFrame,
|
||||
) -> None:
|
||||
for feature, signal in (
|
||||
("ventilation", "Driver_Seat_Ventilation_Status"),
|
||||
("heating", "Driver_Seat_Heating_Status"),
|
||||
):
|
||||
raw = int(values[signal])
|
||||
if 1 <= raw <= 4:
|
||||
self._set_thermal_feature(state, feature, raw)
|
||||
state[feature]["feedback_frame"] = frame.public()
|
||||
|
||||
@staticmethod
|
||||
def _set_thermal_feature(
|
||||
state: dict[str, Any], feature: str, raw: int
|
||||
) -> None:
|
||||
state[feature] = {
|
||||
"state": "on" if raw >= 2 else "off",
|
||||
"level": max(0, min(3, raw - 1)),
|
||||
"feedback_value": raw,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _physical_state_complete(state: dict[str, Any]) -> bool:
|
||||
return (
|
||||
set(state.get("position", {})) == set(PHYSICAL_MOTOR_AXES.values())
|
||||
and isinstance(state.get("ventilation"), dict)
|
||||
and isinstance(state.get("heating"), dict)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _physical_state_missing(state: dict[str, Any]) -> list[str]:
|
||||
missing = [
|
||||
axis
|
||||
for axis in PHYSICAL_MOTOR_AXES.values()
|
||||
if axis not in state.get("position", {})
|
||||
]
|
||||
missing.extend(
|
||||
feature
|
||||
for feature in ("ventilation", "heating")
|
||||
if not isinstance(state.get(feature), dict)
|
||||
)
|
||||
return missing
|
||||
|
||||
@staticmethod
|
||||
def _first_unreported_operation(
|
||||
operations: dict[str, dict[str, Any]], results: list[dict[str, Any]]
|
||||
@@ -232,27 +431,102 @@ class HardwareSeatExecutor:
|
||||
raise HardwareExecutionError(
|
||||
f"timed out after {timeout_ms} ms waiting for CAN 0x{arbitration_id:03X}"
|
||||
)
|
||||
self._observe_auxiliary_frame(frame)
|
||||
if frame.arbitration_id != arbitration_id or len(frame.data) < 8:
|
||||
continue
|
||||
values = self.matrix.decode_by_frame_id(arbitration_id, frame.data)
|
||||
if predicate(values, frame):
|
||||
return values, frame
|
||||
|
||||
def _observe_auxiliary_frame(self, frame: CanFrame) -> None:
|
||||
"""Retain current telemetry that would otherwise be skipped while waiting.
|
||||
|
||||
Motor 0x304 is cyclic and interleaved with the 0x301/0x302/0x303
|
||||
protocol frames. A STALL decision requires sustained high current and
|
||||
low Hall activity, but after STOP_ALL the public current has already
|
||||
fallen back to its idle baseline. Keeping the samples observed during
|
||||
the command preserves the evidence needed to distinguish a real loaded
|
||||
stall from a stale/incorrect event without changing motor behaviour.
|
||||
"""
|
||||
|
||||
if frame.arbitration_id != 0x304 or len(frame.data) < 8:
|
||||
return
|
||||
values = self.matrix.decode_by_frame_id(0x304, frame.data)
|
||||
motor = int(values["Motor_Sub_ID"])
|
||||
if motor not in PHYSICAL_MOTOR_AXES:
|
||||
return
|
||||
raw = int(values["Current_Raw"])
|
||||
baseline = int(values["Current_Baseline_Raw"])
|
||||
sample = {
|
||||
"current_raw": raw,
|
||||
"current_baseline_raw": baseline,
|
||||
"current_delta_raw": max(0, raw - baseline),
|
||||
"stall_current_delta_threshold_raw": int(
|
||||
values["Stall_Current_Delta_Threshold_Raw"]
|
||||
),
|
||||
"current_sample_valid": bool(values["Current_Sample_Valid"]),
|
||||
"current_baseline_valid": bool(values["Current_Baseline_Valid"]),
|
||||
"high_current": bool(values["High_Current"]),
|
||||
"hall_activity_low": bool(values["Hall_Activity_Low"]),
|
||||
"current_calibrating": bool(values["Current_Calibrating"]),
|
||||
"frame": frame.public(),
|
||||
}
|
||||
samples = self._current_samples.setdefault(motor, [])
|
||||
samples.append(sample)
|
||||
# A normal position command is at most tens of seconds. Bound the
|
||||
# retained diagnostics anyway so repeated state reads cannot grow it.
|
||||
if len(samples) > 512:
|
||||
del samples[:-512]
|
||||
|
||||
def _reset_current_samples(self, motor: int) -> None:
|
||||
self._current_samples[motor] = []
|
||||
|
||||
def _current_diagnostic_summary(self, motor: int) -> dict[str, Any]:
|
||||
samples = self._current_samples.get(motor, [])
|
||||
if not samples:
|
||||
return {
|
||||
"sample_count": 0,
|
||||
"high_current_seen": False,
|
||||
"low_hall_seen": False,
|
||||
"stall_overlap_seen": False,
|
||||
"latest": None,
|
||||
}
|
||||
valid = [item for item in samples if item["current_sample_valid"]]
|
||||
candidates = valid or samples
|
||||
peak = max(candidates, key=lambda item: item["current_delta_raw"])
|
||||
return {
|
||||
"sample_count": len(samples),
|
||||
"valid_sample_count": len(valid),
|
||||
"max_current_raw": max(item["current_raw"] for item in candidates),
|
||||
"max_current_delta_raw": peak["current_delta_raw"],
|
||||
"high_current_seen": any(item["high_current"] for item in candidates),
|
||||
"low_hall_seen": any(item["hall_activity_low"] for item in candidates),
|
||||
"stall_overlap_seen": any(
|
||||
item["high_current"] and item["hall_activity_low"]
|
||||
for item in candidates
|
||||
),
|
||||
"peak": peak,
|
||||
"latest": samples[-1],
|
||||
}
|
||||
|
||||
def _wait_ack(
|
||||
self, bus: CanBus, sequence: int, opcode: int
|
||||
self, bus: CanBus, sequence: int, opcode: int, *, expected_ack: int = 1
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
def matches(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Last_Sequence"] != sequence or values["Last_Opcode"] != opcode:
|
||||
return False
|
||||
if values["Ack"] != 1:
|
||||
if values["Ack"] != expected_ack:
|
||||
raise HardwareExecutionError(
|
||||
"T2G movement command rejected: "
|
||||
f"ack={values['Ack']}, reason={values['Reject_Reason']}, "
|
||||
f"safety_fault={values['Safety_Fault']}"
|
||||
f"system_state={values['System_State']}, "
|
||||
f"fault_summary={values['Fault_Summary']}"
|
||||
)
|
||||
if values["Safety_Inhibit"] or values["Safety_Fault"]:
|
||||
if values["Safety_Inhibit"] or values["Fault_Summary"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G movement safety inhibit/fault: {values['Safety_Fault']}"
|
||||
"T2G movement safety inhibit/fault: "
|
||||
f"state={values['System_State']}, "
|
||||
f"faulted_mask=0x{values['Faulted_Mask']:X}"
|
||||
)
|
||||
return True
|
||||
|
||||
@@ -262,7 +536,7 @@ class HardwareSeatExecutor:
|
||||
sequence = self._next_sequence()
|
||||
signals = {
|
||||
"Opcode": 9,
|
||||
"Motor_Selector": 3,
|
||||
"Motor_Selector": self.stop_all_motor_selector,
|
||||
"Direction": 0,
|
||||
"Protocol_Version": 1,
|
||||
"Sequence": sequence,
|
||||
@@ -272,7 +546,7 @@ class HardwareSeatExecutor:
|
||||
sent = self._send_encoded(bus, "T2G_Seat_Move_Command_300", signals)
|
||||
if not require_ack:
|
||||
return sent, None
|
||||
_, ack = self._wait_ack(bus, sequence, 9)
|
||||
_, ack = self._wait_ack(bus, sequence, 9, expected_ack=4)
|
||||
return sent, ack
|
||||
|
||||
def _best_effort_stop(self, bus: CanBus) -> None:
|
||||
@@ -368,38 +642,61 @@ class HardwareSeatExecutor:
|
||||
bus, command, operations, signals, motor
|
||||
)
|
||||
|
||||
target = int(command["completion"]["value"])
|
||||
tolerance = int(command["completion"].get("tolerance", 0))
|
||||
axis_maximum = self.axis_maximum_x100.get(operation["capability"])
|
||||
preflight_values, preflight_frame = self._motor_status_snapshot(bus, motor)
|
||||
baseline: dict[str, int] | None = None
|
||||
baseline_frame: CanFrame | None = None
|
||||
baseline_error: str | None = None
|
||||
position_recovery: dict[str, Any] | None = None
|
||||
if not preflight_values["Position_Valid"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} absolute-position command blocked before send: "
|
||||
"controller Position_Valid=0. Run the explicit endpoint-calibration "
|
||||
"maintenance procedure with the bench clear and emergency stop ready, "
|
||||
"or use a small relative movement request.",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_valid": False,
|
||||
"calibration_required": True,
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
},
|
||||
)
|
||||
if not self.auto_home:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} absolute-position command blocked before send: "
|
||||
"controller Position_Valid=0. Enable the separately gated known-range "
|
||||
"homing workflow only with the bench clear and emergency stop ready.",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_valid": False,
|
||||
"known_range_home_required": True,
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
},
|
||||
)
|
||||
position_recovery = self._home_known_range(bus, motor)
|
||||
preflight_values, preflight_frame = self._motor_status_snapshot(bus, motor)
|
||||
if not preflight_values["Position_Valid"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} remained position-invalid after known-range homing",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_recovery": position_recovery,
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
},
|
||||
)
|
||||
|
||||
# Capture Hall telemetry after any required homing. It is required
|
||||
# for endpoint recovery at 0% and makes non-zero failures diagnosable.
|
||||
if opcode == 1:
|
||||
try:
|
||||
baseline, baseline_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
except HardwareExecutionError as exc:
|
||||
baseline_error = str(exc)
|
||||
|
||||
sequence = self._next_sequence()
|
||||
signals["Sequence"] = sequence
|
||||
self._reset_current_samples(motor)
|
||||
command_frame = self._send_encoded(bus, command["message"], signals)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, opcode)
|
||||
target = int(command["completion"]["value"])
|
||||
tolerance = int(command["completion"].get("tolerance", 0))
|
||||
|
||||
def complete(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Motor_Sub_ID"] != motor or values["Motor_Sequence"] != sequence:
|
||||
return False
|
||||
task_state = values["Task_State"]
|
||||
result = values["Result"]
|
||||
if task_state == 9 or result not in {1, 2}:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} failed: task_state={task_state}, result={result}"
|
||||
)
|
||||
if task_state != 8 or result != 2:
|
||||
if task_state == 7 or result not in {1, 2}:
|
||||
return True
|
||||
if task_state != 6 or result != 2:
|
||||
return False
|
||||
if not values["Position_Valid"]:
|
||||
raise HardwareExecutionError(
|
||||
@@ -418,8 +715,151 @@ class HardwareSeatExecutor:
|
||||
int(command["completion"]["timeout_ms"]),
|
||||
complete,
|
||||
)
|
||||
task_state = int(values["Task_State"])
|
||||
result = int(values["Result"])
|
||||
far_endpoint_tolerance_acceptance = bool(
|
||||
opcode == 1
|
||||
and axis_maximum is not None
|
||||
and target == axis_maximum
|
||||
and task_state == 7
|
||||
and result in {5, 6}
|
||||
and values["Position_Valid"]
|
||||
and abs(int(values["Value_X100"]) - target) <= tolerance
|
||||
)
|
||||
terminal_tolerance_acceptance: dict[str, Any] | None = None
|
||||
if task_state != 6 or result != 2:
|
||||
if far_endpoint_tolerance_acceptance:
|
||||
terminal_tolerance_acceptance = {
|
||||
"mode": "verified_far_endpoint_within_target_tolerance",
|
||||
"reported_motor_result": MOTOR_RESULT_NAMES.get(
|
||||
result, f"UNKNOWN_{result}"
|
||||
),
|
||||
"effective_value_x100": int(values["Value_X100"]),
|
||||
"requested_value_x100": target,
|
||||
"position_tolerance_x100": tolerance,
|
||||
}
|
||||
else:
|
||||
diagnostic: dict[str, int] | None = None
|
||||
diagnostic_frame: CanFrame | None = None
|
||||
diagnostic_error: str | None = None
|
||||
try:
|
||||
diagnostic, diagnostic_frame = self._motor_diagnostic_snapshot(
|
||||
bus, motor
|
||||
)
|
||||
except HardwareExecutionError as exc:
|
||||
diagnostic_error = str(exc)
|
||||
|
||||
hall_before = int(baseline["Hall_Count"]) if baseline else None
|
||||
hall_after = int(diagnostic["Hall_Count"]) if diagnostic else None
|
||||
hall_delta = (
|
||||
abs(hall_after - hall_before)
|
||||
if hall_before is not None and hall_after is not None
|
||||
else None
|
||||
)
|
||||
calibrated_max_hall = int(self.motor_max_hall.get(motor, 0))
|
||||
endpoint_recovery_max_near_hall = ENDPOINT_RECOVERY_MAX_NEAR_HALL
|
||||
if calibrated_max_hall > 0:
|
||||
endpoint_recovery_max_near_hall = max(
|
||||
ENDPOINT_RECOVERY_MAX_NEAR_HALL,
|
||||
(
|
||||
calibrated_max_hall
|
||||
* ENDPOINT_RECOVERY_MAX_NEAR_HALL_PERCENT
|
||||
+ 50
|
||||
)
|
||||
// 100,
|
||||
)
|
||||
endpoint_drift = bool(
|
||||
opcode == 1
|
||||
and target == 0
|
||||
and result in {5, 6}
|
||||
and values["Position_Valid"]
|
||||
and hall_after is not None
|
||||
and 0 <= hall_after <= endpoint_recovery_max_near_hall
|
||||
)
|
||||
if not endpoint_drift:
|
||||
result_name = MOTOR_RESULT_NAMES.get(result, f"UNKNOWN_{result}")
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} {result_name}: task_state={task_state}, "
|
||||
f"Hall={hall_before}->{hall_after}",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"motion_axis": MOTION_CAPABILITY_TO_AXIS[
|
||||
operation["capability"]
|
||||
],
|
||||
"task_state": task_state,
|
||||
"motor_result": result,
|
||||
"motor_result_name": result_name,
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"hall_before": hall_before,
|
||||
"hall_after": hall_after,
|
||||
"hall_delta": hall_delta,
|
||||
"endpoint_recovery_max_near_hall": (
|
||||
endpoint_recovery_max_near_hall
|
||||
),
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_before_frame": (
|
||||
baseline_frame.public() if baseline_frame else None
|
||||
),
|
||||
"diagnostic_frame": (
|
||||
diagnostic_frame.public() if diagnostic_frame else None
|
||||
),
|
||||
"diagnostic_read_error": diagnostic_error or baseline_error,
|
||||
"current_diagnostic": self._current_diagnostic_summary(motor),
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
away_probe = self._probe_away_from_near_endpoint(
|
||||
bus,
|
||||
motor,
|
||||
diagnostic,
|
||||
diagnostic_frame,
|
||||
)
|
||||
except HardwareExecutionError as exc:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} endpoint-drift recovery refused: {exc}",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"motion_axis": MOTION_CAPABILITY_TO_AXIS[
|
||||
operation["capability"]
|
||||
],
|
||||
"failure_category": "endpoint_recovery_probe_failed",
|
||||
"failed_absolute_command_frame": command_frame.public(),
|
||||
"failed_absolute_feedback_frame": feedback.public(),
|
||||
**exc.telemetry,
|
||||
},
|
||||
) from exc
|
||||
home = self._home_known_range(bus, motor)
|
||||
recovered_values, recovered_frame = self._motor_status_snapshot(bus, motor)
|
||||
if (
|
||||
not recovered_values["Position_Valid"]
|
||||
or recovered_values["Value_X100"] > tolerance
|
||||
):
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} endpoint re-home did not establish zero",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"away_probe": away_probe,
|
||||
"home": home,
|
||||
"recovered_status_frame": recovered_frame.public(),
|
||||
},
|
||||
)
|
||||
position_recovery = {
|
||||
"mode": "verified_endpoint_drift_rehome",
|
||||
"failed_absolute_hall_before": hall_before,
|
||||
"failed_absolute_hall_after": hall_after,
|
||||
"endpoint_recovery_max_near_hall": (
|
||||
endpoint_recovery_max_near_hall
|
||||
),
|
||||
"away_probe": away_probe,
|
||||
"home": home,
|
||||
}
|
||||
values = recovered_values
|
||||
feedback = recovered_frame
|
||||
axis = MOTION_CAPABILITY_TO_AXIS[operation["capability"]]
|
||||
maximum = self.axis_maximum_x100.get(operation["capability"])
|
||||
maximum = axis_maximum
|
||||
if maximum is None:
|
||||
requested_pct = int(operation["parameters"]["position_pct"])
|
||||
maximum = max(1, target * 100 // max(1, requested_pct))
|
||||
@@ -439,6 +879,8 @@ class HardwareSeatExecutor:
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"system_ack": ack_values["Ack"],
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
"position_recovery": position_recovery,
|
||||
"terminal_tolerance_acceptance": terminal_tolerance_acceptance,
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
@@ -462,6 +904,83 @@ class HardwareSeatExecutor:
|
||||
for item in operations
|
||||
]
|
||||
|
||||
def _probe_away_from_near_endpoint(
|
||||
self,
|
||||
bus: CanBus,
|
||||
motor: int,
|
||||
baseline: dict[str, int] | None,
|
||||
baseline_frame: CanFrame | None,
|
||||
) -> dict[str, Any]:
|
||||
if baseline is None or baseline_frame is None:
|
||||
raise HardwareExecutionError(
|
||||
"endpoint recovery requires a physical Hall baseline"
|
||||
)
|
||||
sequence = self._next_sequence()
|
||||
command = self._send_encoded(
|
||||
bus,
|
||||
"T2G_Seat_Move_Command_300",
|
||||
{
|
||||
"Opcode": 3,
|
||||
"Motor_Selector": motor,
|
||||
"Direction": 1,
|
||||
"Protocol_Version": 1,
|
||||
"Sequence": sequence,
|
||||
"Param_A": ENDPOINT_RECOVERY_PROBE_DURATION_MS,
|
||||
"Param_B": ENDPOINT_RECOVERY_PROBE_LEVEL_X10,
|
||||
},
|
||||
)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, 3)
|
||||
|
||||
def terminal(values: dict[str, int], _: CanFrame) -> bool:
|
||||
return bool(
|
||||
values["Motor_Sub_ID"] == motor
|
||||
and values["Motor_Sequence"] == sequence
|
||||
and (
|
||||
values["Task_State"] in {6, 7}
|
||||
or values["Result"] not in {1, 2}
|
||||
)
|
||||
)
|
||||
|
||||
values, feedback = self._receive_until(bus, 0x302, 5_000, terminal)
|
||||
after, after_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
hall_before = int(baseline["Hall_Count"])
|
||||
hall_after = int(after["Hall_Count"])
|
||||
hall_delta = hall_after - hall_before
|
||||
if (
|
||||
values["Task_State"] != 6
|
||||
or values["Result"] != 2
|
||||
or hall_delta <= ENDPOINT_RECOVERY_MAX_STATIONARY_HALL_DELTA
|
||||
):
|
||||
raise HardwareExecutionError(
|
||||
"opposite-direction probe did not prove free Hall movement",
|
||||
telemetry={
|
||||
"probe_task_state": int(values["Task_State"]),
|
||||
"probe_motor_result": int(values["Result"]),
|
||||
"probe_hall_before": hall_before,
|
||||
"probe_hall_after": hall_after,
|
||||
"probe_hall_delta": hall_delta,
|
||||
"probe_command_frame": command.public(),
|
||||
"probe_ack_frame": ack_frame.public(),
|
||||
"probe_feedback_frame": feedback.public(),
|
||||
"probe_diagnostic_before_frame": baseline_frame.public(),
|
||||
"probe_diagnostic_frame": after_frame.public(),
|
||||
},
|
||||
)
|
||||
return {
|
||||
"sequence": sequence,
|
||||
"duration_ms": ENDPOINT_RECOVERY_PROBE_DURATION_MS,
|
||||
"duty_percent": ENDPOINT_RECOVERY_PROBE_LEVEL_X10 // 10,
|
||||
"hall_before": hall_before,
|
||||
"hall_after": hall_after,
|
||||
"hall_delta": hall_delta,
|
||||
"system_ack": ack_values["Ack"],
|
||||
"command_frame": command.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_before_frame": baseline_frame.public(),
|
||||
"diagnostic_frame": after_frame.public(),
|
||||
}
|
||||
|
||||
def _motor_status_snapshot(
|
||||
self, bus: CanBus, motor: int
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
@@ -482,6 +1001,86 @@ class HardwareSeatExecutor:
|
||||
lambda values, _: values["Motor_Sub_ID"] == motor,
|
||||
)
|
||||
|
||||
def _home_known_range(self, bus: CanBus, motor: int) -> dict[str, Any]:
|
||||
"""Home one invalid axis to its verified near endpoint without re-ranging."""
|
||||
|
||||
before, before_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
expected_max = self.motor_max_hall.get(motor)
|
||||
if expected_max is None:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} has no repository MaxHall calibration"
|
||||
)
|
||||
observed_max = int(before["Max_Hall_Count"])
|
||||
max_tolerance = max(4, round(expected_max * 0.05))
|
||||
if abs(observed_max - expected_max) > max_tolerance:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} MaxHall mismatch blocks known-range homing: "
|
||||
f"observed={observed_max}, expected={expected_max}±{max_tolerance}"
|
||||
)
|
||||
|
||||
sequence = self._next_sequence()
|
||||
command = self._send_encoded(
|
||||
bus,
|
||||
"T2G_Seat_Move_Command_300",
|
||||
{
|
||||
"Opcode": 0x0B,
|
||||
"Motor_Selector": motor,
|
||||
"Direction": 0,
|
||||
"Protocol_Version": 1,
|
||||
"Sequence": sequence,
|
||||
"Param_A": 0x4B52,
|
||||
"Param_B": self.home_timeout_s,
|
||||
},
|
||||
)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, 0x0B)
|
||||
|
||||
def complete(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Motor_Sub_ID"] != motor or values["Motor_Sequence"] != sequence:
|
||||
return False
|
||||
if values["Task_State"] == 7 or values["Result"] not in {1, 2}:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} known-range homing failed: "
|
||||
f"task_state={values['Task_State']}, result={values['Result']}"
|
||||
)
|
||||
return values["Task_State"] == 6 and values["Result"] == 2
|
||||
|
||||
status, status_frame = self._receive_until(
|
||||
bus,
|
||||
0x302,
|
||||
(self.home_timeout_s + 15) * 1000,
|
||||
complete,
|
||||
)
|
||||
after, after_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
after_max = int(after["Max_Hall_Count"])
|
||||
if (
|
||||
not status["Position_Valid"]
|
||||
or not status["Near_Endpoint"]
|
||||
or int(status["Value_X100"]) > 100
|
||||
or int(after["Hall_Count"]) > 2
|
||||
or abs(after_max - expected_max) > max_tolerance
|
||||
):
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} known-range homing feedback was inconsistent",
|
||||
telemetry={
|
||||
"home_status_frame": status_frame.public(),
|
||||
"home_diagnostic_frame": after_frame.public(),
|
||||
},
|
||||
)
|
||||
return {
|
||||
"mode": "home_known_range",
|
||||
"motor_selector": motor,
|
||||
"sequence": sequence,
|
||||
"expected_max_hall": expected_max,
|
||||
"hall_before": int(before["Hall_Count"]),
|
||||
"hall_after": int(after["Hall_Count"]),
|
||||
"command_frame": command.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"status_frame": status_frame.public(),
|
||||
"diagnostic_before_frame": before_frame.public(),
|
||||
"diagnostic_after_frame": after_frame.public(),
|
||||
"system_ack": ack_values["Ack"],
|
||||
}
|
||||
|
||||
def _execute_timed_motion(
|
||||
self,
|
||||
bus: CanBus,
|
||||
@@ -493,6 +1092,7 @@ class HardwareSeatExecutor:
|
||||
baseline, baseline_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
sequence = self._next_sequence()
|
||||
signals["Sequence"] = sequence
|
||||
self._reset_current_samples(motor)
|
||||
command_frame = self._send_encoded(bus, command["message"], signals)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, 3)
|
||||
|
||||
@@ -501,7 +1101,7 @@ class HardwareSeatExecutor:
|
||||
return False
|
||||
task_state = values["Task_State"]
|
||||
result = values["Result"]
|
||||
return task_state in {8, 9} or result not in {1, 2}
|
||||
return task_state in {6, 7} or result not in {1, 2}
|
||||
|
||||
values, feedback = self._receive_until(
|
||||
bus,
|
||||
@@ -543,8 +1143,8 @@ class HardwareSeatExecutor:
|
||||
else int(baseline["Max_Hall_Count"]),
|
||||
)
|
||||
|
||||
if task_state != 8 or result != 2:
|
||||
if result == 4 and hall_delta <= 2:
|
||||
if task_state != 6 or result != 2:
|
||||
if result == 5 and hall_delta <= 2:
|
||||
failure_category = "stall_no_hall_activity"
|
||||
diagnostic_hint = (
|
||||
"No sustained Hall activity was observed. The commanded direction may "
|
||||
@@ -553,13 +1153,13 @@ class HardwareSeatExecutor:
|
||||
"small command in the opposite direction to distinguish endpoint from "
|
||||
"a Hall wiring fault; do not run automatic full-range calibration."
|
||||
)
|
||||
elif result == 4:
|
||||
elif result == 5:
|
||||
failure_category = "stall_after_hall_activity"
|
||||
diagnostic_hint = (
|
||||
"Hall activity was observed before the stall; inspect the mechanism for "
|
||||
"an endpoint or obstruction in the commanded direction."
|
||||
)
|
||||
elif result == 5 or last_event in {2, 3}:
|
||||
elif result == 4 or last_event in {2, 3}:
|
||||
failure_category = "endpoint"
|
||||
diagnostic_hint = (
|
||||
"The controller reported an endpoint. Verify the requested direction "
|
||||
@@ -606,6 +1206,7 @@ class HardwareSeatExecutor:
|
||||
else None
|
||||
),
|
||||
"diagnostic_read_error": diagnostic_error,
|
||||
"current_diagnostic": self._current_diagnostic_summary(motor),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -11,6 +11,11 @@ TARGET_PATTERN = re.compile(r"主副驾|前排|主驾|驾驶位|司机位|副驾
|
||||
FEATURE_PATTERN = re.compile(r"通风|加热")
|
||||
STATE_PATTERN = re.compile(r"关闭|关掉|停止|取消|打开|开启|启动|开")
|
||||
LEVEL_PATTERN = re.compile(r"([1-5一二三四五])\s*档")
|
||||
POSITION_PATTERN_TEMPLATE = (
|
||||
r"{alias}(?:位置)?(?:调到|移动到|移到|设为|设置为|调至|移动至|到|为)?"
|
||||
r"(?P<position>\d{{1,3}})(?:%|%)"
|
||||
)
|
||||
ZERO_POSITION_PATTERN = re.compile(r"归零|回零|复位|零位")
|
||||
VENTILATION_FEEDBACK_PATTERN = re.compile(r"(?<!不)(?<!加)热|(?<!不)闷|出汗")
|
||||
HEATING_FEEDBACK_PATTERN = re.compile(r"(?<!不)冷|(?<!不)凉")
|
||||
MILD_PATTERN = re.compile(r"有点|一点|稍微|微微")
|
||||
@@ -18,6 +23,7 @@ STRONG_PATTERN = re.compile(r"很|太|非常|特别|难受")
|
||||
AIRFLOW_CAUSE_PATTERN = re.compile(r"吹|风太大|风吹")
|
||||
FIRST_PERSON_PATTERN = re.compile(r"我|自己")
|
||||
VEHICLE_SCOPE_PATTERN = re.compile(r"车上|车内|车里|全车|整车|整个车")
|
||||
PASSENGER_ONLY_QUESTION = "当前所有座椅能力仅控制副驾,不支持主驾座椅目标。"
|
||||
|
||||
TARGETS = {
|
||||
"主驾": ["driver"],
|
||||
@@ -40,6 +46,12 @@ STATES = {
|
||||
"取消": "off",
|
||||
}
|
||||
LEVELS = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5}
|
||||
MOTION_AXIS_ALIASES = {
|
||||
"longitudinal": ("水平", "前后", "纵向"),
|
||||
"cushion_tilt": ("坐盆", "坐垫"),
|
||||
"backrest_recline": ("靠背",),
|
||||
"legrest_height": ("腿托", "腿部支撑"),
|
||||
}
|
||||
|
||||
|
||||
def _nearest_preceding_target(clause: str, position: int) -> str | None:
|
||||
@@ -107,8 +119,15 @@ def _feedback_targets(
|
||||
if last_target in {"driver", "passenger"}:
|
||||
return [last_target], "history"
|
||||
if FIRST_PERSON_PATTERN.search(text):
|
||||
return ["driver"], "speaker_default"
|
||||
return [], "missing"
|
||||
return ["passenger"], "passenger_default"
|
||||
return ["passenger"], "passenger_default"
|
||||
|
||||
|
||||
def _contains_unsupported_driver_target(text: str) -> bool:
|
||||
return any(
|
||||
"driver" in TARGETS[match.group(0)]
|
||||
for match in TARGET_PATTERN.finditer(text)
|
||||
)
|
||||
|
||||
|
||||
def _inferred_level(text: str) -> tuple[int, str]:
|
||||
@@ -162,6 +181,17 @@ def resolve_contextual_intent(
|
||||
if not ventilation_cues and not cold_cues:
|
||||
return result
|
||||
|
||||
if _contains_unsupported_driver_target(text):
|
||||
result["actions"] = []
|
||||
result["ambiguities"] = [
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
]
|
||||
result["decision_summary"] = [
|
||||
"识别到主驾或包含主驾的座椅范围;当前系统不会将其重定向到副驾。"
|
||||
]
|
||||
result["confidence"] = max(float(result.get("confidence", 0)), 0.99)
|
||||
return result
|
||||
|
||||
if VEHICLE_SCOPE_PATTERN.search(text) and not TARGET_PATTERN.search(text):
|
||||
result["actions"] = []
|
||||
result["ambiguities"] = [
|
||||
@@ -197,8 +227,8 @@ def resolve_contextual_intent(
|
||||
summaries: list[str] = []
|
||||
if target_source == "history":
|
||||
summaries.append(f"本轮省略了座位,沿用上一轮的{labels[targets[0]]}目标。")
|
||||
elif target_source == "speaker_default":
|
||||
summaries.append("当前为座椅单域驾驶员交互,将第一人称“我”解析为主驾。")
|
||||
elif target_source == "passenger_default":
|
||||
summaries.append("当前座椅域仅控制副驾,本轮省略座位并安全限定为副驾。")
|
||||
|
||||
planned_actions: list[dict[str, Any]] = []
|
||||
desired_level, intensity = _inferred_level(text)
|
||||
@@ -342,8 +372,31 @@ def parse_local_intent(
|
||||
text = re.sub(r"\s+", "", utterance.strip())
|
||||
clauses = [part for part in re.split(r"[,,。;;!!??]", text) if part]
|
||||
actions: list[dict[str, Any]] = []
|
||||
motion_actions: list[dict[str, Any]] = []
|
||||
ambiguities: list[dict[str, str]] = []
|
||||
|
||||
mentioned_axes = [
|
||||
axis
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items()
|
||||
if any(alias in text for alias in aliases)
|
||||
]
|
||||
if ZERO_POSITION_PATTERN.search(text) and (mentioned_axes or "四轴" in text):
|
||||
reset_axes = list(MOTION_AXIS_ALIASES) if "四轴" in text else mentioned_axes
|
||||
if _contains_unsupported_driver_target(text):
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
else:
|
||||
motion_actions.extend(
|
||||
{
|
||||
"target": "passenger",
|
||||
"action_type": "position",
|
||||
"axis": axis,
|
||||
"position_pct": 0,
|
||||
}
|
||||
for axis in reset_axes
|
||||
)
|
||||
|
||||
for clause in clauses:
|
||||
level = _level_for_clause(clause)
|
||||
for feature_match in FEATURE_PATTERN.finditer(clause):
|
||||
@@ -351,8 +404,10 @@ def parse_local_intent(
|
||||
target_zh = _nearest_preceding_target(clause, feature_match.start())
|
||||
state = _state_for_feature(clause, feature_match.end())
|
||||
if target_zh is None:
|
||||
target_zh = "副驾"
|
||||
if "driver" in TARGETS[target_zh]:
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": f"“{feature_zh}”需要作用于主驾还是副驾?"}
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
continue
|
||||
if state is None:
|
||||
@@ -369,6 +424,40 @@ def parse_local_intent(
|
||||
"requested_level": level,
|
||||
}
|
||||
)
|
||||
if ZERO_POSITION_PATTERN.search(text):
|
||||
continue
|
||||
for axis, aliases in MOTION_AXIS_ALIASES.items():
|
||||
for alias in aliases:
|
||||
match = re.search(
|
||||
POSITION_PATTERN_TEMPLATE.format(alias=re.escape(alias)),
|
||||
clause,
|
||||
)
|
||||
if match is None:
|
||||
continue
|
||||
target_zh = _nearest_preceding_target(clause, match.start()) or "副驾"
|
||||
if "driver" in TARGETS[target_zh]:
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": PASSENGER_ONLY_QUESTION}
|
||||
)
|
||||
break
|
||||
position_pct = int(match.group("position"))
|
||||
if position_pct > 100:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": f"passenger.position.{axis}",
|
||||
"question": f"{alias}位置百分比必须在0到100之间。",
|
||||
}
|
||||
)
|
||||
break
|
||||
motion_actions.append(
|
||||
{
|
||||
"target": "passenger",
|
||||
"action_type": "position",
|
||||
"axis": axis,
|
||||
"position_pct": position_pct,
|
||||
}
|
||||
)
|
||||
break
|
||||
|
||||
merged: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for action in actions:
|
||||
@@ -382,7 +471,19 @@ def parse_local_intent(
|
||||
}
|
||||
)
|
||||
merged[key] = action
|
||||
actions = list(merged.values())
|
||||
merged_motion: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for action in motion_actions:
|
||||
key = (action["target"], action["axis"])
|
||||
previous = merged_motion.get(key)
|
||||
if previous and previous["position_pct"] != action["position_pct"]:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": f"{key[0]}.position.{key[1]}",
|
||||
"question": "同一个座椅轴出现多个位置目标,请确认最终位置。",
|
||||
}
|
||||
)
|
||||
merged_motion[key] = action
|
||||
actions = [*merged.values(), *merged_motion.values()]
|
||||
|
||||
parsed = resolve_contextual_intent(
|
||||
{
|
||||
@@ -401,11 +502,11 @@ def parse_local_intent(
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "capability",
|
||||
"question": "当前仅支持主/副驾座椅通风或加热的打开与关闭。",
|
||||
"question": "当前仅支持副驾座椅通风、加热、移动和姿态预设能力。",
|
||||
}
|
||||
)
|
||||
|
||||
summary = ["识别为座椅热舒适单域请求。", *parsed["decision_summary"]]
|
||||
summary = ["识别为副驾座椅功能单域请求。", *parsed["decision_summary"]]
|
||||
labels = {
|
||||
"driver": "主驾",
|
||||
"passenger": "副驾",
|
||||
@@ -414,11 +515,24 @@ def parse_local_intent(
|
||||
"on": "打开",
|
||||
"off": "关闭",
|
||||
}
|
||||
summary.extend(
|
||||
f"目标:{labels[action['target']]}座椅{labels[action['function']]}{labels[action['state']]}。"
|
||||
for action in actions
|
||||
)
|
||||
if any(action["requested_level"] is not None for action in actions):
|
||||
axis_labels = {
|
||||
"longitudinal": "水平",
|
||||
"cushion_tilt": "坐盆",
|
||||
"backrest_recline": "靠背",
|
||||
"legrest_height": "腿托",
|
||||
}
|
||||
for action in actions:
|
||||
if action.get("action_type") == "position":
|
||||
summary.append(
|
||||
f"目标:副驾座椅{axis_labels[action['axis']]}位置"
|
||||
f"{action['position_pct']}%。"
|
||||
)
|
||||
else:
|
||||
summary.append(
|
||||
f"目标:{labels[action['target']]}座椅"
|
||||
f"{labels[action['function']]}{labels[action['state']]}。"
|
||||
)
|
||||
if any(action.get("requested_level") is not None for action in actions):
|
||||
summary.append("目标档位已写入功能配置;现有 CAN 矩阵仅支持档位虚拟回读。")
|
||||
|
||||
response = {
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
|
||||
SUPPORTED_TARGETS = {"driver", "passenger"}
|
||||
SUPPORTED_TARGETS = {"passenger"}
|
||||
SUPPORTED_FUNCTIONS = {"ventilation", "heating"}
|
||||
SUPPORTED_STATES = {"on", "off"}
|
||||
SUPPORTED_MOTION_AXES = {
|
||||
@@ -17,7 +17,7 @@ SUPPORTED_MOTION_AXES = {
|
||||
}
|
||||
ANGLE_RANGES = {
|
||||
"backrest_recline": (95, 163),
|
||||
"legrest_height": (95, 173),
|
||||
"legrest_height": (95, 167),
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ def validate_semantics(
|
||||
seen: dict[tuple[str, str], str] = {}
|
||||
active_functions: dict[str, set[str]] = {}
|
||||
has_motion = False
|
||||
has_driver_motion = False
|
||||
for index, action in enumerate(parsed.get("actions", [])):
|
||||
path = f"actions[{index}]"
|
||||
if action.get("target") not in SUPPORTED_TARGETS:
|
||||
@@ -80,12 +79,11 @@ def validate_semantics(
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_TARGET",
|
||||
"path": f"{path}.target",
|
||||
"message": "当前只支持主驾和前排副驾。",
|
||||
"message": "当前所有座椅能力仅支持前排副驾。",
|
||||
}
|
||||
)
|
||||
if action.get("action_type") == "position":
|
||||
has_motion = True
|
||||
has_driver_motion = has_driver_motion or action.get("target") == "driver"
|
||||
axis = action.get("axis")
|
||||
position = action.get("position_pct")
|
||||
if axis not in SUPPORTED_MOTION_AXES:
|
||||
@@ -206,16 +204,6 @@ def validate_semantics(
|
||||
"message": "座椅位置控制和反馈 CAN 矩阵完全为演示虚构,仅用于验证模型与配置链路。",
|
||||
}
|
||||
)
|
||||
if has_driver_motion:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED",
|
||||
"path": "platform.reference_vehicle.vehicle_state",
|
||||
"message": "未接入车速、挡位、乘员和防夹状态,主驾移动只能虚拟执行。",
|
||||
}
|
||||
)
|
||||
|
||||
for target, functions in active_functions.items():
|
||||
if functions == {"ventilation", "heating"}:
|
||||
issues.append(
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -27,7 +27,7 @@ class SeatContractTests(unittest.TestCase):
|
||||
self.assertEqual(self.validator.validate(config), [])
|
||||
|
||||
def test_inferred_level_function_config_matches_both_schemas(self) -> None:
|
||||
parsed = parse_local_intent("主驾有点热,有点闷")
|
||||
parsed = parse_local_intent("副驾有点热,有点闷")
|
||||
config = build_function_config(parsed, "req-contract-002", "local_rules")
|
||||
|
||||
self.assertEqual(config["spec"]["operations"][0]["parameters"]["level"], 1)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
@@ -6,6 +6,7 @@ from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
|
||||
from cognitive_agent.infrastructure.llm.openai_responses import OpenAIIntentError
|
||||
|
||||
|
||||
class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
@@ -17,13 +18,13 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
def test_supported_request_completes_full_dry_run(self) -> None:
|
||||
result = self.service.process(
|
||||
"打开主驾座椅通风,同时关闭副驾座椅加热", provider="local"
|
||||
"打开副驾座椅通风,同时关闭副驾座椅加热", provider="local"
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 2)
|
||||
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["command_frame"]["data_hex"], "01 80")
|
||||
self.assertEqual(telemetry["command_frame"]["data_hex"], "00 90")
|
||||
self.assertTrue(Path(result["artifact_path"]).is_absolute())
|
||||
self.assertTrue(
|
||||
(self.service.artifact_root / result["request_id"] / "result.json").is_file()
|
||||
@@ -32,7 +33,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(self.service.contracts.validate(event), [])
|
||||
|
||||
def test_gear_request_is_preserved_and_simulated(self) -> None:
|
||||
result = self.service.process("主驾座椅加热开到三档", provider="local")
|
||||
result = self.service.process("副驾座椅加热开到三档", provider="local")
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operation = result["function_config"]["spec"]["operations"][0]
|
||||
@@ -48,21 +49,21 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
conversation_id = "conv-test-context"
|
||||
|
||||
first = self.service.process(
|
||||
"主驾有点热,有点闷",
|
||||
"副驾有点热,有点闷",
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
second = self.service.process(
|
||||
"主驾还有点闷",
|
||||
"副驾还有点闷",
|
||||
provider="local",
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
|
||||
self.assertEqual(first["status"], "succeeded")
|
||||
self.assertEqual(first["actuator_state"]["driver"]["ventilation"]["level"], 1)
|
||||
self.assertEqual(first["actuator_state"]["passenger"]["ventilation"]["level"], 1)
|
||||
self.assertEqual(second["status"], "succeeded")
|
||||
self.assertEqual(second["conversation_turn"], 2)
|
||||
self.assertEqual(second["actuator_state"]["driver"]["ventilation"]["level"], 2)
|
||||
self.assertEqual(second["actuator_state"]["passenger"]["ventilation"]["level"], 2)
|
||||
operation = second["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(operation["parameters"], {"state": "on", "level": 2})
|
||||
self.assertIn("提升到2档", "".join(second["decision_summary"]))
|
||||
@@ -94,12 +95,15 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(fourth_operation["target"]["instance"], "passenger")
|
||||
self.assertEqual(fourth_operation["capability"], "seat.set_ventilation")
|
||||
self.assertEqual(fourth_operation["parameters"], {"state": "off"})
|
||||
self.assertEqual(fourth["actuator_state"]["driver"]["heating"], {"state": "on", "level": 1})
|
||||
self.assertEqual(
|
||||
fourth["actuator_state"]["passenger"]["heating"],
|
||||
{"state": "on", "level": 1},
|
||||
)
|
||||
self.assertEqual(fourth["conversation_turn"], 4)
|
||||
|
||||
def test_compound_cold_and_stuffy_feedback_generates_balanced_dry_run(self) -> None:
|
||||
result = self.service.process(
|
||||
"主驾有点冷、有点闷",
|
||||
"副驾有点冷、有点闷",
|
||||
provider="local",
|
||||
conversation_id="conv-compound-comfort",
|
||||
)
|
||||
@@ -113,7 +117,10 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
command = result["command_plan"]["spec"]["commands"][0]
|
||||
self.assertEqual(
|
||||
command["signals"],
|
||||
{"Driver_Seat_Heating_S": 1, "Driver_Seat_Ventilation_S": 1},
|
||||
{
|
||||
"Front_Passenger_Seat_Heating_S": 1,
|
||||
"Front_Passenger_Seat_Ventilation_S": 1,
|
||||
},
|
||||
)
|
||||
self.assertIn(
|
||||
"COMBINED_THERMAL_SIMULATED",
|
||||
@@ -179,6 +186,30 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
"车上太闷了", context, self.service.capability_context
|
||||
)
|
||||
|
||||
def test_explicit_gpt_failure_falls_back_to_local_rules(self) -> None:
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(
|
||||
self.service.openai,
|
||||
"interpret",
|
||||
side_effect=OpenAIIntentError("invalid_api_key"),
|
||||
):
|
||||
result = self.service.process(
|
||||
"打开副驾座椅通风",
|
||||
"openai",
|
||||
"conv-explicit-gpt-fallback",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertEqual(result["provider_used"], "local_rules_fallback")
|
||||
self.assertEqual(
|
||||
result["function_config"]["spec"]["operations"][0]["target"]["instance"],
|
||||
"passenger",
|
||||
)
|
||||
self.assertTrue(
|
||||
any("已降级到本地规则解析" in item for item in result["decision_summary"])
|
||||
)
|
||||
|
||||
def test_model_climate_request_completes_domain_loop(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["climate"],
|
||||
@@ -220,13 +251,13 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_heating",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "activate",
|
||||
"parameters": [
|
||||
{"name": "state", "value": "on"},
|
||||
{"name": "level", "value": 1},
|
||||
],
|
||||
"reason": "Warm the driver seat.",
|
||||
"reason": "Warm the passenger seat.",
|
||||
},
|
||||
{
|
||||
"domain": "climate",
|
||||
@@ -245,14 +276,17 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾有点冷、有点闷",
|
||||
"副驾有点冷、有点闷",
|
||||
"openai",
|
||||
"conv-model-partial",
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
self.assertIsNotNone(result["execution_report"])
|
||||
self.assertEqual(result["actuator_state"]["driver"]["heating"], {"state": "on", "level": 1})
|
||||
self.assertEqual(
|
||||
result["actuator_state"]["passenger"]["heating"],
|
||||
{"state": "on", "level": 1},
|
||||
)
|
||||
self.assertEqual(result["domain_state"]["climate"]["fan_level"], 2)
|
||||
self.assertEqual(set(result["function_configs"]), {"seat", "climate"})
|
||||
self.assertEqual(result["scene_config"]["kind"], "SceneConfig")
|
||||
@@ -264,18 +298,18 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 35}],
|
||||
"reason": "Move the driver seat backward from its current midpoint.",
|
||||
"reason": "Move the passenger seat backward from its current midpoint.",
|
||||
},
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "position_pct", "value": 60}],
|
||||
"reason": "Recline the driver backrest.",
|
||||
"reason": "Recline the passenger backrest.",
|
||||
},
|
||||
{
|
||||
"domain": "seat",
|
||||
@@ -302,7 +336,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾往后一点并把靠背躺下,副驾腿托升起并伸长",
|
||||
"副驾往后一点并把靠背躺下,腿托升起并伸长",
|
||||
"openai",
|
||||
"conv-model-motion",
|
||||
)
|
||||
@@ -311,20 +345,18 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(len(result["function_config"]["spec"]["operations"]), 4)
|
||||
self.assertEqual(len(result["command_plan"]["spec"]["commands"]), 4)
|
||||
self.assertEqual(len(result["execution_report"]["spec"]["results"]), 4)
|
||||
driver = result["actuator_state"]["driver"]["position"]
|
||||
passenger = result["actuator_state"]["passenger"]["position"]
|
||||
self.assertEqual(driver["longitudinal"]["position_pct"], 35)
|
||||
self.assertEqual(driver["backrest_recline"]["position_pct"], 60)
|
||||
self.assertEqual(passenger["longitudinal"]["position_pct"], 35)
|
||||
self.assertEqual(passenger["backrest_recline"]["position_pct"], 60)
|
||||
self.assertEqual(passenger["legrest_height"]["position_pct"], 65)
|
||||
self.assertEqual(passenger["legrest_extension"]["position_pct"], 70)
|
||||
telemetry = result["execution_report"]["spec"]["results"][0]["telemetry"]
|
||||
self.assertEqual(telemetry["start_position_pct"], 50)
|
||||
self.assertEqual(telemetry["effective_position_pct"], 35)
|
||||
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x558")
|
||||
self.assertEqual(telemetry["feedback_frame"]["frame_id"], "0x559")
|
||||
self.assertEqual(telemetry["fault_mask"], 0)
|
||||
codes = {issue["code"] for issue in result["validation"]}
|
||||
self.assertIn("SYNTHETIC_MOTION_CAN", codes)
|
||||
self.assertIn("DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED", codes)
|
||||
|
||||
def test_motion_follow_up_state_is_supplied_to_model(self) -> None:
|
||||
def output(position: int) -> dict[str, object]:
|
||||
@@ -334,7 +366,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "increase",
|
||||
"parameters": [{"name": "position_pct", "value": position}],
|
||||
"reason": "Model resolved the relative request.",
|
||||
@@ -352,27 +384,29 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
side_effect=[output(60), output(70)],
|
||||
) as interpret:
|
||||
first = self.service.process(
|
||||
"主驾往后一点", "openai", "conv-motion-follow-up"
|
||||
"副驾往后一点", "openai", "conv-motion-follow-up"
|
||||
)
|
||||
second = self.service.process(
|
||||
"还是靠前,再往后一点", "openai", "conv-motion-follow-up"
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
first["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
first["actuator_state"]["passenger"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
60,
|
||||
)
|
||||
self.assertEqual(
|
||||
second["actuator_state"]["driver"]["position"]["longitudinal"][
|
||||
second["actuator_state"]["passenger"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
70,
|
||||
)
|
||||
second_context = interpret.call_args_list[1].args[1]
|
||||
self.assertEqual(
|
||||
second_context["devices"]["driver"]["position"]["longitudinal"]["position_pct"],
|
||||
second_context["devices"]["passenger"]["position"]["longitudinal"][
|
||||
"position_pct"
|
||||
],
|
||||
60,
|
||||
)
|
||||
|
||||
@@ -383,7 +417,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "decrease",
|
||||
"parameters": [{"name": "distance_mm", "value": 50}],
|
||||
"reason": "Move forward by a physical distance.",
|
||||
@@ -391,7 +425,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_backrest_recline",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "increase",
|
||||
"parameters": [{"name": "angle_delta_deg", "value": 10}],
|
||||
"reason": "Recline by a relative physical angle.",
|
||||
@@ -405,7 +439,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output) as interpret:
|
||||
result = self.service.process(
|
||||
"主驾水平向前移动50毫米,靠背往后10度",
|
||||
"副驾水平向前移动50毫米,靠背往后10度",
|
||||
"openai",
|
||||
"conv-vehicle-profile-relative",
|
||||
vehicle_id="denza_n7",
|
||||
@@ -417,22 +451,22 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(operations[0]["parameters"], {"position_pct": 27})
|
||||
self.assertEqual(
|
||||
operations[1]["parameters"],
|
||||
{"position_pct": 38, "angle_deg": 116},
|
||||
{"position_pct": 35, "angle_deg": 118.6},
|
||||
)
|
||||
backrest_motion = operations[1]["motion"]
|
||||
self.assertEqual(backrest_motion["mode"], "relative")
|
||||
self.assertEqual(backrest_motion["direction"], "increase")
|
||||
self.assertEqual(backrest_motion["start_position_pct"], 20)
|
||||
self.assertEqual(backrest_motion["delta_position_pct"], 18)
|
||||
self.assertEqual(backrest_motion["start_angle_deg"], 106.0)
|
||||
self.assertEqual(backrest_motion["delta_position_pct"], 15)
|
||||
self.assertEqual(backrest_motion["start_angle_deg"], 108.6)
|
||||
self.assertEqual(backrest_motion["delta_angle_deg"], 10)
|
||||
context_sent_to_model = interpret.call_args.args[1]
|
||||
self.assertEqual(context_sent_to_model["vehicle"]["id"], "denza_n7")
|
||||
self.assertEqual(
|
||||
context_sent_to_model["devices"]["driver"]["position"][
|
||||
context_sent_to_model["devices"]["passenger"]["position"][
|
||||
"backrest_recline"
|
||||
]["angle_deg"],
|
||||
106.0,
|
||||
108.6,
|
||||
)
|
||||
|
||||
def test_vehicle_profile_angle_limits_are_accepted(self) -> None:
|
||||
@@ -452,7 +486,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
"capability": "seat.set_legrest_height",
|
||||
"target_scope": "passenger",
|
||||
"operation": "set",
|
||||
"parameters": [{"name": "angle_deg", "value": 173}],
|
||||
"parameters": [{"name": "angle_deg", "value": 167}],
|
||||
"reason": "Use the vehicle-specific maximum legrest angle.",
|
||||
},
|
||||
],
|
||||
@@ -464,7 +498,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"副驾靠背到163度,腿托到173度",
|
||||
"副驾靠背到163度,腿托到167度",
|
||||
"openai",
|
||||
"conv-vehicle-angle-limits",
|
||||
vehicle_id="denza_n7",
|
||||
@@ -473,7 +507,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operations = result["function_config"]["spec"]["operations"]
|
||||
self.assertEqual(operations[0]["parameters"], {"position_pct": 100, "angle_deg": 163})
|
||||
self.assertEqual(operations[1]["parameters"], {"position_pct": 100, "angle_deg": 173})
|
||||
self.assertEqual(operations[1]["parameters"], {"position_pct": 100, "angle_deg": 167})
|
||||
|
||||
def test_motion_adapter_does_not_resolve_missing_absolute_target(self) -> None:
|
||||
model_output = {
|
||||
@@ -482,7 +516,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_longitudinal_position",
|
||||
"target_scope": "driver",
|
||||
"target_scope": "passenger",
|
||||
"operation": "decrease",
|
||||
"parameters": [],
|
||||
"reason": "No absolute target was produced.",
|
||||
@@ -496,7 +530,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"主驾往后一点", "openai", "conv-motion-missing-target"
|
||||
"副驾往后一点", "openai", "conv-motion-missing-target"
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
@@ -630,7 +664,7 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(
|
||||
first_operations[1]["parameters"],
|
||||
{"position_pct": 49, "angle_deg": 133},
|
||||
{"position_pct": 53, "angle_deg": 133},
|
||||
)
|
||||
self.assertEqual(
|
||||
first["actuator_state"]["passenger"]["position"]["backrest_recline"][
|
||||
@@ -698,11 +732,53 @@ class SeatServiceIntegrationTests(unittest.TestCase):
|
||||
apply_context = interpret.call_args_list[4].args[1]
|
||||
self.assertEqual(apply_context["presets"][0]["name"], "零重力模式")
|
||||
|
||||
def test_missing_target_requests_confirmation(self) -> None:
|
||||
def test_missing_target_defaults_to_passenger(self) -> None:
|
||||
result = self.service.process("打开座椅通风", provider="local")
|
||||
|
||||
self.assertEqual(result["status"], "succeeded")
|
||||
operation = result["function_config"]["spec"]["operations"][0]
|
||||
self.assertEqual(operation["target"]["instance"], "passenger")
|
||||
|
||||
def test_explicit_driver_target_is_rejected(self) -> None:
|
||||
result = self.service.process("打开主驾座椅通风", provider="local")
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
self.assertIsNone(result["function_config"])
|
||||
questions = [
|
||||
item["question"] for item in result["intent_ir"]["spec"]["ambiguities"]
|
||||
]
|
||||
self.assertTrue(any("仅控制副驾" in question for question in questions))
|
||||
|
||||
def test_model_driver_target_is_rejected(self) -> None:
|
||||
model_output = {
|
||||
"detected_domains": ["seat"],
|
||||
"requests": [
|
||||
{
|
||||
"domain": "seat",
|
||||
"capability": "seat.set_ventilation",
|
||||
"target_scope": "driver",
|
||||
"operation": "activate",
|
||||
"parameters": [{"name": "state", "value": "on"}],
|
||||
"reason": "Ventilate the explicitly requested driver seat.",
|
||||
}
|
||||
],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["Explicit driver-seat request."],
|
||||
"confidence": 0.95,
|
||||
}
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
with patch.object(self.service.openai, "interpret", return_value=model_output):
|
||||
result = self.service.process(
|
||||
"打开主驾座椅通风", "openai", "conv-driver-rejected"
|
||||
)
|
||||
|
||||
self.assertEqual(result["status"], "needs_confirmation")
|
||||
self.assertIsNone(result["function_config"])
|
||||
questions = [
|
||||
item["question"] for item in result["intent_ir"]["spec"]["ambiguities"]
|
||||
]
|
||||
self.assertTrue(any("仅控制副驾" in question for question in questions))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -9,27 +9,27 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
def context(level: int = 0) -> dict:
|
||||
return {
|
||||
"last_target": "driver" if level else None,
|
||||
"last_target": "passenger" if level else None,
|
||||
"devices": {
|
||||
"driver": {
|
||||
"ventilation": {"state": "on" if level else "off", "level": level},
|
||||
"ventilation": {"state": "off", "level": 0},
|
||||
"heating": {"state": "off", "level": 0},
|
||||
},
|
||||
"passenger": {
|
||||
"ventilation": {"state": "off", "level": 0},
|
||||
"ventilation": {"state": "on" if level else "off", "level": level},
|
||||
"heating": {"state": "off", "level": 0},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
def test_parses_driver_ventilation_on(self) -> None:
|
||||
result = parse_local_intent("打开主驾座椅通风")
|
||||
def test_parses_passenger_ventilation_on(self) -> None:
|
||||
result = parse_local_intent("打开副驾座椅通风")
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": None,
|
||||
@@ -38,31 +38,65 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
|
||||
def test_expands_front_seats(self) -> None:
|
||||
def test_rejects_scope_that_includes_driver(self) -> None:
|
||||
result = parse_local_intent("主副驾座椅通风都打开")
|
||||
|
||||
self.assertEqual({item["target"] for item in result["actions"]}, {"driver", "passenger"})
|
||||
self.assertTrue(all(item["state"] == "on" for item in result["actions"]))
|
||||
|
||||
def test_keeps_requested_level_for_safety_validation(self) -> None:
|
||||
result = parse_local_intent("主驾座椅加热开到三档")
|
||||
|
||||
self.assertEqual(result["actions"][0]["requested_level"], 3)
|
||||
|
||||
def test_requires_target(self) -> None:
|
||||
result = parse_local_intent("打开座椅通风")
|
||||
|
||||
self.assertEqual(result["actions"], [])
|
||||
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
||||
self.assertIn("仅控制副驾", result["ambiguities"][0]["question"])
|
||||
|
||||
def test_keeps_requested_level_for_safety_validation(self) -> None:
|
||||
result = parse_local_intent("副驾座椅加热开到三档")
|
||||
|
||||
self.assertEqual(result["actions"][0]["requested_level"], 3)
|
||||
|
||||
def test_defaults_missing_target_to_passenger(self) -> None:
|
||||
result = parse_local_intent("打开座椅通风")
|
||||
|
||||
self.assertEqual(result["actions"][0]["target"], "passenger")
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
|
||||
def test_parses_four_axis_zero_for_passenger(self) -> None:
|
||||
result = parse_local_intent("把水平、坐盆、靠背、腿托四轴归零")
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(
|
||||
[(action["axis"], action["position_pct"]) for action in result["actions"]],
|
||||
[
|
||||
("longitudinal", 0),
|
||||
("cushion_tilt", 0),
|
||||
("backrest_recline", 0),
|
||||
("legrest_height", 0),
|
||||
],
|
||||
)
|
||||
self.assertTrue(
|
||||
all(action["target"] == "passenger" for action in result["actions"])
|
||||
)
|
||||
|
||||
def test_parses_explicit_motion_percentages(self) -> None:
|
||||
result = parse_local_intent("副驾水平移到63%,靠背调到54%")
|
||||
|
||||
self.assertEqual(result["ambiguities"], [])
|
||||
self.assertEqual(
|
||||
[(action["axis"], action["position_pct"]) for action in result["actions"]],
|
||||
[("longitudinal", 63), ("backrest_recline", 54)],
|
||||
)
|
||||
|
||||
def test_rejects_driver_four_axis_zero(self) -> None:
|
||||
result = parse_local_intent("把主驾座椅四轴归零")
|
||||
|
||||
self.assertEqual(result["actions"], [])
|
||||
self.assertEqual(result["ambiguities"][0]["field"], "target")
|
||||
self.assertIn("仅控制副驾", result["ambiguities"][0]["question"])
|
||||
|
||||
def test_maps_mild_hot_and_stuffy_feedback_to_level_one(self) -> None:
|
||||
result = parse_local_intent("主驾有点热,有点闷", self.context())
|
||||
result = parse_local_intent("副驾有点热,有点闷", self.context())
|
||||
|
||||
self.assertEqual(
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": 1,
|
||||
@@ -75,7 +109,7 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
result = parse_local_intent("还有点闷", self.context(level=1))
|
||||
|
||||
self.assertEqual(result["actions"][0]["requested_level"], 2)
|
||||
self.assertEqual(result["actions"][0]["target"], "driver")
|
||||
self.assertEqual(result["actions"][0]["target"], "passenger")
|
||||
self.assertIn("提升到2档", "".join(result["decision_summary"]))
|
||||
|
||||
def test_cold_feedback_reduces_active_ventilation_before_heating(self) -> None:
|
||||
@@ -85,7 +119,7 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
result["actions"],
|
||||
[
|
||||
{
|
||||
"target": "driver",
|
||||
"target": "passenger",
|
||||
"function": "ventilation",
|
||||
"state": "off",
|
||||
"requested_level": None,
|
||||
@@ -114,7 +148,7 @@ class LocalIntentParserTests(unittest.TestCase):
|
||||
self.assertIn("致冷来源", "".join(result["decision_summary"]))
|
||||
|
||||
def test_cold_and_stuffy_are_independent_comfort_dimensions(self) -> None:
|
||||
result = parse_local_intent("主驾有点冷、有点闷", self.context())
|
||||
result = parse_local_intent("副驾有点冷、有点闷", self.context())
|
||||
|
||||
self.assertEqual(
|
||||
{
|
||||
|
||||
@@ -24,7 +24,7 @@ class MatrixCompilerTests(unittest.TestCase):
|
||||
self.compiler = SeatCompiler(DOMAIN_ROOT / "can" / "signal-mapping.yaml")
|
||||
|
||||
def test_encodes_combined_control_bits(self) -> None:
|
||||
parsed = parse_local_intent("打开主驾座椅通风,同时关闭副驾座椅加热")
|
||||
parsed = parse_local_intent("打开副驾座椅通风,同时关闭副驾座椅加热")
|
||||
config = build_function_config(parsed, "req-test-001", "local_rules")
|
||||
plan = self.compiler.compile(config)
|
||||
command = plan["spec"]["commands"][0]
|
||||
@@ -32,7 +32,7 @@ class MatrixCompilerTests(unittest.TestCase):
|
||||
frame = self.matrix.encode(command["message"], command["signals"])
|
||||
|
||||
self.assertEqual(frame["frame_id"], "0x42A")
|
||||
self.assertEqual(frame["data_hex"], "01 80")
|
||||
self.assertEqual(frame["data_hex"], "00 90")
|
||||
|
||||
def test_compilation_is_deterministic(self) -> None:
|
||||
parsed = parse_local_intent("关闭副驾座椅通风")
|
||||
@@ -46,20 +46,20 @@ class MatrixCompilerTests(unittest.TestCase):
|
||||
"actions": [
|
||||
{
|
||||
"action_type": "position",
|
||||
"target": "driver",
|
||||
"target": "passenger",
|
||||
"axis": "longitudinal",
|
||||
"position_pct": 35,
|
||||
},
|
||||
{
|
||||
"action_type": "position",
|
||||
"target": "driver",
|
||||
"target": "passenger",
|
||||
"axis": "backrest_recline",
|
||||
"position_pct": 60,
|
||||
},
|
||||
],
|
||||
"deferred_requests": [],
|
||||
"ambiguities": [],
|
||||
"decision_summary": ["Move two driver-seat axes."],
|
||||
"decision_summary": ["Move two front-passenger-seat axes."],
|
||||
"confidence": 1.0,
|
||||
}
|
||||
config = build_function_config(parsed, "req-motion-001", "test")
|
||||
@@ -68,15 +68,15 @@ class MatrixCompilerTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(len(plan["spec"]["commands"]), 2)
|
||||
first, second = plan["spec"]["commands"]
|
||||
self.assertEqual(first["message"], "Driver_Seat_Motion_Command_550")
|
||||
self.assertEqual(first["message"], "Passenger_Seat_Motion_Command_551")
|
||||
self.assertEqual(first["signals"]["Longitudinal_Target_Pct"], 35)
|
||||
self.assertEqual(first["signals"]["Motion_Enable_Mask"], 1)
|
||||
self.assertEqual(first["completion"]["message"], "Driver_Seat_Motion_Status_558")
|
||||
self.assertEqual(first["completion"]["message"], "Passenger_Seat_Motion_Status_559")
|
||||
self.assertEqual(first["completion"]["signal"], "Longitudinal_Actual_Pct")
|
||||
self.assertEqual(second["signals"]["Backrest_Recline_Target_Pct"], 60)
|
||||
self.assertEqual(second["signals"]["Motion_Enable_Mask"], 4)
|
||||
frame = self.combined_matrix.encode(first["message"], first["signals"])
|
||||
self.assertEqual(frame["frame_id"], "0x550")
|
||||
self.assertEqual(frame["frame_id"], "0x551")
|
||||
self.assertEqual(frame["payload_size"], 8)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
CYPRESS END USER LICENSE AGREEMENT
|
||||
|
||||
PLEASE READ THIS END USER LICENSE AGREEMENT ("Agreement") CAREFULLY BEFORE DOWNLOADING, INSTALLING, OR USING THIS SOFTWARE AND ACCOMPANYING DOCUMENTATION ("Software"). BY DOWNLOADING, INSTALLING, OR USING THE SOFTWARE, YOU ARE AGREEING TO BE BOUND BY THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS OF THIS AGREEMENT, PROMPTLY RETURN AND DO NOT USE THE SOFTWARE. IF YOU HAVE PURCHASED THE SOFTWARE, YOUR RIGHT TO RETURN THE SOFTWARE EXPIRES 30 DAYS AFTER YOUR PURCHASE AND APPLIES ONLY TO THE ORIGINAL PURCHASER.
|
||||
|
||||
License. Subject to the terms and conditions of this Agreement, Cypress Semiconductor Corporation ("Cypress") and its suppliers grant to you a non-exclusive, non-transferable license under its copyright rights to use the Software in object code form solely for the purpose of creating programming code for use on Cypress hardware products and transferring such programming code onto such hardware products ("Purpose"). For purposes of this Agreement, "Software" shall also include any upgrades, updates, bug fixes or modified versions provided to you by Cypress. You may make one backup copy of the Software.
|
||||
|
||||
SAMPLE (ES) SOFTWARE IS MADE AVAILABLE SOLELY FOR PURPOSES OF EVALUATION. ALL ES SOFTWARE IS PROVIDED “AS-IS” WITH NO WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED. CYPRESS DOES NOT WARRANT THAT ES SOFTWARE IS FULLY VERIFIED, TESTED, OR WILL OPERATE IN ACCORDANCE WITH DATA SHEET SPECIFICATIONS. CYPRESS DISCLAIMS ANY OBLIGATIONS FOR TECHNICAL SUPPORT AND BUG FIXES. CYPRESS SHALL NOT BE LIABLE FOR ANY DAMAGES, INCLUDING WITHOUT LIMITATION DIRECT, INDIRECT, INCIDENTAL, SPECIAL, RELIANCE, OR CONSEQUENTIAL DAMAGES ARISING FROM OR IN CONNECTION WITH THE USE OF ES SOFTWARE IN ANY MANNER WHATSOEVER, EVEN IF CYPRESS HAS BEEN ADVISED OF THE POSSIBILITY THEREOF. CYPRESS MAKES NO REPRESENTATION THAT ES SOFTWARE PROVIDE ANY PARTICULAR FUNCTIONALITY, OR THAT ES SOFTWARE WILL MEET THE REQUIREMENTS OF A PARTICULAR USER APPLICATION. CYPRESS DOES NOT WARRANT THAT ES SOFTWARE IS ERROR-FREE, NOR DOES CYPRESS MAKE ANY OTHER REPRESENTATIONS OR WARRANTIES, WHETHER EXPRESS OR IMPLIED, STATUTORY OR OTHERWISE, INCLUDING, WITHOUT LIMITATION, IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NON-INFRINGEMENT. THE FOREGOING STATES THE ENTIRE LIABILITY OF CYPRESS WITH RESPECT TO ES SOFTWARE.
|
||||
|
||||
Free and Open Source Software. Portions of the Software may be licensed under free and/or open source licenses such as the GNU General Public License ("FOSS"). FOSS is subject to the applicable license agreement and not this Agreement. If you are entitled to receive the source code from Cypress for any FOSS included with the Software, you may obtain the source code at no charge from http://www.cypress.com/go/opensource. The applicable license terms will accompany each source code package. To review the license terms applicable to any FOSS for which Cypress is not required to provide you with source code, please see the Software's installation directory on your computer.
|
||||
|
||||
Source Code. The Software may include source code of Cypress or its suppliers that is used to create applications that execute on Cypress hardware products or on other platforms in order to program Cypress hardware products ("Cypress Source Code"). Applications that you create using the Software that execute on Cypress hardware products are referred to as "Firmware." Applications that you create using the Software that execute on other platforms in order to program Cypress hardware products are referred to "Host Applications." The Cypress Source Code, Host Applications, and Firmware are subject to the license granted in this paragraph and not the license granted in the paragraph entitled "License." Subject to the terms and conditions of this Agreement, Cypress grants you a non-exclusive, non-transferable license under its copyright rights to (i) copy, use, modify, and compile the Cypress Source Code, Host Applications, and Firmware solely for the Purpose, and (ii) distribute Firmware in binary code form only, only when installed onto a Cypress hardware product, and (iii) distribute Host Applications, in binary code form only, only when installed on a device that includes a Cypress hardware product that the Host Application is intended to program, and (iv) to freely distribute any hardware setup information file (.inf file) created by the Software to allow a Microsoft Windows operating system to install the driver for a Cypress hardware product. Cypress retains ownership of the Cypress Source Code and any compiled version thereof. Subject to Cypress' ownership of the underlying Cypress Source Code and Software, you retain ownership of any modifications you make to the Cypress Source Code. You agree not to remove any Cypress copyright or other notices from the Cypress Source Code and any modifications thereof. You agree to keep the Cypress Source Code confidential. Any reproduction, modification, translation, compilation, or representation of the Cypress Source Code except as permitted in this paragraph is prohibited without the express written permission of Cypress.
|
||||
|
||||
Driver Files. The Software may include driver files in binary code form, to allow the use of a Cypress hardware product with a particular host operating system ("Driver"). Unless different license terms accompany a Driver, subject to the terms and conditions of this Agreement, Cypress grants you a non-exclusive, non-transferable license under its copyright rights to distribute the Driver, in binary code form only, only when installed on a device that includes the Cypress hardware product that the Driver is intended to enable.
|
||||
|
||||
Proprietary Rights. The Software, including all intellectual property rights therein, is and will remain the sole and exclusive property of Cypress or its suppliers. Except as otherwise expressly provided in this Agreement, you may not: (i) modify, adapt, or create derivative works based upon the Software; (ii) copy the Software; (iii) except and only to the extent explicitly permitted by applicable law despite this limitation, decompile, translate, reverse engineer, disassemble or otherwise reduce the Software to human-readable form; or (iv) use the Software or any sample code other than for the Purpose.
|
||||
|
||||
No Support. Cypress may, but is not required to, provide technical support for the Software.
|
||||
|
||||
Term and Termination. This Agreement is effective until terminated, and either party may terminate this Agreement at any time with or without cause. Your license rights under this Agreement will terminate immediately without notice from Cypress if you fail to comply with any provision of this Agreement. Upon termination, you must destroy all copies of Software in your possession or control. The following paragraphs shall survive any termination of this Agreement: "Free and Open Source Software," "Proprietary Rights," "Compliance With Law," "Disclaimer," "Limitation of Liability," and "General."
|
||||
|
||||
Compliance With Law. Each party agrees to comply with all applicable laws, rules and regulations in connection with its activities under this Agreement. Without limiting the foregoing, the Software may be subject to export control laws and regulations of the United States and other countries. You agree to comply strictly with all such laws and regulations and acknowledge that you have the responsibility to obtain licenses to export, re-export, or import the Software.
|
||||
|
||||
Disclaimer. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, CYPRESS MAKES NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, WITH REGARD TO THE SOFTWARE, INCLUDING, BUT NOT LIMITED TO, INFRINGEMENT AND THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. Cypress reserves the right to make changes to the Software without notice. Cypress does not assume any liability arising out of the application or use of Software or any product or circuit described in the Software. Cypress does not authorize its products for use as critical components in life-support systems where a malfunction or failure may reasonably be expected to result in significant injury to the user. The inclusion of Cypress' product in a life-support system or application implies that the manufacturer of such system or application assumes all risk of such use and in doing so indemnifies Cypress against all charges.
|
||||
|
||||
Limitation of Liability. IN NO EVENT WILL CYPRESS OR ITS SUPPLIERS, RESELLERS, OR DISTRIBUTORS BE LIABLE FOR ANY LOST REVENUE, PROFIT, OR DATA, OR FOR SPECIAL, INDIRECT, CONSEQUENTIAL, INCIDENTAL, OR PUNITIVE DAMAGES HOWEVER CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THE SOFTWARE EVEN IF CYPRESS OR ITS SUPPLIERS, RESELLERS, OR DISTRIBUTORS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. IN NO EVENT SHALL CYPRESS' OR ITS SUPPLIERS', RESELLERS', OR DISTRIBUTORS' TOTAL LIABILITY TO YOU, WHETHER IN CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, EXCEED THE PRICE PAID BY YOU FOR THE SOFTWARE. THE FOREGOING LIMITATIONS SHALL APPLY EVEN IF THE ABOVE-STATED WARRANTY FAILS OF ITS ESSENTIAL PURPOSE. BECAUSE SOME STATES OR JURISDICTIONS DO NOT ALLOW LIMITATION OR EXCLUSION OF CONSEQUENTIAL OR INCIDENTAL DAMAGES, THE ABOVE LIMITATION MAY NOT APPLY TO YOU.
|
||||
|
||||
Restricted Rights. The Software under this Agreement is commercial computer software as that term is described in 48 C.F.R. 252.227-7014(a)(1). If acquired by or on behalf of a civilian agency, the U.S. Government acquires this commercial computer software and/or commercial computer software documentation subject to the terms of this Agreement as specified in 48 C.F.R. 12.212 (Computer Software) and 12.211 (Technical Data) of the Federal Acquisition Regulations ("FAR") and its successors. If acquired by or on behalf of any agency within the Department of Defense ("DOD"), the U.S. Government acquires this commercial computer software and/or commercial computer software documentation subject to the terms of this Agreement as specified in 48 C.F.R. 227.7202-3 of the DOD FAR Supplement ("DFAR") and its successors.
|
||||
|
||||
General. This Agreement will bind and inure to the benefit of each party's successors and assigns, provided that you may not assign or transfer this Agreement, in whole or in part, without Cypress' written consent. This Agreement shall be governed by and construed in accordance with the laws of the State of California, United States of America, as if performed wholly within the state and without giving effect to the principles of conflict of law. The parties consent to personal and exclusive jurisdiction of and venue in, the state and federal courts within Santa Clara County, California; provided however, that nothing in this Agreement will limit Cypress' right to bring legal action in any venue in order to protect or enforce its intellectual property rights. No failure of either party to exercise or enforce any of its rights under this Agreement will act as a waiver of such rights. If any portion hereof is found to be void or unenforceable, the remaining provisions of this Agreement shall remain in full force and effect. This Agreement is the complete and exclusive agreement between the parties with respect to the subject matter hereof, superseding and replacing any and all prior agreements, communications, and understandings (both written and oral) regarding such subject matter. Any notice to Cypress will be deemed effective when actually received and must be sent to Cypress Semiconductor Corporation, ATTN: General Counsel, 198 Champion Court, San Jose, CA 95134 USA.
|
||||
|
||||
====================================================================================================================================================================================================================================
|
||||
Export control classification:
|
||||
|
||||
The Software contained in this package may be subject to export control:
|
||||
ALNR: N
|
||||
|
||||
ECCN: N_
|
||||
|
||||
ECCN-US: EAR99
|
||||
|
||||
SI-class: N
|
||||
|
||||
MYCLASS: N
|
||||
|
||||
MXCLASS: N
|
||||
|
||||
HKCLASS: N
|
||||
|
||||
Software classified with “ALNR not equal to N“ are subject to European or German export authorization when being exported out of the EU.
|
||||
|
||||
Software classified with “ECCN not equal to N“ are subject to US re-export authorization.
|
||||
|
||||
Software classified with “SICLASS not equal to N" are subject to Singaporean Strategic Goods Control authorization when being exported out of Singapore.
|
||||
|
||||
Even without a label, or with label "ALNR:N" or "ECCN:N" authorization may be required due to the final whereabouts and purpose for which the goods are to be used.
|
||||
|
||||
Furthermore, the specified export control classifications are not binding, as export control regulations are subject to interpretations, changes and possible differences in national requirements. The ultimate determination of applicable export control classifications and related categorizations remains solely your responsibility.
|
||||
|
||||
Infineon makes no representation as to the accuracy or reliability of the information provided. You agree to use this information at your own risk and to hold Infineon harmless in the event of any damages or penalties incurred by any party based on your use or reliance on the information provided here.
|
||||
+35
-28
@@ -1,37 +1,44 @@
|
||||
# T2G seat-controller firmware
|
||||
# Self-contained T2G seat-controller firmware
|
||||
|
||||
`Seat_Controller_demo/` is the repository-owned IAR interface project for the
|
||||
physical seat bench. It combines the verified heater, fan and four-motor demos
|
||||
and is the only firmware accepted by the hardware executor.
|
||||
`Seat_Controller_demo/` is the repository-owned IAR project used by the real
|
||||
seat-bench executor. It contains the 2026-07-30 heater, ventilation, four-axis
|
||||
motion, SMV8 position memory, Hall feedback and ADC current-diagnostic code.
|
||||
|
||||
The Infineon/T2G vendor library is intentionally not duplicated here. The IAR
|
||||
project keeps its original relative `common/` and `tviibh8m/` references;
|
||||
`tools/build_seat_firmware.ps1` stages the repository project beside junctions
|
||||
to `COGNITIVE_AGENT_T2G_SDK_ROOT`, builds it, and copies only the resulting
|
||||
`Seat_Controller_demo.hex` and `Seat_Controller_demo.out` back into the
|
||||
repository.
|
||||
The matching Cypress 7.9.0 build dependencies are vendored beside it:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\build_seat_firmware.ps1 -Profile combined
|
||||
```text
|
||||
firmware/t2g/
|
||||
├── common/
|
||||
├── tviibh8m/
|
||||
└── Seat_Controller_demo/
|
||||
```
|
||||
|
||||
The build script never opens a debug probe or flashes a target. Physical
|
||||
programming is performed only by an authorized `execution_mode=hardware`
|
||||
request when `COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED=true` and the CAN
|
||||
identity handshake does not match the repository firmware.
|
||||
This preserves the IAR project's `../../common` and `../../tviibh8m` paths and
|
||||
removes any dependency on the original Desktop SDK directory. The applicable
|
||||
vendor terms are retained in `CYPRESS_SDK_LICENSE.txt`. An IAR installation is
|
||||
still a toolchain prerequisite, just as the ZLG driver remains a CAN-adapter
|
||||
prerequisite; neither is project source data.
|
||||
|
||||
Automatic programming uses `IAR/Seat_Controller_program.mac`, not the
|
||||
interactive-debug `Seat_Controller_demo.mac`. The automatic-programming macro
|
||||
forces motor, fan and heater outputs low before download, clears the retained
|
||||
debug-session marker, and uses a bounded Download-and-Debug handoff with
|
||||
`--leave_target_running`. `--download_only` is intentionally not used because
|
||||
it leaves the CM0+ stopped after programming.
|
||||
Build without flashing:
|
||||
|
||||
Firmware identity protocol:
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\build_seat_firmware.ps1
|
||||
```
|
||||
|
||||
- request: `0x304`, `53 45 41 54 01 00 00 00`
|
||||
- expected response: `0x305`, `53 45 41 54 01 01 00 3F`
|
||||
Set `COGNITIVE_AGENT_IAR_BUILD` or pass `-IarBuild` when `iarbuild.exe` is not
|
||||
at a standard location. The script stages the project beside repository-local
|
||||
vendor junctions, copies the resulting HEX/OUT back, removes build products,
|
||||
and never opens a debug probe.
|
||||
|
||||
The response identifies protocol 1, firmware 1.1.0 and the combined capability
|
||||
mask. It is a non-actuating readiness probe and is checked before any seat
|
||||
control frame is sent.
|
||||
The latest firmware uses periodic CAN `0x304`/`0x305` for motor and thermal
|
||||
current diagnostics. The backend therefore verifies it passively by observing
|
||||
`0x301` through `0x305`, `0x4FA`, all four motor IDs and MaxHall
|
||||
`480/374/917/647`; it sends no identity request and no actuator frame during
|
||||
verification.
|
||||
|
||||
When real hardware execution is enabled, automatic programming defaults to
|
||||
enabled and uses `IAR/Seat_Controller_program.mac` to force outputs safe before
|
||||
download. A missing protocol signature triggers programming, a boot wait and
|
||||
passive re-verification before the original request continues. Set
|
||||
`COGNITIVE_AGENT_SEAT_AUTO_FLASH_ENABLED=false` to opt out explicitly; dry-run
|
||||
execution never programs hardware.
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
#include "cy_project.h"
|
||||
#include "cy_device_headers.h"
|
||||
#include "bsp_seat_move_can.h"
|
||||
#include "motor_control.h"
|
||||
#include "safety_manager.h"
|
||||
#include "seat_move_application.h"
|
||||
@@ -31,8 +30,6 @@ static void SeatMove_HoldSafeOff(void)
|
||||
|
||||
int main(void)
|
||||
{
|
||||
bool startupInhibited;
|
||||
|
||||
(void)BspSeatBackrestHeater_ForceOff();
|
||||
(void)BspSeatFan_ForceOff();
|
||||
SystemInit();
|
||||
@@ -48,38 +45,28 @@ int main(void)
|
||||
Cy_SysReset_ClearAllResetReasons();
|
||||
|
||||
SafetyManager_Init(g_seatMoveResetReason);
|
||||
startupInhibited = SafetyManager_IsInhibited();
|
||||
if(SafetyManager_IsInhibited())
|
||||
{
|
||||
SeatMove_HoldSafeOff();
|
||||
}
|
||||
|
||||
__enable_irq();
|
||||
g_seatMoveInitResult = Motor_Init();
|
||||
if(g_seatMoveInitResult != MOTOR_API_SUCCESS)
|
||||
{
|
||||
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
|
||||
}
|
||||
|
||||
/* A retained reset/debug fault must remain fail-safe, but it must not
|
||||
* suppress CAN diagnostics. Motor_Init() establishes the timer and SPI
|
||||
* state needed by status reporting, so re-apply the inhibit afterwards. */
|
||||
if(startupInhibited)
|
||||
{
|
||||
Motor_LatchSafetyInhibitFromIsr(0u);
|
||||
Motor_ForceSafeOff();
|
||||
SeatMove_HoldSafeOff();
|
||||
}
|
||||
|
||||
Motor_SetWatchdogServiceEnabled(true);
|
||||
if(!SeatControllerThermal_Init())
|
||||
{
|
||||
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
|
||||
SeatMove_HoldSafeOff();
|
||||
}
|
||||
if(!SeatMoveApplication_Init())
|
||||
{
|
||||
/* When CAN itself could not initialize there is no diagnostic path to
|
||||
* preserve. Other startup failures remain visible through identity
|
||||
* and inhibited status frames while every actuator stays safe-off. */
|
||||
if(g_bspSeatMoveCanInitStatus != (uint32_t)CY_CANFD_SUCCESS)
|
||||
{
|
||||
SeatMove_HoldSafeOff();
|
||||
}
|
||||
SeatMove_HoldSafeOff();
|
||||
}
|
||||
|
||||
for(;;)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "bsp_seat_backrest_heater.h"
|
||||
#include "bsp_seat_backrest_heater_adc.h"
|
||||
#include "motor_control.h"
|
||||
#include "seat_backrest_heater_config.h"
|
||||
|
||||
volatile seat_backrest_heater_level_t g_seatBackrestHeaterRequestedLevel =
|
||||
@@ -19,6 +20,7 @@ volatile uint16_t g_seatBackrestHeaterSenseRaw = 0u;
|
||||
volatile uint32_t g_seatBackrestHeaterEstimatedCurrentMa = 0u;
|
||||
|
||||
static bool s_faultLatched = false;
|
||||
static uint32_t s_activeStartTimeMs = 0u;
|
||||
static uint32_t s_senseSampleElapsedMs = 0u;
|
||||
static uint32_t s_senseConsecutiveErrors = 0u;
|
||||
|
||||
@@ -48,6 +50,7 @@ bool SeatBackrestHeater_Init(void)
|
||||
}
|
||||
|
||||
s_faultLatched = false;
|
||||
s_activeStartTimeMs = 0u;
|
||||
s_senseSampleElapsedMs = 0u;
|
||||
s_senseConsecutiveErrors = 0u;
|
||||
g_seatBackrestHeaterRequestedLevel =
|
||||
@@ -92,7 +95,12 @@ void SeatBackrestHeater_Task1ms(void)
|
||||
if(g_seatBackrestHeaterAppliedLevel !=
|
||||
SEAT_BACKREST_HEATER_LEVEL_OFF)
|
||||
{
|
||||
g_seatBackrestHeaterActiveTimeMs++;
|
||||
/* The 1 ms application loop includes CAN service, ADC work and ISR
|
||||
* latency, so counting loop iterations makes a nominal 600 s limit
|
||||
* drift several seconds late. Use the motor driver's hardware
|
||||
* monotonic timebase, which is already running before thermal init. */
|
||||
g_seatBackrestHeaterActiveTimeMs =
|
||||
(uint32_t)(Motor_GetTimeMs() - s_activeStartTimeMs);
|
||||
if(g_seatBackrestHeaterActiveTimeMs >=
|
||||
SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS)
|
||||
{
|
||||
@@ -160,6 +168,7 @@ bool SeatBackrestHeater_ClearFault(void)
|
||||
g_seatBackrestHeaterState = SEAT_BACKREST_HEATER_STATE_OFF;
|
||||
g_seatBackrestHeaterDutyPercent = 0u;
|
||||
g_seatBackrestHeaterActiveTimeMs = 0u;
|
||||
s_activeStartTimeMs = 0u;
|
||||
s_senseConsecutiveErrors = 0u;
|
||||
return true;
|
||||
}
|
||||
@@ -183,6 +192,7 @@ static bool SeatBackrestHeater_ApplyLevel(
|
||||
g_seatBackrestHeaterState = SEAT_BACKREST_HEATER_STATE_OFF;
|
||||
g_seatBackrestHeaterDutyPercent = 0u;
|
||||
g_seatBackrestHeaterActiveTimeMs = 0u;
|
||||
s_activeStartTimeMs = 0u;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -207,6 +217,7 @@ static bool SeatBackrestHeater_ApplyLevel(
|
||||
if(g_seatBackrestHeaterAppliedLevel ==
|
||||
SEAT_BACKREST_HEATER_LEVEL_OFF)
|
||||
{
|
||||
s_activeStartTimeMs = Motor_GetTimeMs();
|
||||
g_seatBackrestHeaterActiveTimeMs = 0u;
|
||||
}
|
||||
g_seatBackrestHeaterAppliedLevel = level;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "seat_controller_thermal.h"
|
||||
|
||||
#include "bsp_seat_backrest_heater.h"
|
||||
#include "bsp_seat_current_adc.h"
|
||||
#include "bsp_seat_fan.h"
|
||||
#include "safety_manager.h"
|
||||
#include "seat_backrest_heater.h"
|
||||
@@ -12,6 +13,7 @@
|
||||
#define SEAT_CONTROLLER_THERMAL_MODE_ID (0x3CEu)
|
||||
#define SEAT_CONTROLLER_THERMAL_SWITCH_ID (0x42Au)
|
||||
#define SEAT_CONTROLLER_THERMAL_FEEDBACK_ID (0x4FAu)
|
||||
#define SEAT_CONTROLLER_THERMAL_CURRENT_DIAGNOSTIC_ID (0x305u)
|
||||
#define SEAT_CONTROLLER_THERMAL_FEEDBACK_MS (100u)
|
||||
|
||||
volatile uint8_t g_seatControllerFanFeedbackRaw = 1u;
|
||||
@@ -20,6 +22,9 @@ volatile uint32_t g_seatControllerThermalRxCount = 0u;
|
||||
volatile uint32_t g_seatControllerThermalInvalidCount = 0u;
|
||||
volatile uint32_t g_seatControllerThermalFeedbackTxCount = 0u;
|
||||
volatile uint32_t g_seatControllerThermalFeedbackTxFailureCount = 0u;
|
||||
volatile uint32_t g_seatControllerThermalOutputFaultStage = 0u;
|
||||
volatile uint16_t g_seatControllerFanCurrentSenseRaw = 0u;
|
||||
volatile uint32_t g_seatControllerFanCurrentAdcErrorCount = 0u;
|
||||
|
||||
static bool s_fanRunning = false;
|
||||
static bool s_heaterModeValid = false;
|
||||
@@ -32,6 +37,8 @@ static uint8_t s_heaterLastVehicleGear =
|
||||
static uint8_t s_heaterConfiguredMaxLevel = 3u;
|
||||
static uint32_t s_fanStartupRemainingMs = 0u;
|
||||
static uint32_t s_feedbackElapsedMs = 0u;
|
||||
static uint32_t s_fanCurrentSampleElapsedMs = 0u;
|
||||
static bool s_fanCurrentSampleValid = false;
|
||||
|
||||
static void SeatControllerThermal_HandleControl29e(
|
||||
const bsp_seat_move_can_frame_t *frame);
|
||||
@@ -48,9 +55,12 @@ static void SeatControllerThermal_StopHeater(uint8_t feedbackRaw);
|
||||
static uint8_t SeatControllerThermal_GearToDuty(uint8_t physicalGear);
|
||||
static void SeatControllerThermal_LatchOutputFault(void);
|
||||
static void SeatControllerThermal_ServiceFeedback(void);
|
||||
static void SeatControllerThermal_ServiceFanCurrent(void);
|
||||
|
||||
bool SeatControllerThermal_Init(void)
|
||||
{
|
||||
uint16_t fanCurrentRaw;
|
||||
|
||||
if(!SeatBackrestHeater_Init())
|
||||
{
|
||||
SeatControllerThermal_ForceSafeOff();
|
||||
@@ -61,6 +71,16 @@ bool SeatControllerThermal_Init(void)
|
||||
SeatControllerThermal_ForceSafeOff();
|
||||
return false;
|
||||
}
|
||||
if(!BspSeatCurrentAdc_Init() ||
|
||||
!BspSeatCurrentAdc_Sample(BSP_SEAT_CURRENT_ADC_FAN,
|
||||
&fanCurrentRaw))
|
||||
{
|
||||
SeatControllerThermal_ForceSafeOff();
|
||||
return false;
|
||||
}
|
||||
g_seatControllerFanCurrentSenseRaw = fanCurrentRaw;
|
||||
s_fanCurrentSampleValid = true;
|
||||
s_fanCurrentSampleElapsedMs = 0u;
|
||||
|
||||
SeatControllerThermal_StopFan(1u);
|
||||
SeatControllerThermal_StopHeater(1u);
|
||||
@@ -101,6 +121,7 @@ void SeatControllerThermal_HandleFrame(const bsp_seat_move_can_frame_t *frame)
|
||||
void SeatControllerThermal_Service1ms(void)
|
||||
{
|
||||
SeatBackrestHeater_Task1ms();
|
||||
SeatControllerThermal_ServiceFanCurrent();
|
||||
if((g_seatBackrestHeaterState ==
|
||||
SEAT_BACKREST_HEATER_STATE_FAULT_SAFE_OFF) ||
|
||||
(g_seatBackrestHeaterState ==
|
||||
@@ -131,6 +152,7 @@ void SeatControllerThermal_Service1ms(void)
|
||||
if((s_fanStartupRemainingMs == 0u) &&
|
||||
!BspSeatFan_SetDutyPercent(s_fanTargetDutyPercent))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 1u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -138,6 +160,28 @@ void SeatControllerThermal_Service1ms(void)
|
||||
SeatControllerThermal_ServiceFeedback();
|
||||
}
|
||||
|
||||
static void SeatControllerThermal_ServiceFanCurrent(void)
|
||||
{
|
||||
uint16_t raw;
|
||||
|
||||
s_fanCurrentSampleElapsedMs++;
|
||||
if(s_fanCurrentSampleElapsedMs < SEAT_FAN_CURRENT_SAMPLE_MS)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s_fanCurrentSampleElapsedMs = 0u;
|
||||
if(BspSeatCurrentAdc_Sample(BSP_SEAT_CURRENT_ADC_FAN, &raw))
|
||||
{
|
||||
g_seatControllerFanCurrentSenseRaw = raw;
|
||||
s_fanCurrentSampleValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
g_seatControllerFanCurrentAdcErrorCount++;
|
||||
s_fanCurrentSampleValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
void SeatControllerThermal_ForceSafeOff(void)
|
||||
{
|
||||
SeatBackrestHeater_EmergencyOff();
|
||||
@@ -159,7 +203,7 @@ static void SeatControllerThermal_HandleControl29e(
|
||||
uint8_t heaterOn;
|
||||
uint8_t heaterGear;
|
||||
|
||||
if(frame->dlc < 3u)
|
||||
if(frame->dlc != SEAT_FAN_CAN_DLC)
|
||||
{
|
||||
g_seatControllerThermalInvalidCount++;
|
||||
SeatControllerThermal_StopFan(0u);
|
||||
@@ -183,6 +227,7 @@ static void SeatControllerThermal_HandleControl29e(
|
||||
{
|
||||
if(!SeatControllerThermal_RequestFan(fanGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 2u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -202,6 +247,7 @@ static void SeatControllerThermal_HandleControl29e(
|
||||
{
|
||||
if(!SeatControllerThermal_RequestHeater(heaterGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 3u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -219,7 +265,7 @@ static void SeatControllerThermal_HandleConfig2aa(
|
||||
uint8_t fanConfig;
|
||||
uint8_t heaterConfig;
|
||||
|
||||
if(frame->dlc < 3u)
|
||||
if(frame->dlc != SEAT_FAN_CAN_DLC)
|
||||
{
|
||||
g_seatControllerThermalInvalidCount++;
|
||||
return;
|
||||
@@ -239,6 +285,7 @@ static void SeatControllerThermal_HandleConfig2aa(
|
||||
if(s_fanRunning &&
|
||||
!SeatControllerThermal_RequestFan(s_fanLastVehicleGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 4u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
if(g_seatBackrestHeaterAppliedLevel !=
|
||||
@@ -246,6 +293,7 @@ static void SeatControllerThermal_HandleConfig2aa(
|
||||
{
|
||||
if(!SeatControllerThermal_RequestHeater(s_heaterLastVehicleGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 5u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -255,7 +303,7 @@ static void SeatControllerThermal_HandleMode3ce(
|
||||
const bsp_seat_move_can_frame_t *frame)
|
||||
{
|
||||
uint8_t mode;
|
||||
if(frame->dlc < 1u)
|
||||
if(frame->dlc != SEAT_FAN_CAN_DLC)
|
||||
{
|
||||
s_heaterModeValid = false;
|
||||
g_seatControllerThermalInvalidCount++;
|
||||
@@ -276,7 +324,7 @@ static void SeatControllerThermal_HandleSwitch42a(
|
||||
{
|
||||
uint8_t fanSwitch;
|
||||
uint8_t heaterSwitch;
|
||||
if(frame->dlc < 1u)
|
||||
if(frame->dlc != SEAT_FAN_CAN_DLC)
|
||||
{
|
||||
g_seatControllerThermalInvalidCount++;
|
||||
SeatControllerThermal_StopFan(0u);
|
||||
@@ -289,6 +337,7 @@ static void SeatControllerThermal_HandleSwitch42a(
|
||||
{
|
||||
if(!SeatControllerThermal_RequestFan(s_fanLastVehicleGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 6u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -300,6 +349,7 @@ static void SeatControllerThermal_HandleSwitch42a(
|
||||
{
|
||||
if(!SeatControllerThermal_RequestHeater(s_heaterLastVehicleGear))
|
||||
{
|
||||
g_seatControllerThermalOutputFaultStage = 7u;
|
||||
SeatControllerThermal_LatchOutputFault();
|
||||
}
|
||||
}
|
||||
@@ -405,6 +455,13 @@ static void SeatControllerThermal_LatchOutputFault(void)
|
||||
static void SeatControllerThermal_ServiceFeedback(void)
|
||||
{
|
||||
uint32_t data0;
|
||||
uint32_t currentData0;
|
||||
uint32_t currentData1;
|
||||
uint8_t currentFlags;
|
||||
uint8_t outputState;
|
||||
uint8_t fanErrors;
|
||||
uint8_t heaterErrors;
|
||||
|
||||
s_feedbackElapsedMs++;
|
||||
if(!s_feedbackPending &&
|
||||
(s_feedbackElapsedMs < SEAT_CONTROLLER_THERMAL_FEEDBACK_MS))
|
||||
@@ -413,7 +470,31 @@ static void SeatControllerThermal_ServiceFeedback(void)
|
||||
}
|
||||
data0 = ((uint32_t)g_seatControllerFanFeedbackRaw & 0x07u) |
|
||||
(((uint32_t)g_seatControllerHeaterFeedbackRaw & 0x07u) << 3u);
|
||||
if(BspSeatMoveCan_Send(SEAT_CONTROLLER_THERMAL_FEEDBACK_ID, data0, 0u))
|
||||
currentFlags = (uint8_t)
|
||||
((s_fanCurrentSampleValid ? 0x01u : 0u) |
|
||||
((g_seatBackrestHeaterState !=
|
||||
SEAT_BACKREST_HEATER_STATE_FAULT_SAFE_OFF) ? 0x02u : 0u));
|
||||
outputState = (uint8_t)
|
||||
((s_fanRunning ? 0x01u : 0u) |
|
||||
(((uint8_t)g_seatBackrestHeaterAppliedLevel & 0x03u) << 1u));
|
||||
fanErrors = (g_seatControllerFanCurrentAdcErrorCount > UINT8_MAX) ?
|
||||
UINT8_MAX :
|
||||
(uint8_t)g_seatControllerFanCurrentAdcErrorCount;
|
||||
heaterErrors = (g_seatBackrestHeaterAdcErrorCount > UINT8_MAX) ?
|
||||
UINT8_MAX :
|
||||
(uint8_t)g_seatBackrestHeaterAdcErrorCount;
|
||||
currentData0 = (uint32_t)currentFlags |
|
||||
((uint32_t)outputState << 8u) |
|
||||
((uint32_t)g_seatControllerFanCurrentSenseRaw << 16u);
|
||||
currentData1 = (uint32_t)g_seatBackrestHeaterSenseRaw |
|
||||
((uint32_t)fanErrors << 16u) |
|
||||
((uint32_t)heaterErrors << 24u);
|
||||
|
||||
if(BspSeatMoveCan_Send(SEAT_CONTROLLER_THERMAL_FEEDBACK_ID, data0, 0u) &&
|
||||
BspSeatMoveCan_Send(
|
||||
SEAT_CONTROLLER_THERMAL_CURRENT_DIAGNOSTIC_ID,
|
||||
currentData0,
|
||||
currentData1))
|
||||
{
|
||||
g_seatControllerThermalFeedbackTxCount++;
|
||||
s_feedbackPending = false;
|
||||
|
||||
@@ -17,5 +17,9 @@ extern volatile uint32_t g_seatControllerThermalRxCount;
|
||||
extern volatile uint32_t g_seatControllerThermalInvalidCount;
|
||||
extern volatile uint32_t g_seatControllerThermalFeedbackTxCount;
|
||||
extern volatile uint32_t g_seatControllerThermalFeedbackTxFailureCount;
|
||||
/* Read-only HIL diagnostic; see seat_controller_thermal.c stage constants. */
|
||||
extern volatile uint32_t g_seatControllerThermalOutputFaultStage;
|
||||
extern volatile uint16_t g_seatControllerFanCurrentSenseRaw;
|
||||
extern volatile uint32_t g_seatControllerFanCurrentAdcErrorCount;
|
||||
|
||||
#endif /* SEAT_CONTROLLER_THERMAL_H */
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "seat_motor_manager.h"
|
||||
|
||||
#include "seat_move_config.h"
|
||||
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -7,6 +9,7 @@
|
||||
#define SEAT_MOTOR_SPEED_CAL_WARMUP_MS (500ul)
|
||||
#define SEAT_MOTOR_SPEED_CAL_MEASURE_MS (1000ul)
|
||||
#define SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT (95u)
|
||||
#define SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS (70000ul)
|
||||
|
||||
typedef char seat_motor_manager_requires_four_motors[
|
||||
(MOTOR_MODE_COUNT == 4) ? 1 : -1];
|
||||
@@ -48,7 +51,7 @@ static uint32_t SeatMotor_MaxValueX100(motor_mode_t mode)
|
||||
}
|
||||
if(mode == MOTOR_MODE_1_ANGLE)
|
||||
{
|
||||
return MOTOR_MODE1_MAX_ANGLE_DEG_X100;
|
||||
return MOTOR_MODE1_MAX_VERTICAL_MM_X100;
|
||||
}
|
||||
if(mode == MOTOR_MODE_2_ANGLE)
|
||||
{
|
||||
@@ -56,11 +59,32 @@ static uint32_t SeatMotor_MaxValueX100(motor_mode_t mode)
|
||||
}
|
||||
if(mode == MOTOR_MODE_3_LINEAR_TRACK)
|
||||
{
|
||||
return MOTOR_MODE3_MAX_POSITION_MM_X100;
|
||||
return MOTOR_MODE3_MAX_ANGLE_DEG_X100;
|
||||
}
|
||||
return 0ul;
|
||||
}
|
||||
|
||||
static uint8_t SeatMotor_DefaultDutyPercent(motor_mode_t mode)
|
||||
{
|
||||
if(mode == MOTOR_MODE_0_LINEAR)
|
||||
{
|
||||
return SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT;
|
||||
}
|
||||
if(mode == MOTOR_MODE_1_ANGLE)
|
||||
{
|
||||
return SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT;
|
||||
}
|
||||
if(mode == MOTOR_MODE_2_ANGLE)
|
||||
{
|
||||
return SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT;
|
||||
}
|
||||
if(mode == MOTOR_MODE_3_LINEAR_TRACK)
|
||||
{
|
||||
return SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
static bool SeatMotor_TimeReached(uint32_t now, uint32_t deadline)
|
||||
{
|
||||
return ((int32_t)(now - deadline) >= 0);
|
||||
@@ -71,6 +95,26 @@ static uint32_t SeatMotor_AbsDelta(uint32_t lhs, uint32_t rhs)
|
||||
return (lhs >= rhs) ? (lhs - rhs) : (rhs - lhs);
|
||||
}
|
||||
|
||||
static bool SeatMotor_TargetWithinHallTolerance(
|
||||
const seat_motor_task_t *task,
|
||||
const motor_position_info_t *info)
|
||||
{
|
||||
uint32_t targetHallCount;
|
||||
|
||||
if((NULL == task) || (NULL == info) ||
|
||||
(false == info->positionValid) || (0ul == info->maxValueX100) ||
|
||||
(0ul == info->maxHallCount) ||
|
||||
(task->targetValueX100 > info->maxValueX100))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
targetHallCount = (uint32_t)
|
||||
(((uint64_t)task->targetValueX100 * info->maxHallCount +
|
||||
(info->maxValueX100 / 2ul)) / info->maxValueX100);
|
||||
return SeatMotor_AbsDelta(targetHallCount, info->hallCount) <=
|
||||
SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE;
|
||||
}
|
||||
|
||||
static bool SeatMotor_DutyValid(uint8_t dutyPercent)
|
||||
{
|
||||
return (dutyPercent >= MOTOR_SPEED_MIN_DUTY_PERCENT) &&
|
||||
@@ -496,6 +540,16 @@ void SeatMotorManager_Service(void)
|
||||
(info.lastEvent == MOTOR_EVENT_FAR_ENDPOINT);
|
||||
if((info.lastEvent == MOTOR_EVENT_STALL) || endpointEvent)
|
||||
{
|
||||
/* The low-level safety event has already stopped the hardware.
|
||||
* At a discrete Hall endpoint, the mechanism may stop up to two
|
||||
* pulses short of the mathematically rounded target. This is a
|
||||
* successful position arrival, not a fault/retry condition. */
|
||||
if(SeatMotor_TaskIsPosition(task) &&
|
||||
SeatMotor_TargetWithinHallTolerance(task, &info))
|
||||
{
|
||||
SeatMotor_SetComplete(mode);
|
||||
continue;
|
||||
}
|
||||
if(SeatMotor_TaskIsPosition(task) &&
|
||||
(0u == task->retryCount))
|
||||
{
|
||||
@@ -619,6 +673,69 @@ bool SeatMotor_MoveRelativeDuty(motor_mode_t mode,
|
||||
commandSequence);
|
||||
}
|
||||
|
||||
bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode,
|
||||
uint32_t targetPercentX100,
|
||||
uint32_t timeoutMs,
|
||||
uint8_t dutyPercent,
|
||||
uint32_t commandSequence)
|
||||
{
|
||||
motor_position_info_t info;
|
||||
uint32_t targetValueX100;
|
||||
|
||||
if((targetPercentX100 > 10000ul) || (0ul == timeoutMs) ||
|
||||
(MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
|
||||
(false == info.positionValid) || (0ul == info.maxValueX100))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
targetValueX100 = (uint32_t)
|
||||
((((uint64_t)targetPercentX100 * info.maxValueX100) + 5000ull) /
|
||||
10000ull);
|
||||
return SeatMotor_StartPositionTask(mode,
|
||||
targetValueX100,
|
||||
dutyPercent,
|
||||
0ul,
|
||||
timeoutMs,
|
||||
commandSequence,
|
||||
false,
|
||||
false,
|
||||
SEAT_MOTOR_TASK_POSITION);
|
||||
}
|
||||
|
||||
bool SeatMotor_MoveRelativeNormalized(motor_mode_t mode,
|
||||
int32_t deltaPercentX100,
|
||||
uint32_t timeoutMs,
|
||||
uint8_t dutyPercent,
|
||||
uint32_t commandSequence)
|
||||
{
|
||||
motor_position_info_t info;
|
||||
int32_t targetPercentX100;
|
||||
|
||||
if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) ||
|
||||
(false == info.positionValid))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
targetPercentX100 = (int32_t)info.percentX100 + deltaPercentX100;
|
||||
if(targetPercentX100 < 0)
|
||||
{
|
||||
targetPercentX100 = 0;
|
||||
}
|
||||
else if(targetPercentX100 > 10000)
|
||||
{
|
||||
targetPercentX100 = 10000;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Target already lies in the normalized range. */
|
||||
}
|
||||
return SeatMotor_MoveAbsoluteNormalized(mode,
|
||||
(uint32_t)targetPercentX100,
|
||||
timeoutMs,
|
||||
dutyPercent,
|
||||
commandSequence);
|
||||
}
|
||||
|
||||
bool SeatMotor_MoveTimedDuty(motor_mode_t mode,
|
||||
motor_direction_t direction,
|
||||
uint32_t durationMs,
|
||||
@@ -793,19 +910,27 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
|
||||
SEAT_MOTOR_TASK_POSITION);
|
||||
}
|
||||
|
||||
bool SeatMotor_CalibrateRange(motor_mode_t mode, uint32_t commandSequence)
|
||||
bool SeatMotor_CalibrateRange(motor_mode_t mode,
|
||||
uint32_t legTimeoutMs,
|
||||
uint32_t commandSequence)
|
||||
{
|
||||
motor_calibration_result_t result;
|
||||
|
||||
if(false == SeatMotor_ModeValid(mode))
|
||||
if((false == SeatMotor_ModeValid(mode)) || (0ul == legTimeoutMs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if((false == SeatMotor_PrepareNewTask(mode, commandSequence)) ||
|
||||
(MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(
|
||||
mode, MOTOR_SPEED_MIN_DUTY_PERCENT)) ||
|
||||
(MOTOR_API_SUCCESS != Motor_Calibrate(mode,
|
||||
mode, MOTOR_SPEED_MIN_DUTY_PERCENT)))
|
||||
{
|
||||
SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED);
|
||||
return false;
|
||||
}
|
||||
tasks[(uint32_t)mode].state = SEAT_MOTOR_TASK_RANGE_CALIBRATION;
|
||||
if((MOTOR_API_SUCCESS != Motor_Calibrate(mode,
|
||||
MOTOR_CALIBRATE_FIND_BOTH,
|
||||
legTimeoutMs,
|
||||
&result)) ||
|
||||
(false == result.nearEndpointFound) ||
|
||||
(false == result.farEndpointFound))
|
||||
@@ -817,6 +942,39 @@ bool SeatMotor_CalibrateRange(motor_mode_t mode, uint32_t commandSequence)
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SeatMotor_HomeKnownRange(motor_mode_t mode,
|
||||
uint32_t legTimeoutMs,
|
||||
uint32_t commandSequence)
|
||||
{
|
||||
motor_calibration_result_t result;
|
||||
uint8_t dutyPercent;
|
||||
|
||||
if((false == SeatMotor_ModeValid(mode)) || (0ul == legTimeoutMs))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
dutyPercent = SeatMotor_DefaultDutyPercent(mode);
|
||||
if((false == SeatMotor_PrepareNewTask(mode, commandSequence)) ||
|
||||
(MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(
|
||||
mode, dutyPercent)))
|
||||
{
|
||||
SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED);
|
||||
return false;
|
||||
}
|
||||
tasks[(uint32_t)mode].state = SEAT_MOTOR_TASK_RANGE_CALIBRATION;
|
||||
if((MOTOR_API_SUCCESS != Motor_Calibrate(mode,
|
||||
MOTOR_CALIBRATE_FIND_NEAR,
|
||||
legTimeoutMs,
|
||||
&result)) ||
|
||||
(false == result.nearEndpointFound))
|
||||
{
|
||||
SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED);
|
||||
return false;
|
||||
}
|
||||
SeatMotor_SetComplete(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SeatMotor_StartMaximumSpeedCalibration(motor_mode_t mode,
|
||||
uint32_t commandSequence)
|
||||
{
|
||||
@@ -852,6 +1010,27 @@ bool SeatMotor_StartMaximumSpeedCalibration(motor_mode_t mode,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SeatMotorManager_ClearFault(motor_mode_t mode)
|
||||
{
|
||||
seat_motor_task_t *task;
|
||||
uint32_t sequence;
|
||||
|
||||
if((false == SeatMotor_ModeValid(mode)) || Motor_IsMoving(mode) ||
|
||||
Motor_IsDriverFaultLatched())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
task = &tasks[(uint32_t)mode];
|
||||
if(false != task->active)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
sequence = task->commandSequence;
|
||||
(void)memset(task, 0, sizeof(*task));
|
||||
task->commandSequence = sequence;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SeatMotor_RecoverPosition(motor_mode_t mode, uint32_t savedHallCount)
|
||||
{
|
||||
motor_calibration_result_t calibration;
|
||||
@@ -860,6 +1039,7 @@ bool SeatMotor_RecoverPosition(motor_mode_t mode, uint32_t savedHallCount)
|
||||
|
||||
if((MOTOR_API_SUCCESS != Motor_Calibrate(mode,
|
||||
MOTOR_CALIBRATE_FIND_NEAR,
|
||||
SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS,
|
||||
&calibration)) ||
|
||||
(false == calibration.nearEndpointFound) ||
|
||||
(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &state)))
|
||||
@@ -911,7 +1091,14 @@ bool SeatMotorManager_GetStatus(motor_mode_t mode,
|
||||
status->hallCount = position.hallCount;
|
||||
status->maxHallCount = position.maxHallCount;
|
||||
status->maxSpeedPpsX100 = persistent.maxSpeedPulsesPerSecondX100;
|
||||
status->hallSpeedPpsX100 = speed.actualPulsesPerSecondX100;
|
||||
status->positionPercentX100 = position.percentX100;
|
||||
status->positionMmX100 = position.positionMmX100;
|
||||
status->horizontalMmX100 = position.horizontalMmX100;
|
||||
status->verticalMmX100 = position.verticalMmX100;
|
||||
status->angleDegX100 = position.angleDegX100;
|
||||
status->dutyPercent = speed.currentDutyPercent;
|
||||
status->retryCount = task->retryCount;
|
||||
status->active = task->active;
|
||||
status->retried = (task->retryCount != 0u);
|
||||
status->positionValid = position.positionValid;
|
||||
|
||||
@@ -13,6 +13,7 @@ typedef enum
|
||||
SEAT_MOTOR_TASK_POSITION,
|
||||
SEAT_MOTOR_TASK_TIMED,
|
||||
SEAT_MOTOR_TASK_RETRY_DELAY,
|
||||
SEAT_MOTOR_TASK_RANGE_CALIBRATION,
|
||||
SEAT_MOTOR_TASK_SPEED_CAL_WARMUP,
|
||||
SEAT_MOTOR_TASK_SPEED_CAL_MEASURE,
|
||||
SEAT_MOTOR_TASK_SPEED_CAL_SETTLE,
|
||||
@@ -45,7 +46,14 @@ typedef struct
|
||||
uint32_t hallCount;
|
||||
uint32_t maxHallCount;
|
||||
uint32_t maxSpeedPpsX100;
|
||||
uint32_t hallSpeedPpsX100;
|
||||
uint32_t positionPercentX100;
|
||||
uint32_t positionMmX100;
|
||||
uint32_t horizontalMmX100;
|
||||
uint32_t verticalMmX100;
|
||||
uint32_t angleDegX100;
|
||||
uint8_t dutyPercent;
|
||||
uint8_t retryCount;
|
||||
bool active;
|
||||
bool retried;
|
||||
bool positionValid;
|
||||
@@ -64,6 +72,16 @@ bool SeatMotor_MoveAbsoluteDuty(motor_mode_t mode,
|
||||
uint32_t targetValueX100,
|
||||
uint8_t dutyPercent,
|
||||
uint32_t commandSequence);
|
||||
bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode,
|
||||
uint32_t targetPercentX100,
|
||||
uint32_t timeoutMs,
|
||||
uint8_t dutyPercent,
|
||||
uint32_t commandSequence);
|
||||
bool SeatMotor_MoveRelativeNormalized(motor_mode_t mode,
|
||||
int32_t deltaPercentX100,
|
||||
uint32_t timeoutMs,
|
||||
uint8_t dutyPercent,
|
||||
uint32_t commandSequence);
|
||||
bool SeatMotor_MoveRelativeDuty(motor_mode_t mode,
|
||||
int32_t deltaValueX100,
|
||||
uint8_t dutyPercent,
|
||||
@@ -94,9 +112,17 @@ bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
|
||||
uint32_t commandSequence);
|
||||
|
||||
/* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */
|
||||
bool SeatMotor_CalibrateRange(motor_mode_t mode, uint32_t commandSequence);
|
||||
bool SeatMotor_CalibrateRange(motor_mode_t mode,
|
||||
uint32_t legTimeoutMs,
|
||||
uint32_t commandSequence);
|
||||
/* Re-establish zero at the known near endpoint while retaining the confirmed
|
||||
* default MaxHall. This performs one near-seek only, not a full-range test. */
|
||||
bool SeatMotor_HomeKnownRange(motor_mode_t mode,
|
||||
uint32_t legTimeoutMs,
|
||||
uint32_t commandSequence);
|
||||
bool SeatMotor_StartMaximumSpeedCalibration(motor_mode_t mode,
|
||||
uint32_t commandSequence);
|
||||
bool SeatMotorManager_ClearFault(motor_mode_t mode);
|
||||
|
||||
/* Optional endpoint-based position recovery primitive. */
|
||||
bool SeatMotor_RecoverPosition(motor_mode_t mode, uint32_t savedHallCount);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,22 +10,35 @@
|
||||
|
||||
#define SEAT_MOVE_STORE_SLOT_WORDS (CY_WORK_SES_SIZE_IN_WORD)
|
||||
#define SEAT_MOVE_STORE_SLOT_COUNT (2u)
|
||||
#define SEAT_MOVE_STORE_LEGACY_V5_MAGIC (0x534D5635u) /* "SMV5" */
|
||||
#define SEAT_MOVE_STORE_LEGACY_V5_VERSION (5u)
|
||||
#define SEAT_MOVE_STORE_LEGACY_V6_MAGIC (0x534D5636u) /* "SMV6" */
|
||||
#define SEAT_MOVE_STORE_LEGACY_V6_VERSION (6u)
|
||||
#define SEAT_MOVE_STORE_LEGACY_V7_MAGIC (0x534D5637u) /* "SMV7" */
|
||||
#define SEAT_MOVE_STORE_LEGACY_V7_VERSION (7u)
|
||||
#define SEAT_MOVE_STORE_HEADER_WORDS (4u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_WORDS (5u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_WORDS (6u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_HALL (0u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_MAX_HALL (1u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_MAX_SPEED (2u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_RAW_EDGES (3u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_ABS_VALUE (4u)
|
||||
#define SEAT_MOVE_STORE_MOTOR_VALID (5u)
|
||||
#define SEAT_MOVE_STORE_CRC_WORD_INDEX \
|
||||
(SEAT_MOVE_STORE_HEADER_WORDS + \
|
||||
(SEAT_MOVE_MOTOR_COUNT * SEAT_MOVE_STORE_MOTOR_WORDS))
|
||||
#define SEAT_MOVE_STORE_LEGACY_CRC_WORD_INDEX \
|
||||
(SEAT_MOVE_STORE_HEADER_WORDS + \
|
||||
(SEAT_MOVE_STORE_LEGACY_MOTOR_COUNT * SEAT_MOVE_STORE_MOTOR_WORDS))
|
||||
|
||||
typedef char seat_move_store_record_must_fit[
|
||||
(SEAT_MOVE_STORE_CRC_WORD_INDEX < SEAT_MOVE_STORE_SLOT_WORDS) ? 1 : -1];
|
||||
typedef char seat_move_store_slots_must_fit_work_flash[
|
||||
((SEAT_MOVE_STORE_ADDRESS +
|
||||
(SEAT_MOVE_STORE_SLOT_COUNT * CY_WORK_SES_SIZE_IN_BYTE)) <=
|
||||
CY_WFLASH_SM_SBM_END) ? 1 : -1];
|
||||
|
||||
typedef enum
|
||||
{
|
||||
SEAT_MOVE_STORE_FORMAT_INVALID = 0,
|
||||
SEAT_MOVE_STORE_FORMAT_LEGACY_THREE_MOTOR,
|
||||
SEAT_MOVE_STORE_FORMAT_LEGACY_INCOMPATIBLE,
|
||||
SEAT_MOVE_STORE_FORMAT_CURRENT_FOUR_MOTOR
|
||||
} seat_move_store_format_t;
|
||||
|
||||
@@ -33,13 +46,21 @@ static uint32_t s_record[SEAT_MOVE_STORE_SLOT_WORDS];
|
||||
static uint32_t s_activeSlot = 0u;
|
||||
static bool s_valid = false;
|
||||
static bool s_clean = false;
|
||||
static seat_move_store_format_t s_format = SEAT_MOVE_STORE_FORMAT_INVALID;
|
||||
static bool s_corrupt = false;
|
||||
|
||||
static uint32_t SeatMoveStore_Crc32(const uint32_t *words, uint32_t wordCount);
|
||||
static uint32_t SeatMoveStore_SlotAddress(uint32_t slot);
|
||||
static uint32_t SeatMoveStore_MaxValueX100(motor_mode_t mode);
|
||||
static uint32_t SeatMoveStore_AbsoluteValueX100(motor_mode_t mode,
|
||||
uint32_t hallCount,
|
||||
uint32_t maxHallCount);
|
||||
static seat_move_store_format_t SeatMoveStore_ReadSlot(
|
||||
uint32_t slot, uint32_t *destination);
|
||||
static bool SeatMoveStore_SequenceNewer(uint32_t candidate, uint32_t reference);
|
||||
static bool SeatMoveStore_IsBlank(const uint32_t *record);
|
||||
static bool SeatMoveStore_PayloadMatches(const uint32_t *candidate);
|
||||
static bool SeatMoveStore_WriteRecord(const uint32_t *record,
|
||||
uint32_t targetSlot);
|
||||
|
||||
void SeatMoveStore_Init(void)
|
||||
{
|
||||
@@ -54,14 +75,18 @@ void SeatMoveStore_Init(void)
|
||||
Cy_FlashInit(false);
|
||||
slot0Format = SeatMoveStore_ReadSlot(0u, slot0);
|
||||
slot1Format = SeatMoveStore_ReadSlot(1u, slot1);
|
||||
slot0Valid = (slot0Format != SEAT_MOVE_STORE_FORMAT_INVALID);
|
||||
slot1Valid = (slot1Format != SEAT_MOVE_STORE_FORMAT_INVALID);
|
||||
slot0Valid = (slot0Format == SEAT_MOVE_STORE_FORMAT_CURRENT_FOUR_MOTOR);
|
||||
slot1Valid = (slot1Format == SEAT_MOVE_STORE_FORMAT_CURRENT_FOUR_MOTOR);
|
||||
s_valid = slot0Valid || slot1Valid;
|
||||
s_corrupt = !s_valid &&
|
||||
(((slot0Format == SEAT_MOVE_STORE_FORMAT_INVALID) &&
|
||||
!SeatMoveStore_IsBlank(slot0)) ||
|
||||
((slot1Format == SEAT_MOVE_STORE_FORMAT_INVALID) &&
|
||||
!SeatMoveStore_IsBlank(slot1)));
|
||||
if(!s_valid)
|
||||
{
|
||||
s_clean = false;
|
||||
s_activeSlot = 0u;
|
||||
s_format = SEAT_MOVE_STORE_FORMAT_INVALID;
|
||||
for(index = 0u; index < SEAT_MOVE_STORE_SLOT_WORDS; index++)
|
||||
{
|
||||
s_record[index] = 0xFFFFFFFFu;
|
||||
@@ -73,7 +98,6 @@ void SeatMoveStore_Init(void)
|
||||
(!slot0Valid || SeatMoveStore_SequenceNewer(slot1[2], slot0[2])))
|
||||
{
|
||||
s_activeSlot = 1u;
|
||||
s_format = slot1Format;
|
||||
for(index = 0u; index < SEAT_MOVE_STORE_SLOT_WORDS; index++)
|
||||
{
|
||||
s_record[index] = slot1[index];
|
||||
@@ -82,7 +106,6 @@ void SeatMoveStore_Init(void)
|
||||
else
|
||||
{
|
||||
s_activeSlot = 0u;
|
||||
s_format = slot0Format;
|
||||
for(index = 0u; index < SEAT_MOVE_STORE_SLOT_WORDS; index++)
|
||||
{
|
||||
s_record[index] = slot0[index];
|
||||
@@ -93,33 +116,51 @@ void SeatMoveStore_Init(void)
|
||||
|
||||
bool SeatMoveStore_Restore(void)
|
||||
{
|
||||
motor_persistent_state_t states[SEAT_MOVE_MOTOR_COUNT];
|
||||
uint32_t motorIndex;
|
||||
uint32_t restoreMotorCount;
|
||||
|
||||
if(!s_valid || !s_clean)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Preserve the already calibrated motors 0..2 when upgrading an SMV2
|
||||
* three-motor record. Motor3 remains at its estimated defaults with
|
||||
* positionValid=false until it is physically connected and calibrated. */
|
||||
restoreMotorCount =
|
||||
(s_format == SEAT_MOVE_STORE_FORMAT_LEGACY_THREE_MOTOR) ?
|
||||
SEAT_MOVE_STORE_LEGACY_MOTOR_COUNT : SEAT_MOVE_MOTOR_COUNT;
|
||||
for(motorIndex = 0u; motorIndex < restoreMotorCount; motorIndex++)
|
||||
for(motorIndex = 0u; motorIndex < SEAT_MOVE_MOTOR_COUNT; motorIndex++)
|
||||
{
|
||||
uint32_t base = SEAT_MOVE_STORE_HEADER_WORDS +
|
||||
(motorIndex * SEAT_MOVE_STORE_MOTOR_WORDS);
|
||||
motor_persistent_state_t state;
|
||||
uint32_t savedAbsoluteValue;
|
||||
uint32_t expectedAbsoluteValue;
|
||||
|
||||
state.positionHallCount = s_record[base];
|
||||
state.maxHallCount = s_record[base + 1u];
|
||||
state.maxSpeedPulsesPerSecondX100 = s_record[base + 2u];
|
||||
state.rawRisingEdges = s_record[base + 3u];
|
||||
state.positionValid = (s_record[base + 4u] == 1u);
|
||||
states[motorIndex].positionHallCount =
|
||||
s_record[base + SEAT_MOVE_STORE_MOTOR_HALL];
|
||||
states[motorIndex].maxHallCount =
|
||||
s_record[base + SEAT_MOVE_STORE_MOTOR_MAX_HALL];
|
||||
states[motorIndex].maxSpeedPulsesPerSecondX100 =
|
||||
s_record[base + SEAT_MOVE_STORE_MOTOR_MAX_SPEED];
|
||||
states[motorIndex].rawRisingEdges =
|
||||
s_record[base + SEAT_MOVE_STORE_MOTOR_RAW_EDGES];
|
||||
savedAbsoluteValue =
|
||||
s_record[base + SEAT_MOVE_STORE_MOTOR_ABS_VALUE];
|
||||
states[motorIndex].positionValid =
|
||||
(s_record[base + SEAT_MOVE_STORE_MOTOR_VALID] == 1u);
|
||||
expectedAbsoluteValue = SeatMoveStore_AbsoluteValueX100(
|
||||
(motor_mode_t)motorIndex,
|
||||
states[motorIndex].positionHallCount,
|
||||
states[motorIndex].maxHallCount);
|
||||
if((s_record[base + SEAT_MOVE_STORE_MOTOR_VALID] > 1u) ||
|
||||
(savedAbsoluteValue != expectedAbsoluteValue) ||
|
||||
(MOTOR_API_SUCCESS != Motor_ValidatePersistentState(
|
||||
(motor_mode_t)motorIndex,
|
||||
&states[motorIndex])))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for(motorIndex = 0u; motorIndex < SEAT_MOVE_MOTOR_COUNT; motorIndex++)
|
||||
{
|
||||
if(MOTOR_API_SUCCESS != Motor_ApplyPersistentState(
|
||||
(motor_mode_t)motorIndex, &state))
|
||||
(motor_mode_t)motorIndex,
|
||||
&states[motorIndex]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -127,15 +168,55 @@ bool SeatMoveStore_Restore(void)
|
||||
return true;
|
||||
}
|
||||
|
||||
void SeatMoveStore_RejectRestore(void)
|
||||
{
|
||||
/* A structurally valid record can still fail semantic validation (for
|
||||
* example, an inconsistent Hall/physical value). Keep its sequence so
|
||||
* the next clean save supersedes both flash slots, but never expose the
|
||||
* record as restorable. This condition is recoverable by finding the
|
||||
* known near endpoint; an actual flash erase/write failure remains a
|
||||
* latched SAFETY_FAULT_CONFIG_STORE in the caller. */
|
||||
s_clean = false;
|
||||
s_corrupt = true;
|
||||
}
|
||||
|
||||
bool SeatMoveStore_MarkMovementStarted(void)
|
||||
{
|
||||
uint32_t dirtyRecord[SEAT_MOVE_STORE_SLOT_WORDS];
|
||||
uint32_t wordIndex;
|
||||
uint32_t targetSlot;
|
||||
|
||||
/* No restorable state exists on a first boot or after an interrupted
|
||||
* motion. There is therefore no stale clean record to invalidate. */
|
||||
if(!s_valid || !s_clean)
|
||||
{
|
||||
s_clean = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
dirtyRecord[wordIndex] = s_record[wordIndex];
|
||||
}
|
||||
dirtyRecord[2] = s_record[2] + 1u;
|
||||
dirtyRecord[3] = 0u;
|
||||
dirtyRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
|
||||
SeatMoveStore_Crc32(dirtyRecord, SEAT_MOVE_STORE_CRC_WORD_INDEX);
|
||||
targetSlot = s_activeSlot ^ 1u;
|
||||
|
||||
/* The dirty record is committed before PWM is allowed to start. If
|
||||
* power fails while this blocking write is in progress, the old clean
|
||||
* record still describes the unchanged pre-motion position. Once this
|
||||
* call returns, the newer dirty sequence suppresses that old position. */
|
||||
return SeatMoveStore_WriteRecord(dirtyRecord, targetSlot);
|
||||
}
|
||||
|
||||
bool SeatMoveStore_Save(void)
|
||||
{
|
||||
uint32_t newRecord[SEAT_MOVE_STORE_SLOT_WORDS];
|
||||
uint32_t motorIndex;
|
||||
uint32_t wordIndex;
|
||||
uint32_t targetSlot = s_valid ? (s_activeSlot ^ 1u) : 0u;
|
||||
uint32_t address = SeatMoveStore_SlotAddress(targetSlot);
|
||||
volatile const uint32_t *flashWords =
|
||||
(volatile const uint32_t *)(uintptr_t)address;
|
||||
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
@@ -151,46 +232,36 @@ bool SeatMoveStore_Save(void)
|
||||
uint32_t base = SEAT_MOVE_STORE_HEADER_WORDS +
|
||||
(motorIndex * SEAT_MOVE_STORE_MOTOR_WORDS);
|
||||
motor_persistent_state_t state;
|
||||
motor_position_info_t position;
|
||||
|
||||
if(MOTOR_API_SUCCESS != Motor_GetPersistentState(
|
||||
(motor_mode_t)motorIndex, &state))
|
||||
(motor_mode_t)motorIndex, &state) ||
|
||||
MOTOR_API_SUCCESS != Motor_GetPositionInfo(
|
||||
(motor_mode_t)motorIndex, &position))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
newRecord[base] = state.positionHallCount;
|
||||
newRecord[base + 1u] = state.maxHallCount;
|
||||
newRecord[base + 2u] = state.maxSpeedPulsesPerSecondX100;
|
||||
newRecord[base + 3u] = state.rawRisingEdges;
|
||||
newRecord[base + 4u] = state.positionValid ? 1u : 0u;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_HALL] =
|
||||
state.positionHallCount;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_MAX_HALL] =
|
||||
state.maxHallCount;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_MAX_SPEED] =
|
||||
state.maxSpeedPulsesPerSecondX100;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_RAW_EDGES] =
|
||||
state.rawRisingEdges;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_ABS_VALUE] =
|
||||
position.valueX100;
|
||||
newRecord[base + SEAT_MOVE_STORE_MOTOR_VALID] =
|
||||
state.positionValid ? 1u : 0u;
|
||||
}
|
||||
newRecord[SEAT_MOVE_STORE_CRC_WORD_INDEX] =
|
||||
SeatMoveStore_Crc32(newRecord, SEAT_MOVE_STORE_CRC_WORD_INDEX);
|
||||
|
||||
Cy_FlashSectorErase(address, CY_FLASH_DRIVER_BLOCKING);
|
||||
if(!Cy_WorkFlashBlankCheck(address, CY_FLASH_DRIVER_BLOCKING))
|
||||
if(s_valid && s_clean && SeatMoveStore_PayloadMatches(newRecord))
|
||||
{
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
Cy_FlashWriteWork(address + (wordIndex * sizeof(uint32_t)),
|
||||
&newRecord[wordIndex],
|
||||
CY_FLASH_DRIVER_BLOCKING);
|
||||
}
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
if(flashWords[wordIndex] != newRecord[wordIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
s_record[wordIndex] = newRecord[wordIndex];
|
||||
}
|
||||
|
||||
s_activeSlot = targetSlot;
|
||||
s_valid = true;
|
||||
s_clean = true;
|
||||
s_format = SEAT_MOVE_STORE_FORMAT_CURRENT_FOUR_MOTOR;
|
||||
return true;
|
||||
return SeatMoveStore_WriteRecord(newRecord, targetSlot);
|
||||
}
|
||||
|
||||
bool SeatMoveStore_IsValid(void)
|
||||
@@ -203,6 +274,11 @@ bool SeatMoveStore_IsClean(void)
|
||||
return s_clean;
|
||||
}
|
||||
|
||||
bool SeatMoveStore_HasCorruptRecord(void)
|
||||
{
|
||||
return s_corrupt;
|
||||
}
|
||||
|
||||
uint32_t SeatMoveStore_GetSequence(void)
|
||||
{
|
||||
return s_valid ? s_record[2] : 0u;
|
||||
@@ -239,6 +315,43 @@ static uint32_t SeatMoveStore_SlotAddress(uint32_t slot)
|
||||
return SEAT_MOVE_STORE_ADDRESS + (slot * CY_WORK_SES_SIZE_IN_BYTE);
|
||||
}
|
||||
|
||||
static uint32_t SeatMoveStore_MaxValueX100(motor_mode_t mode)
|
||||
{
|
||||
switch(mode)
|
||||
{
|
||||
case MOTOR_MODE_0_LINEAR:
|
||||
return MOTOR_MODE0_MAX_POSITION_MM_X100;
|
||||
case MOTOR_MODE_1_ANGLE:
|
||||
return MOTOR_MODE1_MAX_VERTICAL_MM_X100;
|
||||
case MOTOR_MODE_2_ANGLE:
|
||||
return MOTOR_MODE2_MAX_ANGLE_DEG_X100;
|
||||
case MOTOR_MODE_3_LINEAR_TRACK:
|
||||
return MOTOR_MODE3_MAX_ANGLE_DEG_X100;
|
||||
default:
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
static uint32_t SeatMoveStore_AbsoluteValueX100(motor_mode_t mode,
|
||||
uint32_t hallCount,
|
||||
uint32_t maxHallCount)
|
||||
{
|
||||
uint32_t maxValue = SeatMoveStore_MaxValueX100(mode);
|
||||
uint32_t boundedHall = hallCount;
|
||||
|
||||
if((0u == maxValue) || (0u == maxHallCount))
|
||||
{
|
||||
return UINT32_MAX;
|
||||
}
|
||||
if(boundedHall > maxHallCount)
|
||||
{
|
||||
boundedHall = maxHallCount;
|
||||
}
|
||||
return (uint32_t)((((uint64_t)boundedHall * maxValue) +
|
||||
(maxHallCount / 2u)) /
|
||||
maxHallCount);
|
||||
}
|
||||
|
||||
static seat_move_store_format_t SeatMoveStore_ReadSlot(
|
||||
uint32_t slot, uint32_t *destination)
|
||||
{
|
||||
@@ -266,13 +379,21 @@ static seat_move_store_format_t SeatMoveStore_ReadSlot(
|
||||
{
|
||||
return SEAT_MOVE_STORE_FORMAT_CURRENT_FOUR_MOTOR;
|
||||
}
|
||||
if((destination[0] == SEAT_MOVE_STORE_LEGACY_MAGIC) &&
|
||||
(destination[1] == SEAT_MOVE_STORE_LEGACY_VERSION) &&
|
||||
(destination[SEAT_MOVE_STORE_LEGACY_CRC_WORD_INDEX] ==
|
||||
if((((destination[0] == SEAT_MOVE_STORE_LEGACY_V5_MAGIC) &&
|
||||
(destination[1] == SEAT_MOVE_STORE_LEGACY_V5_VERSION)) ||
|
||||
((destination[0] == SEAT_MOVE_STORE_LEGACY_V6_MAGIC) &&
|
||||
(destination[1] == SEAT_MOVE_STORE_LEGACY_V6_VERSION)) ||
|
||||
((destination[0] == SEAT_MOVE_STORE_LEGACY_V7_MAGIC) &&
|
||||
(destination[1] == SEAT_MOVE_STORE_LEGACY_V7_VERSION))) &&
|
||||
(destination[SEAT_MOVE_STORE_CRC_WORD_INDEX] ==
|
||||
SeatMoveStore_Crc32(destination,
|
||||
SEAT_MOVE_STORE_LEGACY_CRC_WORD_INDEX)))
|
||||
SEAT_MOVE_STORE_CRC_WORD_INDEX)))
|
||||
{
|
||||
return SEAT_MOVE_STORE_FORMAT_LEGACY_THREE_MOTOR;
|
||||
/* SMV5/SMV6 used obsolete Hall ranges. SMV7 stored the final
|
||||
* numeric ranges against a non-identity channel map. Treat all as
|
||||
* obsolete rather than corrupt, but never restore their positions.
|
||||
* A new four-axis calibration will commit an SMV8 record. */
|
||||
return SEAT_MOVE_STORE_FORMAT_LEGACY_INCOMPATIBLE;
|
||||
}
|
||||
return SEAT_MOVE_STORE_FORMAT_INVALID;
|
||||
}
|
||||
@@ -281,3 +402,82 @@ static bool SeatMoveStore_SequenceNewer(uint32_t candidate, uint32_t reference)
|
||||
{
|
||||
return ((int32_t)(candidate - reference) > 0);
|
||||
}
|
||||
|
||||
static bool SeatMoveStore_IsBlank(const uint32_t *record)
|
||||
{
|
||||
uint32_t index;
|
||||
|
||||
if(record == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for(index = 0u; index < SEAT_MOVE_STORE_SLOT_WORDS; index++)
|
||||
{
|
||||
if(record[index] != 0xFFFFFFFFu)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SeatMoveStore_PayloadMatches(const uint32_t *candidate)
|
||||
{
|
||||
uint32_t wordIndex;
|
||||
|
||||
if(candidate == NULL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for(wordIndex = SEAT_MOVE_STORE_HEADER_WORDS;
|
||||
wordIndex < SEAT_MOVE_STORE_CRC_WORD_INDEX;
|
||||
wordIndex++)
|
||||
{
|
||||
if(candidate[wordIndex] != s_record[wordIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool SeatMoveStore_WriteRecord(const uint32_t *record,
|
||||
uint32_t targetSlot)
|
||||
{
|
||||
uint32_t address;
|
||||
uint32_t wordIndex;
|
||||
volatile const uint32_t *flashWords;
|
||||
|
||||
if((record == NULL) || (targetSlot >= SEAT_MOVE_STORE_SLOT_COUNT))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
address = SeatMoveStore_SlotAddress(targetSlot);
|
||||
flashWords = (volatile const uint32_t *)(uintptr_t)address;
|
||||
|
||||
Cy_FlashSectorErase(address, CY_FLASH_DRIVER_BLOCKING);
|
||||
if(!Cy_WorkFlashBlankCheck(address, CY_FLASH_DRIVER_BLOCKING))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
Cy_FlashWriteWork(address + (wordIndex * sizeof(uint32_t)),
|
||||
&record[wordIndex],
|
||||
CY_FLASH_DRIVER_BLOCKING);
|
||||
}
|
||||
for(wordIndex = 0u; wordIndex < SEAT_MOVE_STORE_SLOT_WORDS; wordIndex++)
|
||||
{
|
||||
if(flashWords[wordIndex] != record[wordIndex])
|
||||
{
|
||||
return false;
|
||||
}
|
||||
s_record[wordIndex] = record[wordIndex];
|
||||
}
|
||||
|
||||
s_activeSlot = targetSlot;
|
||||
s_valid = true;
|
||||
s_clean = (record[3] == 1u);
|
||||
s_corrupt = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -6,9 +6,14 @@
|
||||
|
||||
void SeatMoveStore_Init(void);
|
||||
bool SeatMoveStore_Restore(void);
|
||||
/* Keep the record sequence for replacement, but make a failed restore
|
||||
* non-restorable so known-range homing can rebuild a clean position. */
|
||||
void SeatMoveStore_RejectRestore(void);
|
||||
bool SeatMoveStore_MarkMovementStarted(void);
|
||||
bool SeatMoveStore_Save(void);
|
||||
bool SeatMoveStore_IsValid(void);
|
||||
bool SeatMoveStore_IsClean(void);
|
||||
bool SeatMoveStore_HasCorruptRecord(void);
|
||||
uint32_t SeatMoveStore_GetSequence(void);
|
||||
|
||||
#endif /* SEAT_MOVE_STORE_H */
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user