feat: add seat firmware workbench and OTA delivery

This commit is contained in:
vee
2026-09-16 17:55:30 +08:00
parent 3eeca2ea75
commit ab8e43050c
70 changed files with 22594 additions and 316 deletions
+9
View File
@@ -30,4 +30,13 @@ firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_log
firmware/t2g/Seat_Controller_demo/IAR/rev_d/BrowseInfo/ firmware/t2g/Seat_Controller_demo/IAR/rev_d/BrowseInfo/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/List/ firmware/t2g/Seat_Controller_demo/IAR/rev_d/List/
firmware/t2g/Seat_Controller_demo/IAR/rev_d/Obj/ firmware/t2g/Seat_Controller_demo/IAR/rev_d/Obj/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/settings/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.eww
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.Passenger_Seat_Timed_Controller_demo_build_cache
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.ninja_deps
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/.ninja_log
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/BrowseInfo/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/List/
firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Obj/
firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem firmware/t2g/tviibh8m/src/examples/crypto/ClientRunsOnCM7_0/RSA/key_data/private_key*.pem
artifacts/logs/
+1 -1
View File
@@ -184,7 +184,7 @@ config/gesture/default.yaml
.\.venv\Scripts\python.exe tools\start_n7web.py .\.venv\Scripts\python.exe tools\start_n7web.py
``` ```
脚本自动设置 `PYTHONPATH`,主页面默认仅监听 `127.0.0.1:8000`,模式卡片 HTTP/UDP 服务监听 `0.0.0.0:8090`。需要让可信局域网内的设备同时访问主页面时,使用 `--host 0.0.0.0`;可通过 `--port`、`--scene-port`、`--catalog` 和 `--server-id` 覆盖其他参数。任一子服务异常退出或按一次 `Ctrl+C` 时,脚本会停止另一个服务。 脚本自动设置 `PYTHONPATH`:管理页面默认仅监听 `127.0.0.1:8000`,只读固件 OTA 服务监听 `127.0.0.1:8012`,模式卡片 HTTP/UDP 服务监听 `0.0.0.0:8090`。OTA 服务只提供健康检查、全部历史发布列表、最新版本清单和固件下载,设计为通过固定公网 HTTPS 隧道供车机访问,不暴露工作台和车辆控制接口。车机通过 `GET /api/firmware-workbench/releases` 枚举所有可下载固件,通过 `GET /api/firmware-workbench/releases/latest` 查询最新固件。如果 `8012` 端口已经运行健康的 N7 OTA 服务,启动器会直接复用它。可通过 `--host`、`--port`、`--ota-host`、`--ota-port`、`--scene-port`、`--catalog` 和 `--server-id` 覆盖参数。任一由启动器管理的子服务异常退出或按一次 `Ctrl+C` 时,脚本会停止其余受管服务。
同步由模式注册表的创建、更新、删除、恢复和显式重载事件触发,不会持续扫描目录。每次变更只重新生成并原子替换一份小型 JSON;独立目录服务在收到 HTTP 请求时才读取它。因此空闲时的额外开销主要是一个等待请求的 Python 进程和一个 UDP 发现线程,而不是持续的 CPU 或磁盘轮询。 同步由模式注册表的创建、更新、删除、恢复和显式重载事件触发,不会持续扫描目录。每次变更只重新生成并原子替换一份小型 JSON;独立目录服务在收到 HTTP 请求时才读取它。因此空闲时的额外开销主要是一个等待请求的 Python 进程和一个 UDP 发现线程,而不是持续的 CPU 或磁盘轮询。
+21 -4
View File
@@ -170,11 +170,13 @@
if (!compact || !context?.name) return false; if (!compact || !context?.name) return false;
const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?"; const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?";
const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)"; const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)";
const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)"; const addPredicate = "(?:加入|添加|增加|补充|加上|追加|新增)";
const changePredicate = `(?:${addPredicate}|修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)`;
const modeName = "([^,。!?!?、;;::]{1,64}?模式)"; const modeName = "([^,。!?!?、;;::]{1,64}?模式)";
const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)"; const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)";
const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`; const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`;
const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/; const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/;
const declinedAddition = new RegExp(`^(?:先|暂时)?(?:不要|别|不用|不必|无需|取消|撤销)(?:再|继续)?${addPredicate}`);
const candidates = [ const candidates = [
{ {
pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`), pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`),
@@ -188,14 +190,29 @@
pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`), pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`),
predicateRequired: true, predicateRequired: true,
}, },
{
pattern: new RegExp(`^${politePrefix}(?:在|给|为|往|向)${modeName}(?:${modePossessiveBoundary})?[,,::]?(.+)$`),
predicateRequired: true,
},
{
pattern: new RegExp(`^${politePrefix}${modeName}((?:再|也|还)?${addPredicate}.+)$`),
predicateRequired: true,
},
{
pattern: new RegExp(`^${politePrefix}(?:把|将)(.+?)${addPredicate}(?:到|进)?${modeName}(?:中|里|内)?[。.!!]?$`),
predicateRequired: false,
nameIndex: 2,
changeIndex: 1,
},
]; ];
return candidates.some(({ pattern, predicateRequired }) => { return candidates.some(({ pattern, predicateRequired, nameIndex = 1, changeIndex = 2 }) => {
const matched = compact.match(pattern); const matched = compact.match(pattern);
const change = String(matched?.[2] || ""); const change = String(matched?.[changeIndex] || "");
return Boolean( return Boolean(
matched matched
&& normalizeModeName(matched[1]) === normalizeModeName(context.name) && normalizeModeName(matched[nameIndex]) === normalizeModeName(context.name)
&& !question.test(change) && !question.test(change)
&& !declinedAddition.test(change)
&& (!predicateRequired || new RegExp(changePredicate).test(change)) && (!predicateRequired || new RegExp(changePredicate).test(change))
); );
}); });
@@ -0,0 +1,75 @@
.console-v2 .firmware-workbench-layer {
position: fixed;
z-index: 5000;
inset: 0;
display: grid;
place-items: center;
padding: 68px 20px 20px;
background: color-mix(in oklch, var(--background) 58%, transparent);
backdrop-filter: blur(6px) saturate(.9);
-webkit-backdrop-filter: blur(6px) saturate(.9);
overscroll-behavior: contain;
}
.console-v2 .firmware-workbench-layer[hidden] { display: none !important; }
.console-v2 .firmware-workbench-page {
width: min(1180px, 100%);
height: min(664px, 100%);
min-height: 0;
display: grid;
grid-template-rows: 76px minmax(0, 1fr);
overflow: hidden;
color: var(--foreground);
background: color-mix(in oklch, var(--card) 97%, var(--background));
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: 22px;
box-shadow: 0 28px 70px color-mix(in oklch, var(--foreground) 22%, transparent), 0 4px 18px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 1px 0 color-mix(in oklch, var(--primary-foreground) 85%, transparent);
animation: firmware-workbench-enter 180ms ease-out both;
}
.console-v2 .firmware-workbench-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 14px 18px 13px 20px;
border-bottom: 1px solid color-mix(in oklch, var(--primary) 10%, var(--border));
background: color-mix(in oklch, var(--card) 85%, transparent);
}
.console-v2 .firmware-workbench-heading { min-width: 0; display: flex; align-items: center; gap: 12px; }
.console-v2 .firmware-workbench-heading > span {
width: 46px;
height: 46px;
flex: 0 0 46px;
display: grid;
place-items: center;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 10%, var(--card));
border: 1px solid color-mix(in oklch, var(--primary) 22%, var(--border));
border-radius: 15px;
}
.console-v2 .firmware-workbench-heading svg { width: 24px; height: 24px; stroke-width: 1.8; }
.console-v2 .firmware-workbench-heading h2 { margin: 0; color: var(--heading-foreground, var(--foreground)); font-size: 20px; line-height: 1.2; letter-spacing: -.025em; }
.console-v2 #firmwareWorkbenchClose {
width: 38px;
height: 38px;
display: grid;
flex: 0 0 38px;
place-items: center;
padding: 0;
color: var(--foreground);
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
border: 1px solid color-mix(in oklch, var(--primary) 13%, var(--border));
border-radius: 12px;
cursor: pointer;
}
.console-v2 #firmwareWorkbenchClose:hover,
.console-v2 #firmwareWorkbenchClose:focus-visible { color: var(--primary); background: var(--accent); outline: 3px solid color-mix(in oklch, var(--ring) 16%, transparent); }
.console-v2 #firmwareWorkbenchClose svg { width: 18px; height: 18px; }
.console-v2 .firmware-workbench-frame { width: 100%; height: 100%; display: block; border: 0; background: transparent; }
@keyframes firmware-workbench-enter { from { opacity: 0; transform: translateY(8px) scale(.992); } to { opacity: 1; transform: none; } }
@media (max-width: 760px) {
.console-v2 .firmware-workbench-layer { padding: 54px 8px 8px; }
.console-v2 .firmware-workbench-page { height: 100%; border-radius: 17px; grid-template-rows: 64px minmax(0, 1fr); }
.console-v2 .firmware-workbench-header { padding: 9px 11px; }
.console-v2 .firmware-workbench-heading > span { width: 40px; height: 40px; flex-basis: 40px; border-radius: 12px; }
.console-v2 .firmware-workbench-heading h2 { font-size: 17px; }
}
@@ -0,0 +1,108 @@
(() => {
"use strict";
const state = { open: false, returnFocus: null, backgroundInert: [] };
function setBackgroundInert(inert) {
const layer = document.querySelector("#firmwareWorkbenchLayer");
if (!layer) return;
if (inert) {
state.backgroundInert = [...document.body.children]
.filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName))
.map((element) => ({ element, wasInert: element.hasAttribute("inert") }));
state.backgroundInert.forEach(({ element }) => element.toggleAttribute("inert", true));
return;
}
state.backgroundInert.forEach(({ element, wasInert }) => element.toggleAttribute("inert", wasInert));
state.backgroundInert = [];
}
function setOpen(open) {
const next = Boolean(open);
const layer = document.querySelector("#firmwareWorkbenchLayer");
const trigger = document.querySelector("#firmwareWorkbenchToggle");
if (!layer || !trigger || state.open === next) return;
state.open = next;
trigger.classList.toggle("active", next);
trigger.setAttribute("aria-pressed", String(next));
trigger.setAttribute("aria-expanded", String(next));
trigger.setAttribute("aria-label", next ? "关闭座椅固件开发工作台" : "打开座椅固件开发工作台");
trigger.title = next ? "关闭座椅固件开发工作台" : "打开座椅固件开发工作台";
if (next) {
state.returnFocus = document.activeElement;
layer.hidden = false;
layer.inert = false;
layer.dataset.open = "true";
setBackgroundInert(true);
requestAnimationFrame(() => document.querySelector("#firmwareWorkbenchClose")?.focus());
} else {
layer.dataset.open = "false";
layer.inert = true;
layer.hidden = true;
setBackgroundInert(false);
const target = state.returnFocus instanceof HTMLElement && state.returnFocus.isConnected
? state.returnFocus
: trigger;
state.returnFocus = null;
target.focus();
}
}
function mount() {
const dock = document.querySelector("#v2PanelControlDock");
if (!dock || document.querySelector("#firmwareWorkbenchToggle")) return false;
const trigger = document.createElement("button");
trigger.id = "firmwareWorkbenchToggle";
trigger.className = "firmware-workbench-toggle v2-panel-control-button";
trigger.type = "button";
trigger.setAttribute("aria-controls", "firmwareWorkbenchLayer");
trigger.setAttribute("aria-pressed", "false");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", "打开座椅固件开发工作台");
trigger.title = "打开座椅固件开发工作台";
trigger.innerHTML = '<i data-lucide="blocks"></i><span class="visually-hidden">座椅固件开发工作台</span>';
const layer = document.createElement("div");
layer.id = "firmwareWorkbenchLayer";
layer.className = "firmware-workbench-layer";
layer.hidden = true;
layer.inert = true;
layer.setAttribute("role", "dialog");
layer.setAttribute("aria-modal", "true");
layer.setAttribute("aria-labelledby", "firmwareWorkbenchTitle");
layer.innerHTML = `
<section class="firmware-workbench-page">
<header class="firmware-workbench-header">
<div class="firmware-workbench-heading">
<span><i data-lucide="blocks"></i></span>
<h2 id="firmwareWorkbenchTitle">座椅固件开发工作台</h2>
</div>
<button id="firmwareWorkbenchClose" type="button" aria-label="关闭座椅固件开发工作台" title="关闭"><i data-lucide="x"></i></button>
</header>
<iframe class="firmware-workbench-frame" src="/firmware-workbench.html?embedded=1" title="座椅固件开发工作台"></iframe>
</section>`;
dock.append(trigger);
document.body.append(layer);
trigger.addEventListener("click", () => setOpen(!state.open));
document.querySelector("#firmwareWorkbenchClose", layer).addEventListener("click", () => setOpen(false));
layer.addEventListener("click", (event) => { if (event.target === layer) setOpen(false); });
document.addEventListener("keydown", (event) => {
if (state.open && event.key === "Escape") {
event.preventDefault();
setOpen(false);
}
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
return true;
}
if (!mount()) {
const observer = new MutationObserver(() => {
if (mount()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
window.setTimeout(() => observer.disconnect(), 15000);
}
})();
+456
View File
@@ -0,0 +1,456 @@
:root {
color-scheme: light;
font-family: "Microsoft YaHei UI", "PingFang SC", Inter, system-ui, sans-serif;
--page: #eef3f9;
--surface: #ffffff;
--surface-soft: #f7f9fc;
--line: #dce4ee;
--line-strong: #cbd7e5;
--text: #172033;
--muted: #68758b;
--primary: #2869e8;
--primary-soft: #eaf1ff;
--success: #16845b;
--success-soft: #e7f7f0;
--warning: #ba7314;
--danger: #c23f4a;
--shadow: 0 22px 60px rgba(43, 67, 101, .14), 0 4px 14px rgba(43, 67, 101, .08);
}
* { box-sizing: border-box; }
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; }
body {
display: grid;
place-items: center;
padding: 16px;
color: var(--text);
font-size: 14px;
line-height: 1.45;
background:
radial-gradient(circle at 15% 8%, rgba(74, 130, 235, .11), transparent 31%),
linear-gradient(145deg, #f7f9fc 0%, var(--page) 100%);
}
button, textarea, select { font: inherit; }
button, a, select { -webkit-tap-highlight-color: transparent; }
button, select { cursor: pointer; }
.shell {
width: min(1280px, 100%);
height: min(780px, calc(100dvh - 32px));
min-height: 620px;
overflow: hidden;
background: #f7f9fc;
border: 1px solid rgba(205, 216, 230, .9);
border-radius: 22px;
box-shadow: var(--shadow);
}
.topbar {
height: 68px;
padding: 0 22px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
background: rgba(255, 255, 255, .94);
border-bottom: 1px solid var(--line);
}
.topbar-main, .topbar-status, .card-heading, .button-row, .log-actions,
.server-target, .command-footer, .capability-brief { display: flex; align-items: center; }
.topbar-main { gap: 11px; min-width: 0; }
.topbar-status { gap: 9px; flex-wrap: wrap; justify-content: flex-end; }
.back-link, .product-mark, .icon-button, .result-icon {
display: inline-grid;
place-items: center;
}
.back-link, .icon-button {
width: 34px;
height: 34px;
border: 1px solid var(--line);
border-radius: 10px;
color: #4d5b71;
background: var(--surface);
text-decoration: none;
}
.back-link:hover, .icon-button:hover { color: var(--primary); border-color: #a9c1ef; background: var(--primary-soft); }
.back-link svg, .icon-button svg { width: 17px; height: 17px; }
.product-mark { width: 36px; height: 36px; border-radius: 11px; color: white; background: linear-gradient(145deg, #3a7cf0, #1d59cd); box-shadow: 0 7px 16px rgba(40, 105, 232, .22); }
.product-mark svg { width: 19px; }
.eyebrow, .section-kicker { margin: 0; color: #7a88a0; font-size: 12px; font-weight: 700; letter-spacing: .08em; }
h1 { margin: 1px 0 0; font-size: 20px; line-height: 1.15; letter-spacing: -.02em; }
h2 { margin: 1px 0 0; font-size: 17px; line-height: 1.2; }
.environment-chip, .server-state, .status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
min-height: 29px;
padding: 4px 10px;
border: 1px solid var(--line);
border-radius: 999px;
color: #58677d;
background: #f8fafc;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.environment-chip svg { width: 14px; }
.server-state span { width: 7px; height: 7px; border-radius: 50%; background: #9aa7b8; }
.server-state.online { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.server-state.online span { background: var(--success); box-shadow: 0 0 0 4px rgba(22, 132, 91, .1); }
main {
height: calc(100% - 68px);
min-height: 0;
padding: 11px;
display: grid;
grid-template-columns: 380px minmax(0, 1fr);
grid-template-rows: 116px minmax(0, 1fr) 172px;
grid-template-areas:
"pipeline pipeline"
"command execution"
"software software";
gap: 10px;
}
.card {
position: relative;
min-width: 0;
min-height: 0;
overflow: hidden;
padding: 12px 14px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 15px;
box-shadow: 0 2px 8px rgba(46, 66, 94, .045);
}
.card-heading { justify-content: space-between; gap: 12px; min-height: 32px; }
.hero-grid { grid-area: command; min-width: 0; min-height: 0; }
.hero-grid .command-card { height: 100%; }
.pipeline-card { grid-area: pipeline; }
.execution-grid { grid-area: execution; min-width: 0; min-height: 0; }
.execution-grid .log-card { width: 100%; height: 100%; }
.software-card { grid-area: software; }
.baseline-picker {
display: grid;
grid-template-columns: auto auto;
align-items: center;
column-gap: 5px;
color: var(--muted);
font-size: 12px;
font-weight: 700;
}
.baseline-picker svg { grid-row: 1 / 3; width: 16px; color: var(--primary); }
.baseline-picker span { line-height: 1; }
.baseline-picker select {
max-width: 148px;
border: 0;
padding: 1px 18px 1px 0;
color: var(--text);
background: transparent;
font-size: 13px;
font-weight: 700;
outline: none;
}
.command-card { display: flex; flex-direction: column; gap: 7px; }
.command-card > label { color: #5d6b80; font-size: 13px; font-weight: 650; }
textarea {
width: 100%;
min-height: 64px;
flex: 1 1 70px;
resize: none;
padding: 9px 11px;
color: var(--text);
background: var(--surface-soft);
border: 1px solid var(--line-strong);
border-radius: 10px;
font-size: 13px;
line-height: 1.55;
outline: none;
}
textarea:focus { border-color: #79a4f4; box-shadow: 0 0 0 3px rgba(40, 105, 232, .1); background: #fff; }
.prompt-templates { display: flex; gap: 6px; overflow: hidden; }
.prompt-templates button {
min-width: 0;
padding: 5px 8px;
border: 1px solid #cbd9ef;
border-radius: 8px;
color: #3563a9;
background: #f3f7ff;
font-size: 12px;
font-weight: 700;
white-space: nowrap;
}
.capability-brief {
min-height: 50px;
justify-content: space-between;
gap: 8px;
padding: 7px 9px;
border: 1px solid #dbe6f6;
border-radius: 10px;
background: #f6f9fe;
}
.capability-brief > div { min-width: 0; display: grid; gap: 1px; }
.capability-brief span { color: var(--muted); font-size: 12px; }
.capability-brief strong { overflow: hidden; text-overflow: ellipsis; color: #344157; font-size: 13px; white-space: nowrap; }
.capability-brief > svg { width: 17px; flex: 0 0 auto; color: #8fa7ca; }
.capability-target { flex: 1; }
.capability-target strong { color: var(--primary); }
.command-footer { justify-content: flex-end; gap: 8px; }
.button-row { gap: 6px; }
.button {
min-height: 33px;
padding: 6px 11px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border-radius: 9px;
border: 1px solid transparent;
font-size: 13px;
font-weight: 750;
white-space: nowrap;
}
.button svg { width: 15px; }
.button-primary { color: white; background: var(--primary); box-shadow: 0 5px 12px rgba(40, 105, 232, .18); }
.button-secondary { color: #526178; background: #fff; border-color: var(--line-strong); }
.button:disabled, .icon-button:disabled { cursor: not-allowed; opacity: .48; }
.pipeline {
width: calc(100% - 230px);
height: 58px;
margin: 7px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(8, minmax(0, 1fr));
list-style: none;
}
.pipeline-stage { position: relative; min-width: 0; padding-top: 2px; text-align: center; }
.pipeline-stage::after { content: ""; position: absolute; top: 14px; left: calc(50% + 15px); width: calc(100% - 30px); height: 2px; background: #dce4ee; }
.pipeline-stage:last-child::after { display: none; }
.stage-node { position: relative; z-index: 1; width: 28px; height: 28px; margin: 0 auto 1px; display: grid; place-items: center; border: 2px solid #d4deea; border-radius: 50%; color: #7b8aa0; background: #fff; }
.stage-node svg { width: 14px; }
.stage-name { display: block; overflow: hidden; text-overflow: ellipsis; font-size: 13px; line-height: 1.15; white-space: nowrap; }
.stage-state { display: block; overflow: hidden; text-overflow: ellipsis; color: var(--muted); font-size: 12px; line-height: 1.1; white-space: nowrap; }
.pipeline-stage[data-status="running"] .stage-node { color: var(--primary); border-color: var(--primary); background: var(--primary-soft); animation: pulse 1.4s infinite; }
.pipeline-stage[data-status="success"] .stage-node { color: var(--success); border-color: #65bd9e; background: var(--success-soft); }
.pipeline-stage[data-status="failed"] .stage-node, .pipeline-stage[data-status="blocked"] .stage-node { color: var(--danger); border-color: #e39aa1; background: #fff0f1; }
.pipeline-stage[data-status="canceled"] .stage-node { color: #7b8798; background: #eef1f5; }
.pipeline-stage[data-status="pending_confirmation"] .stage-node { color: var(--warning); border-color: #ecc080; background: #fff7e9; }
.pipeline-stage[data-status="success"]::after { background: #8bcdb5; }
.release-result {
position: absolute;
top: 43px;
right: 14px;
width: 210px;
min-height: 52px;
display: flex;
align-items: center;
gap: 10px;
padding: 8px 10px;
border: 1px solid var(--line);
border-radius: 11px;
background: var(--surface-soft);
}
.result-icon { width: 32px; height: 32px; flex: 0 0 auto; color: #748399; border-radius: 9px; background: #e9eef5; }
.result-icon svg { width: 17px; }
.release-result div { min-width: 0; display: grid; }
.release-result small { color: var(--muted); font-size: 12px; }
.release-result strong { overflow: hidden; text-overflow: ellipsis; font-size: 13px; white-space: nowrap; }
.release-result[data-state="success"] { border-color: #b9e5d3; background: var(--success-soft); }
.release-result[data-state="success"] .result-icon { color: var(--success); background: #d2f0e3; }
.release-result[data-state="failed"] { border-color: #efc5c9; background: #fff2f3; }
.status-legend { display: none; }
.log-card { display: grid; grid-template-rows: auto auto minmax(0, 1fr); gap: 7px; }
.log-actions { gap: 6px; }
.log-actions .icon-button { width: 31px; height: 31px; }
.status-badge { min-height: 25px; padding: 3px 9px; }
.status-badge[data-status="running"] { color: var(--primary); border-color: #b9cff7; background: var(--primary-soft); }
.status-badge[data-status="success"], .status-badge[data-status="published"] { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.status-badge[data-status="failed"], .status-badge[data-status="blocked"] { color: var(--danger); border-color: #efc5c9; background: #fff2f3; }
.status-badge[data-status="ready"], .status-badge[data-status="pending_confirmation"] { color: var(--warning); border-color: #efd3a8; background: #fff7e9; }
.metrics { margin: 0; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
.metrics div { min-width: 0; padding: 5px 7px; border-radius: 8px; background: var(--surface-soft); border: 1px solid #edf1f6; }
.metrics dt { color: var(--muted); font-size: 12px; }
.metrics dd { margin: 1px 0 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; font-weight: 750; white-space: nowrap; }
.log-workspace { min-height: 0; display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(220px, .9fr); gap: 8px; }
.log-stream, .log-details { min-height: 0; overflow: auto; border: 1px solid var(--line); border-radius: 10px; background: #f9fbfd; }
.log-stream { padding: 7px 9px; scrollbar-width: thin; }
.empty-state { height: 100%; min-height: 110px; display: grid; place-content: center; justify-items: center; gap: 4px; color: var(--muted); text-align: center; }
.empty-state svg { width: 24px; color: #93a3ba; }
.empty-state strong { color: #4a596f; font-size: 13px; }
.empty-state span { font-size: 12px; }
.log-entry { display: grid; grid-template-columns: 58px 9px minmax(0, 1fr); gap: 7px; padding: 5px 0; border-bottom: 1px solid #e9eef5; }
.log-entry:last-child { border-bottom: 0; }
.log-time { color: #7b889b; font: 12px/1.4 ui-monospace, Consolas, monospace; }
.log-dot { width: 7px; height: 7px; margin-top: 5px; border-radius: 50%; background: #91a0b4; }
.log-entry[data-level="success"] .log-dot { background: var(--success); }
.log-entry[data-level="error"] .log-dot { background: var(--danger); }
.log-entry[data-level="warning"] .log-dot { background: var(--warning); }
.log-copy { min-width: 0; }
.log-copy strong { font-size: 13px; }
.log-copy p { margin: 1px 0 0; color: #5b687b; font-size: 13px; overflow-wrap: anywhere; }
.log-details { display: flex; flex-direction: column; }
.inspector-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--line); background: #f2f5f9; }
.inspector-tabs button { padding: 7px 4px; border: 0; border-bottom: 2px solid transparent; color: #68768b; background: transparent; font-size: 12px; font-weight: 700; }
.inspector-tabs button.active { color: var(--primary); border-bottom-color: var(--primary); background: #fff; }
.inspector-content { min-height: 0; flex: 1; overflow: auto; padding: 7px; }
.empty-inline, .empty-table { padding: 18px; color: var(--muted); text-align: center; font-size: 12px; }
.change-item, .test-item { display: flex; gap: 7px; padding: 6px; border-bottom: 1px solid #e8edf4; }
.change-type, .test-mark { flex: 0 0 auto; height: 21px; padding: 2px 5px; border-radius: 5px; color: var(--primary); background: var(--primary-soft); font-size: 12px; }
.test-mark { width: 21px; display: grid; place-items: center; color: var(--success); background: var(--success-soft); }
.test-mark svg { width: 12px; }
.change-copy, .test-copy { min-width: 0; display: grid; }
.change-copy strong, .test-copy strong { font-size: 12px; overflow-wrap: anywhere; }
.change-copy span, .test-copy span { color: var(--muted); font-size: 12px; }
.build-output, .code-block { margin: 0; color: #26364d; font: 12px/1.55 ui-monospace, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
.software-card { display: grid; grid-template-rows: 33px minmax(0, 1fr); gap: 5px; }
.server-target { gap: 6px; color: var(--muted); font-size: 12px; }
.server-target svg { width: 15px; color: var(--primary); }
.server-target strong { color: #3b4a61; }
.table-wrap { min-height: 0; overflow: auto; border: 1px solid var(--line); border-radius: 9px; }
table { width: 100%; border-collapse: collapse; table-layout: fixed; font-size: 13px; }
th { position: sticky; top: 0; z-index: 1; padding: 6px 7px; color: #617086; background: #f2f5f9; text-align: left; font-weight: 750; white-space: nowrap; }
td { padding: 6px 7px; border-top: 1px solid #e8edf4; color: #4d5a6e; vertical-align: middle; }
th:nth-child(1) { width: 14%; } th:nth-child(2) { width: 13%; } th:nth-child(3) { width: 11%; }
th:nth-child(4) { width: 28%; } th:nth-child(5) { width: 8%; } th:nth-child(6) { width: 26%; }
.software-name, .file-cell { min-width: 0; display: grid; }
.software-name strong, .file-cell strong { overflow: hidden; text-overflow: ellipsis; color: #26354b; white-space: nowrap; }
.software-name span, .file-cell span { color: var(--muted); }
.file-cell > span { display: block; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cap-tags { display: flex; gap: 3px; flex-wrap: wrap; }
.cap-tags span { padding: 2px 5px; color: #3563a9; background: #eef4ff; border-radius: 5px; white-space: nowrap; }
.hash-value { display: inline; font-family: ui-monospace, Consolas, monospace; white-space: nowrap; }
.table-actions { display: flex; align-items: center; gap: 3px; flex-wrap: nowrap; }
.table-actions button, .table-actions a { min-width: 27px; height: 26px; padding: 3px 5px; display: inline-grid; place-items: center; border: 0; border-radius: 5px; color: #3563a9; background: #edf3ff; font-size: 12px; text-decoration: none; white-space: nowrap; }
.table-actions svg { width: 13px; height: 13px; }
.table-actions .publish-action { color: white; background: var(--primary); }
.table-actions .delete-action { color: var(--danger); background: #fff0f1; }
.baseline-protection { height: 26px; padding: 3px 6px; display: inline-flex; align-items: center; gap: 3px; color: var(--success); background: var(--success-soft); border-radius: 5px; font-size: 12px; white-space: nowrap; }
.baseline-protection svg { width: 13px; }
.status-badge[data-status="baseline"] { color: var(--success); border-color: #b9e5d3; background: var(--success-soft); }
.modal-backdrop { position: fixed; inset: 0; z-index: 50; display: grid; place-items: center; padding: 24px; background: rgba(31, 43, 61, .34); backdrop-filter: blur(4px); }
.modal-backdrop[hidden] { display: none; }
.modal { width: min(780px, 100%); max-height: min(680px, calc(100dvh - 48px)); display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; background: #fff; border: 1px solid var(--line); border-radius: 16px; box-shadow: var(--shadow); }
.modal header, .modal footer { padding: 14px 16px; display: flex; align-items: center; justify-content: space-between; gap: 12px; border-bottom: 1px solid var(--line); }
.modal footer { justify-content: flex-end; border-top: 1px solid var(--line); border-bottom: 0; }
.modal-body { overflow: auto; padding: 16px; }
.detail-grid, .diff-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
.detail-field, .diff-panel { min-width: 0; padding: 11px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface-soft); }
.detail-field span { display: block; color: var(--muted); font-size: 12px; }
.detail-field strong { display: block; margin-top: 3px; overflow-wrap: anywhere; }
.diff-panel h3 { margin: 0 0 8px; font-size: 14px; }
.diff-panel p { margin: 5px 0; color: #58677c; }
.diff-panel.added { border-color: #b9e5d3; background: var(--success-soft); }
.toast-region { position: fixed; z-index: 80; right: 22px; bottom: 22px; display: grid; gap: 8px; }
.toast { max-width: 390px; padding: 10px 13px; color: #fff; background: #344258; border-radius: 9px; box-shadow: 0 10px 28px rgba(31, 43, 61, .22); font-size: 13px; }
.toast.success { background: var(--success); }
.toast.error { background: var(--danger); }
@keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(40, 105, 232, .09); } }
@media (max-width: 980px) {
body { padding: 8px; }
.shell { width: 100%; height: calc(100dvh - 16px); min-height: 560px; border-radius: 16px; }
.topbar { height: 62px; padding: 0 14px; }
.topbar-status .dashboard-chip, .environment-chip:last-child { display: none; }
main { height: calc(100% - 62px); grid-template-columns: 330px minmax(0, 1fr); grid-template-rows: 108px minmax(0, 1fr) 164px; padding: 8px; gap: 8px; }
.card { padding: 10px; }
.pipeline { width: calc(100% - 190px); }
.release-result { width: 176px; right: 10px; }
.log-workspace { grid-template-columns: 1fr; }
.log-details { display: none; }
.metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 720px) {
body { display: block; padding: 0; }
.shell { height: 100dvh; min-height: 0; border: 0; border-radius: 0; }
.topbar { height: 58px; }
h1 { font-size: 17px; }
.eyebrow, .topbar-status { display: none; }
main { height: calc(100% - 58px); overflow: auto; grid-template-columns: 1fr; grid-template-rows: 104px 360px 330px 190px; grid-template-areas: "pipeline" "command" "execution" "software"; }
.execution-grid { display: block; }
.execution-grid .log-card { min-height: 330px; }
.pipeline { width: 100%; }
.pipeline-card { overflow-x: auto; }
.pipeline-card .pipeline { width: 680px; }
.release-result { display: none; }
.stage-name { font-size: 11px; }
.stage-state { display: none; }
.command-card { gap: 5px; }
.command-footer p { display: none; }
.command-footer { justify-content: flex-end; }
.prompt-templates button { flex: 1; }
th:nth-child(2), td:nth-child(2), th:nth-child(3), td:nth-child(3) { display: none; }
th:nth-child(1), th:nth-child(4), th:nth-child(5), th:nth-child(6) { width: auto; }
}
body.embedded {
display: block;
padding: 0;
background: transparent;
}
body.embedded .shell {
width: 100%;
height: 100dvh;
min-height: 0;
border: 0;
border-radius: 0;
box-shadow: none;
background: #f7f9fc;
}
body.embedded .topbar { display: none; }
body.embedded main {
height: 100%;
padding: 8px;
grid-template-columns: 350px minmax(0, 1fr);
grid-template-rows: 82px minmax(0, 1fr) 204px;
gap: 8px;
}
body.embedded .card { padding: 8px 10px; border-radius: 12px; }
body.embedded .card-heading { min-height: 27px; }
body.embedded .command-card { gap: 5px; }
body.embedded textarea { min-height: 48px; padding: 7px 9px; }
body.embedded .prompt-templates button { padding: 4px 7px; }
body.embedded .capability-brief { min-height: 44px; padding: 5px 8px; }
body.embedded .pipeline { height: 48px; margin: 4px 0 0; }
body.embedded .stage-node { width: 24px; height: 24px; }
body.embedded .pipeline-stage::after { top: 12px; }
body.embedded .stage-node svg { width: 12px; }
body.embedded .stage-name { font-size: 12px; }
body.embedded .stage-state { font-size: 11px; }
body.embedded .release-result { top: 18px; min-height: 44px; padding: 5px 8px; }
body.embedded .result-icon { width: 28px; height: 28px; }
body.embedded .log-card { gap: 5px; }
body.embedded .metrics div { padding: 3px 6px; }
body.embedded .software-card { grid-template-rows: 27px minmax(0, 1fr); gap: 3px; }
body.embedded th { padding: 4px 6px; }
body.embedded table { font-size: 12.5px; }
body.embedded td { padding: 3px 6px; }
body.embedded .software-name strong,
body.embedded .file-cell strong { line-height: 1.2; }
body.embedded .software-name span,
body.embedded .file-cell span { line-height: 1.15; }
@media (max-width: 720px) {
body.embedded main {
height: 100%;
overflow: auto;
grid-template-columns: 1fr;
grid-template-rows: 96px 350px 320px 210px;
grid-template-areas: "pipeline" "command" "execution" "software";
}
body.embedded .pipeline-card { overflow-x: auto; }
body.embedded .pipeline-card .pipeline { width: 680px; }
}
+143
View File
@@ -0,0 +1,143 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta name="theme-color" content="#f3f6fb" />
<title>座椅固件开发工作台 · 511 工程</title>
<link rel="icon" href="/icons/app-icon-192.png" />
<link rel="stylesheet" href="/firmware-workbench.css?v=5" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/firmware-workbench.js?v=3" defer></script>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="topbar-main">
<a class="back-link" href="/" aria-label="返回智能座舱">
<i data-lucide="arrow-left"></i>
</a>
<div class="product-mark" aria-hidden="true"><i data-lucide="blocks"></i></div>
<h1>座椅固件开发工作台</h1>
</div>
<div class="topbar-status">
<span class="server-state" id="serverState"><span></span>正在连接发布服务</span>
</div>
</header>
<main>
<section class="hero-grid" aria-label="固件开发任务">
<article class="card command-card">
<div class="card-heading">
<div>
<h2>开发指令</h2>
</div>
<label class="baseline-picker" for="baselineSelect">
<i data-lucide="git-branch"></i>
<span>固件基线</span>
<select id="baselineSelect" aria-label="选择固件基线">
<option value="time_only">时间控制</option>
</select>
</label>
</div>
<label for="instructionInput">描述需要扩展的副驾座椅能力</label>
<textarea id="instructionInput" rows="5" maxlength="2000">将副驾座椅固件从仅时间控制扩展为时间+位置参数控制,并增加座椅通风和座椅加热功能;完成编译、测试验证、固件打包,并发布到升级服务器。</textarea>
<div class="prompt-templates" aria-label="常用开发指令">
<button type="button" data-prompt="增加支持座椅位置参数控制的能力">+ 位置控制</button>
<button type="button" data-prompt="增加座椅通风功能">+ 座椅通风</button>
<button type="button" data-prompt="增加座椅加热功能">+ 座椅加热</button>
</div>
<div class="capability-brief" aria-label="固件能力升级范围">
<div><span>当前基线</span><strong id="baselineCapabilities">时间控制</strong></div>
<i data-lucide="chevrons-right"></i>
<div class="capability-target"><span>目标固件能力</span><strong id="targetCapabilities">时间控制+位置控制+座椅通风+座椅加热</strong></div>
</div>
<div class="command-footer">
<div class="button-row">
<button class="button button-secondary" id="cancelTaskButton" type="button" disabled>
<i data-lucide="square"></i>取消任务
</button>
<button class="button button-primary" id="startTaskButton" type="button">
<i data-lucide="play"></i>开始生成
</button>
</div>
</div>
</article>
</section>
<section class="card pipeline-card" aria-label="固件交付流水线">
<ol class="pipeline" id="pipeline" aria-label="固件交付阶段"></ol>
<div class="release-result" id="releaseResult" data-state="idle">
<span class="result-icon"><i data-lucide="cloud-upload"></i></span>
<div><small>当前交付状态</small><strong>尚未开始</strong></div>
</div>
<div class="status-legend" aria-label="阶段状态图例">
<span data-status="waiting">等待</span><span data-status="running">运行中</span>
<span data-status="pending_confirmation">待确认</span><span data-status="success">成功</span>
<span data-status="failed">失败</span><span data-status="canceled">取消</span>
<span data-status="blocked">阻塞</span>
</div>
</section>
<section class="execution-grid">
<article class="card log-card" aria-labelledby="logTitle">
<div class="card-heading">
<div>
<h2 id="logTitle">任务执行日志</h2>
</div>
<div class="log-actions">
<span class="status-badge" id="taskStatusBadge" data-status="waiting">等待</span>
<button class="icon-button" id="retryTaskButton" type="button" title="重试失败任务" disabled><i data-lucide="refresh-cw"></i></button>
<button class="icon-button" id="scrollLogButton" type="button" title="滚动到最新日志"><i data-lucide="arrow-down-to-line"></i></button>
</div>
</div>
<dl class="metrics" id="taskMetrics">
<div><dt>开始时间</dt><dd>—</dd></div><div><dt>结束时间</dt><dd>—</dd></div>
<div><dt>实际耗时</dt><dd>—</dd></div><div><dt>文件变更</dt><dd>—</dd></div>
</dl>
<div class="log-workspace">
<div class="log-stream" id="logStream" role="log" aria-live="polite">
<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>
</div>
<aside class="log-details" aria-label="日志附属详情">
<div class="inspector-tabs" role="tablist">
<button class="active" type="button" role="tab" data-inspector-tab="changes">文件变更</button>
<button type="button" role="tab" data-inspector-tab="build">编译输出</button>
<button type="button" role="tab" data-inspector-tab="tests">测试验证</button>
</div>
<div class="inspector-content" id="inspectorContent">
<div class="empty-inline">暂无任务数据</div>
</div>
</aside>
</div>
</article>
</section>
<section class="card software-card" aria-labelledby="softwareTitle">
<div class="card-heading software-heading">
<div>
<h2 id="softwareTitle">固件版本与软件产物</h2>
</div>
<div class="server-target"><i data-lucide="server"></i><span>发布目标</span><strong id="otaServerName">车机升级服务器</strong></div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>软件 / 版本</th><th>能力</th><th>构建时间</th><th>固件文件 / 校验</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="artifactTableBody"><tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr></tbody>
</table>
</div>
</section>
</main>
</div>
<div class="modal-backdrop" id="detailModal" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<header><h2 id="modalTitle">固件详情</h2><button class="icon-button" id="closeModalButton" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header>
<div class="modal-body" id="modalBody"></div>
<footer><button class="button button-secondary" id="modalCloseButton" type="button">关闭</button></footer>
</section>
</div>
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
</body>
</html>
+449
View File
@@ -0,0 +1,449 @@
(() => {
"use strict";
if (window.self !== window.top || new URLSearchParams(window.location.search).has("embedded")) {
document.body.classList.add("embedded");
}
const STATUS_LABELS = {
waiting: "等待",
running: "运行中",
pending_confirmation: "待确认",
success: "成功",
failed: "失败",
canceled: "取消",
blocked: "阻塞",
published: "已发布",
ready: "待发布",
baseline: "基线固件",
};
const STAGE_ICONS = {
instruction: "file-input",
requirements: "list-checks",
baseline: "git-compare-arrows",
codegen: "code-2",
build: "hammer",
test: "shield-check",
package: "package-check",
publish: "cloud-upload",
};
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
const FALLBACK_BASELINES = {
time_only: ["时间控制"],
};
const FALLBACK_PIPELINE = [
["instruction", "指令输入"], ["requirements", "需求解析"],
["baseline", "基线检查"], ["codegen", "代码生成"],
["build", "编译构建"], ["test", "测试验证"],
["package", "固件打包"], ["publish", "发布服务器"],
].map(([id, name]) => ({ id, name, status: "waiting", summary: "等待执行" }));
const ui = {
instruction: document.querySelector("#instructionInput"),
baseline: document.querySelector("#baselineSelect"),
baselineCapabilities: document.querySelector("#baselineCapabilities"),
targetCapabilities: document.querySelector("#targetCapabilities"),
start: document.querySelector("#startTaskButton"),
cancel: document.querySelector("#cancelTaskButton"),
retry: document.querySelector("#retryTaskButton"),
server: document.querySelector("#serverState"),
serverName: document.querySelector("#otaServerName"),
release: document.querySelector("#releaseResult"),
pipeline: document.querySelector("#pipeline"),
logs: document.querySelector("#logStream"),
scrollLogs: document.querySelector("#scrollLogButton"),
status: document.querySelector("#taskStatusBadge"),
metrics: document.querySelector("#taskMetrics"),
inspector: document.querySelector("#inspectorContent"),
artifacts: document.querySelector("#artifactTableBody"),
modal: document.querySelector("#detailModal"),
modalTitle: document.querySelector("#modalTitle"),
modalBody: document.querySelector("#modalBody"),
modalClose: document.querySelector("#closeModalButton"),
modalCloseFooter: document.querySelector("#modalCloseButton"),
toasts: document.querySelector("#toastRegion"),
};
const model = {
snapshot: { tasks: [], artifacts: [], releases: [], pipeline: FALLBACK_PIPELINE },
selectedTaskId: null,
inspectorTab: "changes",
pending: false,
pollTimer: null,
lastLogCount: 0,
};
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function api(path, options = {}) {
const response = await fetch(path, {
credentials: "same-origin",
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`);
return payload;
}
function selectedTask() {
return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId)
|| model.snapshot.tasks[0]
|| null;
}
function baselineCapabilities() {
const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value);
return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only;
}
function parseInstructionCapabilities(instruction) {
const lowered = String(instruction || "").toLowerCase();
const aliases = {
"时间控制": ["时间", "定时", "timed"],
"位置控制": ["位置", "position"],
"座椅通风": ["通风", "vent"],
"座椅加热": ["加热", "heater", "heat"],
};
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
}
function updateCapabilityPreview(task = null) {
const baseline = baselineCapabilities();
const taskMatchesDraft = task
&& task.instruction === ui.instruction.value.trim()
&& (task.baseline_id || "time_only") === ui.baseline.value;
const target = taskMatchesDraft
? task.target_capabilities
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
ui.baselineCapabilities.textContent = baseline.join("+");
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
}
function formatDate(value) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
hour12: false,
}).format(date).replaceAll("/", "-");
}
function formatClock(value) {
if (!value) return "--:--:--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--:--:--";
return new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
}).format(date);
}
function formatDuration(milliseconds) {
if (milliseconds === null || milliseconds === undefined) return "—";
if (milliseconds < 1000) return `${milliseconds} 毫秒`;
const seconds = milliseconds / 1000;
return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`;
}
function formatSize(bytes) {
const value = Number(bytes || 0);
if (value < 1024) return `${value} 字节`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`;
return `${(value / 1024 / 1024).toFixed(2)} 兆字节`;
}
function toast(message, type = "info") {
const element = document.createElement("div");
element.className = `toast ${type}`;
element.textContent = message;
ui.toasts.append(element);
window.setTimeout(() => element.remove(), 4200);
}
function refreshIcons() {
if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } });
}
function renderPipeline(task) {
const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE;
ui.pipeline.innerHTML = stages.map((stage) => `
<li class="pipeline-stage" data-status="${escapeHtml(stage.status || "waiting")}" title="${escapeHtml(stage.summary || "")}">
<span class="stage-node"><i data-lucide="${STAGE_ICONS[stage.id] || "circle"}"></i></span>
<strong class="stage-name">${escapeHtml(stage.name)}</strong>
<span class="stage-state">${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}</span>
</li>`).join("");
}
function renderRelease(task) {
let icon = "cloud-upload";
let label = "尚未开始";
let state = "idle";
if (task) {
label = task.message || STATUS_LABELS[task.status] || task.status;
state = task.status;
if (task.status === "success") { icon = "circle-check-big"; state = "success"; }
else if (task.status === "running") icon = "loader-circle";
else if (task.status === "failed") icon = "circle-x";
else if (task.status === "canceled") icon = "ban";
}
ui.release.dataset.state = state;
ui.release.innerHTML = `<span class="result-icon"><i data-lucide="${icon}"></i></span><div><small>当前交付状态</small><strong>${escapeHtml(label)}</strong></div>`;
}
function renderLogs(task) {
const logs = task?.logs || [];
if (!logs.length) {
ui.logs.innerHTML = `<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>`;
model.lastLogCount = 0;
return;
}
const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80;
ui.logs.innerHTML = logs.map((entry) => `
<article class="log-entry" data-level="${escapeHtml(entry.level)}">
<time class="log-time">${escapeHtml(formatClock(entry.time))}</time>
<span class="log-dot"></span>
<div class="log-copy"><strong>${escapeHtml(entry.title)}</strong><p>${escapeHtml(entry.message)}</p></div>
</article>`).join("");
if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight;
model.lastLogCount = logs.length;
}
function renderInspector(task) {
const status = task?.status || "waiting";
ui.status.dataset.status = status;
ui.status.textContent = STATUS_LABELS[status] || status;
const fileCount = task?.file_changes?.length || 0;
ui.metrics.innerHTML = `
<div><dt>开始时间</dt><dd>${escapeHtml(formatDate(task?.started_at))}</dd></div>
<div><dt>结束时间</dt><dd>${escapeHtml(formatDate(task?.finished_at))}</dd></div>
<div><dt>实际耗时</dt><dd>${escapeHtml(formatDuration(task?.duration_ms))}</dd></div>
<div><dt>文件变更</dt><dd>${fileCount ? `${fileCount} 个文件` : "—"}</dd></div>`;
document.querySelectorAll("[data-inspector-tab]").forEach((button) => {
button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab);
});
if (!task) {
ui.inspector.innerHTML = `<div class="empty-inline">暂无任务数据</div>`;
return;
}
if (model.inspectorTab === "build") {
ui.inspector.innerHTML = task.build_output?.length
? `<pre class="build-output">${escapeHtml(task.build_output.join("\n"))}</pre>`
: `<div class="empty-inline">尚无编译输出</div>`;
return;
}
if (model.inspectorTab === "tests") {
ui.inspector.innerHTML = task.test_results?.length
? task.test_results.map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">测试尚未执行</div>`;
return;
}
ui.inspector.innerHTML = task.file_changes?.length
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">尚无文件变更</div>`;
}
function renderArtifacts() {
const artifacts = model.snapshot.artifacts || [];
if (!artifacts.length) {
ui.artifacts.innerHTML = `<tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr>`;
return;
}
ui.artifacts.innerHTML = artifacts.map((artifact) => `
<tr>
<td><div class="software-name"><strong>${escapeHtml(artifact.software_name)}</strong><span>${escapeHtml(artifact.version)}</span></div></td>
<td><div class="cap-tags">${(artifact.capabilities || []).map((capability) => `<span>${escapeHtml(capability)}</span>`).join("")}</div></td>
<td>${escapeHtml(formatDate(artifact.build_time))}</td>
<td><div class="file-cell"><strong>${escapeHtml(artifact.firmware_filename)}</strong><span>${escapeHtml(formatSize(artifact.size_bytes))} · <span class="hash-value" title="${escapeHtml(artifact.sha256)}">${escapeHtml(artifact.sha256)}</span></span></div></td>
<td><span class="status-badge" data-status="${escapeHtml(artifact.status)}">${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</span></td>
<td><div class="table-actions">
<button type="button" title="查看详情" data-artifact-action="detail" data-artifact-id="${artifact.id}">详情</button>
<button type="button" title="查看能力差异" data-artifact-action="capabilities" data-artifact-id="${artifact.id}">能力</button>
<button type="button" title="查看代码差异" data-artifact-action="code" data-artifact-id="${artifact.id}">代码</button>
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
</div></td>
</tr>`).join("");
}
function render() {
const task = selectedTask();
if (task) model.selectedTaskId = task.id;
renderPipeline(task);
renderRelease(task);
renderLogs(task);
renderInspector(task);
renderArtifacts();
updateCapabilityPreview(task);
const active = task && ACTIVE_STATES.has(task.status);
ui.start.disabled = model.pending || Boolean(active);
ui.cancel.disabled = model.pending || !active;
ui.retry.disabled = model.pending || !task || !["failed", "canceled", "blocked"].includes(task.status);
ui.start.innerHTML = model.pending ? `<i data-lucide="loader-circle"></i>正在提交` : `<i data-lucide="play"></i>开始生成`;
refreshIcons();
}
async function refreshState({ quiet = false } = {}) {
try {
model.snapshot = await api("/api/firmware-workbench/state");
if (!model.selectedTaskId && model.snapshot.tasks?.length) model.selectedTaskId = model.snapshot.tasks[0].id;
ui.server.classList.add("online");
ui.server.innerHTML = `<span></span>发布服务已连接`;
ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器";
render();
} catch (error) {
ui.server.classList.remove("online");
ui.server.innerHTML = `<span></span>发布服务连接失败`;
if (!quiet) toast(error.message, "error");
} finally {
schedulePoll();
}
}
function schedulePoll() {
window.clearTimeout(model.pollTimer);
const active = model.snapshot.tasks?.some((task) => ACTIVE_STATES.has(task.status));
model.pollTimer = window.setTimeout(() => refreshState({ quiet: true }), active ? 700 : 5000);
}
async function startTask() {
const instruction = ui.instruction.value.trim();
if (!instruction) { toast("请输入开发指令。", "error"); ui.instruction.focus(); return; }
model.pending = true;
render();
try {
const result = await api("/api/firmware-workbench/tasks", {
method: "POST", body: JSON.stringify({ instruction, baseline_id: ui.baseline.value }),
});
model.selectedTaskId = result.task.id;
toast("开发任务已创建,流水线开始执行。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
async function taskAction(action) {
const task = selectedTask();
if (!task) return;
model.pending = true;
render();
try {
await api(`/api/firmware-workbench/tasks/${encodeURIComponent(task.id)}/${action}`, { method: "POST", body: "{}" });
toast(action === "cancel" ? "取消请求已提交。" : "任务已重新进入流水线。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
function appendPrompt(text) {
const current = ui.instruction.value.trim();
if (current.includes(text)) return;
ui.instruction.value = current ? `${current.replace(/[。;;]?$/, "")};${text}。` : `${text}。`;
ui.instruction.focus();
updateCapabilityPreview();
}
function showModal(title, content) {
ui.modalTitle.textContent = title;
ui.modalBody.innerHTML = content;
ui.modal.hidden = false;
document.body.style.overflow = "hidden";
refreshIcons();
ui.modalClose.focus();
}
function closeModal() {
ui.modal.hidden = true;
document.body.style.overflow = "";
}
async function artifactAction(button) {
const artifact = model.snapshot.artifacts.find((item) => item.id === button.dataset.artifactId);
if (!artifact) return;
const action = button.dataset.artifactAction;
if (action === "delete") {
if (!window.confirm(`确认删除“${artifact.firmware_filename}”及其服务器发布记录吗?此操作不可恢复。`)) return;
button.disabled = true;
try {
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}`, { method: "DELETE" });
toast(result.message, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "publish") {
button.disabled = true;
try {
const release = await api(`/api/firmware-workbench/artifacts/${artifact.id}/publish`, { method: "POST", body: "{}" });
toast(`${release.message}(${release.id})`, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "detail") {
showModal("固件产物详情", `<div class="detail-grid">
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
<div class="detail-field"><span>版本</span><strong>${escapeHtml(artifact.version)}</strong></div>
<div class="detail-field"><span>固件包</span><strong>${escapeHtml(artifact.firmware_filename)}</strong></div>
<div class="detail-field"><span>构建时间</span><strong>${escapeHtml(formatDate(artifact.build_time))}</strong></div>
<div class="detail-field"><span>固件包大小</span><strong>${escapeHtml(formatSize(artifact.size_bytes))}</strong></div>
<div class="detail-field"><span>发布状态</span><strong>${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</strong></div>
<div class="detail-field"><span>SHA-256(固件包)</span><strong>${escapeHtml(artifact.sha256)}</strong></div>
<div class="detail-field"><span>SHA-256(HEX 镜像)</span><strong>${escapeHtml(artifact.image_sha256)}</strong></div>
</div>`);
} else if (action === "capabilities") {
const diff = artifact.capability_diff || {};
showModal("能力差异", `<div class="diff-columns">
<div class="diff-panel"><h3>固件基线</h3>${(artifact.baseline_capabilities || []).map((item) => `<p>• ${escapeHtml(item)}</p>`).join("")}</div>
<div class="diff-panel added"><h3>目标固件变化</h3>${(diff.added || []).map((item) => `<p>+ ${escapeHtml(item)}</p>`).join("")}${(diff.retained || []).map((item) => `<p>✓ 保留${escapeHtml(item)}</p>`).join("") || "<p>无新增能力</p>"}</div>
</div>`);
} else if (action === "code") {
showModal("代码差异", `<pre class="code-block">${escapeHtml(artifact.code_diff || "暂无代码差异记录")}</pre>`);
} else if (action === "validation") {
showModal("验证记录", (artifact.validation_records || []).map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}</span></div></div>`).join("") || `<div class="empty-inline">暂无验证记录</div>`);
}
}
ui.start.addEventListener("click", startTask);
ui.instruction.addEventListener("input", () => updateCapabilityPreview());
ui.baseline.addEventListener("change", () => updateCapabilityPreview());
ui.cancel.addEventListener("click", () => taskAction("cancel"));
ui.retry.addEventListener("click", () => taskAction("retry"));
ui.scrollLogs.addEventListener("click", () => { ui.logs.scrollTop = ui.logs.scrollHeight; });
document.querySelectorAll("[data-prompt]").forEach((button) => button.addEventListener("click", () => appendPrompt(button.dataset.prompt)));
document.querySelectorAll("[data-inspector-tab]").forEach((button) => button.addEventListener("click", () => { model.inspectorTab = button.dataset.inspectorTab; renderInspector(selectedTask()); refreshIcons(); }));
ui.artifacts.addEventListener("click", (event) => {
const button = event.target.closest("[data-artifact-action]");
if (button) artifactAction(button);
});
ui.modalClose.addEventListener("click", closeModal);
ui.modalCloseFooter.addEventListener("click", closeModal);
ui.modal.addEventListener("click", (event) => { if (event.target === ui.modal) closeModal(); });
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !ui.modal.hidden) closeModal(); });
renderPipeline(null);
renderRelease(null);
updateCapabilityPreview();
refreshIcons();
refreshState();
})();
+4 -2
View File
@@ -13,8 +13,9 @@
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" /> <link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" />
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" /> <link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" /> <link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-29" /> <link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-30" />
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" /> <link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
<link rel="stylesheet" href="/firmware-workbench-entry.css?v=1" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script> <script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script> <script src="/vendor/three/0.147.0/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script> <script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
@@ -26,8 +27,9 @@
<script src="/pwa.js?v=android-pad-3" defer></script> <script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-66" defer></script> <script src="/console-v2.js?v=local-first-66" defer></script>
<script src="/self-extension.js?v=gscu-tcp-43" defer></script> <script src="/self-extension.js?v=gscu-tcp-43" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-29" defer></script> <script src="/assistant-chat.js?v=codex-assistant-30" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script> <script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
<script src="/firmware-workbench-entry.js?v=2" defer></script>
</head> </head>
<body class="console-v2"> <body class="console-v2">
<div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite"> <div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
+8 -3
View File
@@ -1,4 +1,4 @@
const CACHE_VERSION = "cognitive-agent-pad-v283"; const CACHE_VERSION = "cognitive-agent-pad-v288";
const SHELL_CACHE = `${CACHE_VERSION}-shell`; const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [ const APP_SHELL = [
@@ -16,10 +16,15 @@ const APP_SHELL = [
"/speech-input.css?v=sensevoice-7", "/speech-input.css?v=sensevoice-7",
"/speech-input.js?v=sensevoice-13", "/speech-input.js?v=sensevoice-13",
"/speech-audio-worklet.js?v=sensevoice-5", "/speech-audio-worklet.js?v=sensevoice-5",
"/assistant-chat.css?v=codex-assistant-29", "/assistant-chat.css?v=codex-assistant-30",
"/assistant-chat.js?v=codex-assistant-29", "/assistant-chat.js?v=codex-assistant-30",
"/energy-monitor.css?v=energy-monitor-4", "/energy-monitor.css?v=energy-monitor-4",
"/energy-monitor.js?v=energy-monitor-4", "/energy-monitor.js?v=energy-monitor-4",
"/firmware-workbench-entry.css?v=1",
"/firmware-workbench-entry.js?v=2",
"/firmware-workbench.html",
"/firmware-workbench.css?v=5",
"/firmware-workbench.js?v=3",
"/assets/pets/self-extension-cat.png?v=3", "/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3", "/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest", "/manifest.webmanifest",
+19
View File
@@ -0,0 +1,19 @@
id: lay_flat
name: 躺平模式
aliases:
- 躺平休息模式
- 平躺休息模式
description: 驻车休息预览:默认仅联动副驾靠背与腿托,配合恒温、柔和氛围光、关闭阅读灯与四扇车窗;指定主驾时仅使用其已接入的按摩能力,不调整驾驶姿态。所有动作需确认后保存或试运行。
icon: armchair
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {state: "on", mode: relax}, reason: 主驾只使用已接入的舒适按摩,不改变驾驶姿态。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 66}, reason: 为驻车躺靠休息提供副驾靠背支撑,不使用机械极限位置。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 76}, reason: 副驾腿托支撑腿部,具体姿态可在预览后调整。}
- {domain: climate, capability: climate.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 休息期间保持座舱恒温。}
- {domain: climate, capability: climate.set_temperature, target_scope: vehicle, operation: set, parameters: {temperature_c: 24}, reason: 提供可调整的中性舒适温度。}
- {domain: ambient_light, capability: ambient_light.set_illumination, target_scope: vehicle, operation: set, parameters: {color_hex: '#FFB347', brightness_pct: 20}, reason: 使用低亮暖色氛围光。}
- {domain: interior_light, capability: interior_light.set_reading_power, target_scope: all, operation: set, parameters: {state: "off"}, reason: 关闭阅读灯,减少躺靠休息时的直射光。}
- {domain: window, capability: window.set_position, target_scope: front_left, operation: set, parameters: {position_pct: 0}, reason: 关闭左前车窗,降低外部噪声。}
- {domain: window, capability: window.set_position, target_scope: front_right, operation: set, parameters: {position_pct: 0}, reason: 关闭右前车窗,降低外部噪声。}
- {domain: window, capability: window.set_position, target_scope: rear_left, operation: set, parameters: {position_pct: 0}, reason: 关闭左后车窗,降低外部噪声。}
- {domain: window, capability: window.set_position, target_scope: rear_right, operation: set, parameters: {position_pct: 0}, reason: 关闭右后车窗,降低外部噪声。}
@@ -0,0 +1,79 @@
/***************************************************************************//**
* \file main_cm0plus.c
*
* CYT4BFBCJE rev_d CM0+ entry point for the passenger-seat timed-only ECU.
*******************************************************************************/
#include "cy_project.h"
#include "cy_device_headers.h"
#include "bsp_seat_backrest_heater.h"
#include "bsp_seat_fan.h"
#include "motor_control.h"
#include "safety_manager.h"
#include "seat_move_config.h"
#include "seat_timed_application.h"
volatile uint32_t g_seatTimedResetReason = 0u;
volatile uint32_t g_seatTimedInitResult = MOTOR_API_FAILURE;
static void SeatTimed_HoldSafeOff(void)
{
for(;;)
{
Motor_ForceSafeOff();
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
SafetyManager_KickWatchdog();
Cy_SysTick_DelayInUs(SEAT_MOVE_TASK_PERIOD_US);
}
}
int main(void)
{
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
SystemInit();
Motor_ForceSafeOff();
if(!BspSeatBackrestHeater_ForceOff() || !BspSeatFan_ForceOff())
{
SeatTimed_HoldSafeOff();
}
g_seatTimedResetReason = Cy_SysReset_GetResetReason();
Cy_SysReset_ClearAllResetReasons();
SafetyManager_Init(g_seatTimedResetReason);
if(SafetyManager_IsInhibited())
{
SeatTimed_HoldSafeOff();
}
__enable_irq();
g_seatTimedInitResult = Motor_Init();
if(g_seatTimedInitResult != MOTOR_API_SUCCESS)
{
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
SeatTimed_HoldSafeOff();
}
Motor_SetWatchdogServiceEnabled(true);
if(!SeatTimedApplication_Init())
{
SeatTimed_HoldSafeOff();
}
for(;;)
{
SeatTimedApplication_Service();
if(SafetyManager_IsInhibited())
{
Motor_ForceSafeOff();
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
}
SafetyManager_KickWatchdog();
Cy_SysTick_DelayInUs(SEAT_MOVE_TASK_PERIOD_US);
}
}
/* [] END OF FILE */
@@ -0,0 +1,332 @@
#include "safety_manager.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "motor_control.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
#if defined(CoreDebug) && defined(CoreDebug_DHCSR_C_DEBUGEN_Msk)
#define SAFETY_DEBUG_DETACH_SUPPORTED (1u)
#else
#define SAFETY_DEBUG_DETACH_SUPPORTED (0u)
#endif
static volatile safety_fault_t latchedFault = SAFETY_FAULT_NONE;
static bool watchdogReset = false;
static bool watchdogInitialized = false;
static bool hilWatchdogResetArmed = false;
static bool hilWatchdogRecovered = false;
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
static bool debuggerWasAttached = false;
#endif
static bool SafetyManager_DebuggerAttached(void)
{
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
return ((CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) != 0ul);
#else
return false;
#endif
}
static uint32_t SafetyManager_ReadDebugMarker(void)
{
return BACKUP->unBREG[SEAT_MOVE_DEBUG_SESSION_BREG_INDEX].u32Register;
}
static bool SafetyManager_WriteDebugMarker(uint32_t marker)
{
BACKUP->unBREG[SEAT_MOVE_DEBUG_SESSION_BREG_INDEX].u32Register = marker;
__DSB();
return (SafetyManager_ReadDebugMarker() == marker);
}
static bool SafetyManager_WaitForWatchdogState(bool enabled)
{
uint32_t remaining = 200ul;
uint32_t expected = enabled ? 1ul : 0ul;
/* WDT ENABLE is an asynchronous request bit; ENABLED is the synchronized
* LF-clock state. Configuration writes made before disable completes are
* ignored by the hardware. */
while((WDT->unCTL.stcField.u1ENABLED != expected) && (remaining > 0ul))
{
Cy_SysTick_DelayInUs(1u);
remaining--;
}
return (WDT->unCTL.stcField.u1ENABLED == expected);
}
static void SafetyManager_ClearHilWatchdogCredential(void)
{
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = 0u;
__DSB();
}
void SafetyManager_Init(uint32_t resetReason)
{
const uint32_t watchdogMask = CY_SYSRESET_WDT |
CY_SYSRESET_MCWDT0 |
CY_SYSRESET_MCWDT1 |
CY_SYSRESET_MCWDT2 |
CY_SYSRESET_MCWDT3;
bool watchdogStopped;
bool debuggerAttached;
uint32_t marker;
uint32_t hilMagic;
uint32_t hilToken;
uint32_t hilCheck;
bool authorizedHilReset;
bool markerOk = true;
/* A watchdog instance is retained across its own reset on this device.
* Stop it before reading credentials or initializing peripherals; the
* 100 ms bench timeout otherwise permits a reset loop. */
Cy_WDT_Disable();
watchdogStopped = SafetyManager_WaitForWatchdogState(false);
debuggerAttached = SafetyManager_DebuggerAttached();
marker = SafetyManager_ReadDebugMarker();
hilMagic = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register;
hilToken = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register;
hilCheck = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register;
authorizedHilReset =
(hilMagic == SEAT_MOVE_HIL_WDT_MARKER) &&
(hilCheck == (hilMagic ^ hilToken ^ 0xFFFFFFFFu)) &&
(((resetReason & watchdogMask) != 0ul) ||
((resetReason & CY_SYSRESET_SOFT) != 0ul));
latchedFault = SAFETY_FAULT_NONE;
watchdogInitialized = false;
hilWatchdogResetArmed = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul) ||
authorizedHilReset;
hilWatchdogRecovered = authorizedHilReset;
if((resetReason & CY_SYSRESET_PORVDDD) != 0ul)
{
markerOk = SafetyManager_WriteDebugMarker(0u);
marker = SafetyManager_ReadDebugMarker();
SafetyManager_ClearHilWatchdogCredential();
hilWatchdogRecovered = false;
}
else if(hilWatchdogRecovered)
{
/* Consume before CAN comes online: transport retries cannot turn one
* authenticated reset into a second physical reset. */
SafetyManager_ClearHilWatchdogCredential();
}
else if(hilMagic == SEAT_MOVE_HIL_WDT_MARKER)
{
/* XRES/programming without a valid reset reason is not completion. */
SafetyManager_ClearHilWatchdogCredential();
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
debuggerWasAttached = debuggerAttached;
#endif
if(!markerOk || (marker == SEAT_MOVE_DEBUG_EXIT_MAGIC) ||
((marker == SEAT_MOVE_DEBUG_ACTIVE_MAGIC) && !debuggerAttached))
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
else if(watchdogReset && !hilWatchdogRecovered)
{
latchedFault = SAFETY_FAULT_BOOT_RECOVERY;
}
else if(debuggerAttached &&
!SafetyManager_WriteDebugMarker(SEAT_MOVE_DEBUG_ACTIVE_MAGIC))
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
/* The system-WDT configuration register is retained across some reset
* paths and AUTO_SERVICE is not reset by Cy_WDT_Init(). If that bit is
* left enabled, stopping the two software service paths can never prove a
* real watchdog reset because hardware services the counter at WARN_LIMIT.
* Configure only while stopped, and make software the sole WDT owner. */
if(!watchdogStopped)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Init();
Cy_WDT_Unlock();
Cy_WDT_SetUpperLimit(SEAT_MOVE_WDT_TIMEOUT_TICKS);
Cy_WDT_SetUpperAction(CY_WDT_LOW_UPP_ACTION_RESET);
Cy_WDT_SetWarnAction(CY_WDT_WARN_ACTION_NONE);
Cy_WDT_SetAutoService(CY_WDT_DISABLE);
Cy_WDT_SetDebugRun(CY_WDT_ENABLE);
Cy_WDT_Lock();
if((WDT->unUPPER_LIMIT.stcField.u32UPPER_LIMIT !=
SEAT_MOVE_WDT_TIMEOUT_TICKS) ||
(WDT->unCONFIG.stcField.u1UPPER_ACTION !=
(uint32_t)CY_WDT_LOW_UPP_ACTION_RESET) ||
(WDT->unCONFIG.stcField.u1AUTO_SERVICE !=
(uint32_t)CY_WDT_DISABLE))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Enable();
if(!SafetyManager_WaitForWatchdogState(true))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_ClearWatchdog();
Cy_SysTick_DelayInUs(1000u);
if(Cy_WDT_GetCount() == 0ul)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
watchdogInitialized = true;
SafetyManager_KickWatchdog();
}
void SafetyManager_KickWatchdog(void)
{
if((false != watchdogInitialized) && !hilWatchdogResetArmed)
{
Cy_WDT_ClearWatchdog();
}
}
bool SafetyManager_IsWatchdogInitialized(void)
{
return watchdogInitialized;
}
void SafetyManager_LatchFault(safety_fault_t fault)
{
if((fault != SAFETY_FAULT_NONE) &&
(latchedFault == SAFETY_FAULT_NONE))
{
latchedFault = fault;
Motor_LatchSafetyInhibitFromIsr(0u);
Motor_StopAll();
SeatMotorManager_StopAll(0ul);
}
}
void SafetyManager_Service(void)
{
bool debuggerAttached = SafetyManager_DebuggerAttached();
uint32_t marker = SafetyManager_ReadDebugMarker();
if(marker == SEAT_MOVE_DEBUG_EXIT_MAGIC)
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
}
if(Motor_IsDriverFaultLatched())
{
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
}
else
{
/* No newly observed driver fault. */
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
if(debuggerAttached)
{
if((marker != SEAT_MOVE_DEBUG_ACTIVE_MAGIC) &&
!SafetyManager_WriteDebugMarker(SEAT_MOVE_DEBUG_ACTIVE_MAGIC))
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
}
debuggerWasAttached = true;
}
else if(debuggerWasAttached)
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
debuggerWasAttached = false;
}
#else
(void)debuggerAttached;
#endif
SafetyManager_KickWatchdog();
}
bool SafetyManager_IsInhibited(void)
{
return (latchedFault != SAFETY_FAULT_NONE);
}
safety_fault_t SafetyManager_GetFault(void)
{
return latchedFault;
}
bool SafetyManager_WasWatchdogReset(void)
{
return watchdogReset;
}
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token)
{
uint32_t check;
if(!watchdogInitialized || hilWatchdogResetArmed)
{
return false;
}
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register =
SEAT_MOVE_HIL_WDT_MARKER;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = token;
check = SEAT_MOVE_HIL_WDT_MARKER ^ (uint32_t)token ^ 0xFFFFFFFFu;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = check;
__DSB();
if((BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register !=
SEAT_MOVE_HIL_WDT_MARKER) ||
(BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register != check))
{
SafetyManager_ClearHilWatchdogCredential();
return false;
}
/* Long-running motor calibration/coast loops normally service WDT
* directly so they remain safe even while the application loop is busy.
* The armed HIL case must suspend both service paths or a real watchdog
* reset can never occur. Startup re-enables this after the reset. */
Motor_SetWatchdogServiceEnabled(false);
Cy_WDT_ClearWatchdog();
hilWatchdogResetArmed = true;
return true;
}
void SafetyManager_CommitHilWatchdogReset(void)
{
if(!hilWatchdogResetArmed)
{
return;
}
/* This bench's connected CMSIS-DAP can suppress the WDT reset output even
* after C-SPY detaches. First prove physical WDT expiry, then use the
* reference project's authenticated processor-reset fallback. */
while(Cy_WDT_GetCount() < SEAT_MOVE_WDT_TIMEOUT_TICKS)
{
__NOP();
}
__DSB();
NVIC_SystemReset();
for(;;)
{
__NOP();
}
}
bool SafetyManager_WasHilWatchdogRecovered(void)
{
return hilWatchdogRecovered;
}
@@ -0,0 +1,30 @@
#ifndef SAFETY_MANAGER_H
#define SAFETY_MANAGER_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
SAFETY_FAULT_NONE = 0,
SAFETY_FAULT_DRIVER = 1,
SAFETY_FAULT_CAN_BUS_OFF = 2,
SAFETY_FAULT_CONFIG_STORE = 3,
SAFETY_FAULT_BOOT_RECOVERY = 4,
SAFETY_FAULT_DEBUG_DETACH = 5,
SAFETY_FAULT_CAN_INIT = 6
} safety_fault_t;
void SafetyManager_Init(uint32_t resetReason);
void SafetyManager_Service(void);
void SafetyManager_LatchFault(safety_fault_t fault);
void SafetyManager_KickWatchdog(void);
bool SafetyManager_IsWatchdogInitialized(void);
bool SafetyManager_IsInhibited(void);
safety_fault_t SafetyManager_GetFault(void);
bool SafetyManager_WasWatchdogReset(void);
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token);
void SafetyManager_CommitHilWatchdogReset(void);
bool SafetyManager_WasHilWatchdogRecovered(void);
#endif /* SAFETY_MANAGER_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
#ifndef SEAT_MOTOR_MANAGER_H
#define SEAT_MOTOR_MANAGER_H
#include <stdbool.h>
#include <stdint.h>
#include "motor_control.h"
typedef enum
{
SEAT_MOTOR_TASK_IDLE = 0,
SEAT_MOTOR_TASK_WAIT_START,
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,
SEAT_MOTOR_TASK_COMPLETE,
SEAT_MOTOR_TASK_FAILED
} seat_motor_task_state_t;
typedef enum
{
SEAT_MOTOR_RESULT_NONE = 0,
SEAT_MOTOR_RESULT_RUNNING,
SEAT_MOTOR_RESULT_OK,
SEAT_MOTOR_RESULT_STOPPED,
SEAT_MOTOR_RESULT_STALL,
SEAT_MOTOR_RESULT_ENDPOINT,
SEAT_MOTOR_RESULT_TIMEOUT,
SEAT_MOTOR_RESULT_DRIVER_FAULT,
SEAT_MOTOR_RESULT_INVALID,
SEAT_MOTOR_RESULT_CALIBRATION_FAILED
} seat_motor_result_t;
typedef struct
{
seat_motor_task_state_t taskState;
seat_motor_result_t result;
motor_event_t lastMotorEvent;
motor_direction_t direction;
uint32_t commandSequence;
uint32_t valueX100;
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;
} seat_motor_status_t;
void SeatMotorManager_Init(void);
void SeatMotorManager_Service(void);
void SeatMotorManager_Stop(motor_mode_t mode, uint32_t commandSequence);
void SeatMotorManager_StopAll(uint32_t commandSequence);
bool SeatMotorManager_IsAnyActive(void);
bool SeatMotorManager_GetStatus(motor_mode_t mode,
seat_motor_status_t *status);
/* Six standardized, non-blocking motion primitives. Physical values are x100. */
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,
uint32_t commandSequence);
bool SeatMotor_MoveTimedDuty(motor_mode_t mode,
motor_direction_t direction,
uint32_t durationMs,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteSpeed(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
bool SeatMotor_MoveRelativeSpeed(motor_mode_t mode,
int32_t deltaValueX100,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
bool SeatMotor_MoveTimedSpeed(motor_mode_t mode,
motor_direction_t direction,
uint32_t durationMs,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
/* Position command with an explicit requested arrival time. */
bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t commandSequence);
/* Keep the same speed plan while allowing a bounded scheduler/settling grace
* before the manager reports TIMEOUT. */
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Hall-coordinate variants used by firmware-owned synchronized trajectories.
* The target is checked against the live calibrated MaxHall before conversion
* to the motor's physical value. */
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */
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);
#endif /* SEAT_MOTOR_MANAGER_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
#ifndef SEAT_TIMED_APPLICATION_H
#define SEAT_TIMED_APPLICATION_H
#include <stdbool.h>
#include <stdint.h>
bool SeatTimedApplication_Init(void);
void SeatTimedApplication_Service(void);
void SeatTimedApplication_StopAll(uint16_t sequence);
#endif /* SEAT_TIMED_APPLICATION_H */
@@ -0,0 +1,687 @@
#include "bsp_seat_backrest_heater.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_backrest_heater_config.h"
static bool BspSeatBackrestHeater_ConfigClock(void);
static uint32_t BspSeatBackrestHeater_DutyToCompare(uint32_t dutyPercent);
static void BspSeatBackrestHeater_SetPinGpioLow(void);
static bool BspSeatBackrestHeaterCan_ConfigClock(void);
static bool BspSeatBackrestHeaterCan_ConfigPins(void);
static void BspSeatBackrestHeaterCan_IrqHandler(void);
static void BspSeatBackrestHeaterCan_RxCallback(
bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
static uint32_t s_heaterDutyPercent = 0u;
volatile uint32_t g_bspSeatBackrestHeaterStartFailStage = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatBackrestHeaterCanRxCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanTxCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastRxId = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastTxId = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastTxStatus =
(uint32_t)CY_CANFD_SUCCESS;
static volatile bsp_seat_backrest_heater_can_frame_t
s_canRxQueue[SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_canRxQueueHead = 0u;
static volatile uint8_t s_canRxQueueTail = 0u;
static const cy_stc_id_filter_t s_canStandardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID,
SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID,
SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID,
SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID,
SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_BACKREST_HEATER_CAN_IRQN,
.intIdx = SEAT_BACKREST_HEATER_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatBackrestHeaterCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = NULL,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_canStandardIdFilters) /
sizeof(s_canStandardIdFilters[0]),
.sidFilter = s_canStandardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements =
SEAT_BACKREST_HEATER_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT,
};
static const cy_stc_tcpwm_pwm_config_t s_heaterPwmConfig =
{
.pwmMode = CY_TCPWM_PWM_MODE_PWM,
.clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1,
.debug_pause = false,
.deadTime = 0u,
.deadTimeComp = 0u,
.runMode = CY_TCPWM_PWM_CONTINUOUS,
.countDirection = CY_TCPWM_COUNTER_COUNT_UP,
.cc0MatchMode = CY_TCPWM_PWM_TR_CTRL2_CLEAR,
.overflowMode = CY_TCPWM_PWM_TR_CTRL2_SET,
.underflowMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.cc1MatchMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.period = SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u,
.period_buff = 0u,
.enablePeriodSwap = false,
.enableLineSelSwap = false,
.compare0 = 0u,
.compare0_buff = 0u,
.enableCompare0Swap = false,
.compare1 = 0u,
.compare1_buff = 0u,
.enableCompare1Swap = false,
.interruptSources = CY_TCPWM_INT_NONE,
.invertPWMOut = 0u,
.invertPWMOutN = 0u,
.killMode = CY_TCPWM_PWM_STOP_ON_KILL,
.switchInputMode = CY_TCPWM_INPUT_LEVEL,
.switchInput = 0u,
.reloadInputMode = CY_TCPWM_INPUT_LEVEL,
.reloadInput = 0u,
.startInputMode = CY_TCPWM_INPUT_LEVEL,
.startInput = 0u,
.kill0InputMode = CY_TCPWM_INPUT_LEVEL,
.kill0Input = 0u,
.kill1InputMode = CY_TCPWM_INPUT_LEVEL,
.kill1Input = 0u,
.countInputMode = CY_TCPWM_INPUT_LEVEL,
.countInput = 1u,
.immediateKill = true,
.pwmOnDisable = CY_TCPWM_PWM_OUT_MODE_LOW,
.trigger0EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
.trigger1EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
};
bool BspSeatBackrestHeater_ForceOff(void)
{
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
cy_en_gpio_status_t status;
status = Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
&pinConfig);
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
s_heaterDutyPercent = 0u;
return ((status == CY_GPIO_SUCCESS) &&
(Cy_GPIO_GetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN) ==
SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM));
}
bool BspSeatBackrestHeater_InitPwm(void)
{
if(!BspSeatBackrestHeater_ConfigClock())
{
return false;
}
Cy_Tcpwm_Pwm_Disable(SEAT_BACKREST_HEATER_PWM_TCPWM);
if(Cy_Tcpwm_Pwm_Init(SEAT_BACKREST_HEATER_PWM_TCPWM,
&s_heaterPwmConfig) != CY_RET_SUCCESS)
{
return false;
}
return ((Cy_Tcpwm_Pwm_GetPeriod(SEAT_BACKREST_HEATER_PWM_TCPWM) ==
(SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u)) &&
(Cy_Tcpwm_Pwm_GetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM) == 0u));
}
bool BspSeatBackrestHeater_Start(uint32_t dutyPercent)
{
g_bspSeatBackrestHeaterStartFailStage = 0u;
if(!BspSeatBackrestHeater_SetDutyPercent(dutyPercent))
{
g_bspSeatBackrestHeaterStartFailStage = 1u;
return false;
}
/* P16.7 remains GPIO low until the PWM counter is confirmed running. */
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
Cy_Tcpwm_Pwm_SetCounter(SEAT_BACKREST_HEATER_PWM_TCPWM,
SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u);
Cy_Tcpwm_Pwm_Enable(SEAT_BACKREST_HEATER_PWM_TCPWM);
Cy_Tcpwm_TriggerStart(SEAT_BACKREST_HEATER_PWM_TCPWM);
Cy_SysTick_DelayInUs(2u);
if((Cy_Tcpwm_Pwm_GetStatus(SEAT_BACKREST_HEATER_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) == 0u)
{
g_bspSeatBackrestHeaterStartFailStage = 2u;
BspSeatBackrestHeater_FailSafeOff();
return false;
}
Cy_GPIO_SetDriveSel(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_SetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
SEAT_BACKREST_HEATER_PWM_HSIOM);
if(Cy_GPIO_GetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN) !=
SEAT_BACKREST_HEATER_PWM_HSIOM)
{
g_bspSeatBackrestHeaterStartFailStage = 3u;
BspSeatBackrestHeater_FailSafeOff();
return false;
}
return true;
}
bool BspSeatBackrestHeater_SetDutyPercent(uint32_t dutyPercent)
{
uint32_t compare;
/* A true OFF uses FailSafeOff, avoiding a possible zero-compare pulse. */
if((dutyPercent == 0u) || (dutyPercent > 100u))
{
return false;
}
compare = BspSeatBackrestHeater_DutyToCompare(dutyPercent);
Cy_Tcpwm_Pwm_SetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM, compare);
if(Cy_Tcpwm_Pwm_GetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM) != compare)
{
return false;
}
s_heaterDutyPercent = dutyPercent;
return true;
}
void BspSeatBackrestHeater_FailSafeOff(void)
{
Cy_Tcpwm_Pwm_Disable(SEAT_BACKREST_HEATER_PWM_TCPWM);
BspSeatBackrestHeater_SetPinGpioLow();
s_heaterDutyPercent = 0u;
}
bool BspSeatBackrestHeater_IsRunning(void)
{
return ((Cy_Tcpwm_Pwm_GetStatus(SEAT_BACKREST_HEATER_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) != 0u);
}
uint32_t BspSeatBackrestHeater_GetDutyPercent(void)
{
return s_heaterDutyPercent;
}
bool BspSeatBackrestHeaterCan_Init(void)
{
cy_en_canfd_status_t canStatus;
if(!BspSeatBackrestHeaterCan_ConfigClock() ||
!BspSeatBackrestHeaterCan_ConfigPins())
{
g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatBackrestHeaterCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_BACKREST_HEATER_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_BACKREST_HEATER_CAN_TYPE, &s_canConfig);
g_bspSeatBackrestHeaterCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatBackrestHeaterCan_Read(
bsp_seat_backrest_heater_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_canRxQueueTail;
if(tail == s_canRxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_canRxQueue[tail].identifier;
frame->data0 = s_canRxQueue[tail].data0;
frame->data1 = s_canRxQueue[tail].data1;
frame->dlc = s_canRxQueue[tail].dlc;
tail++;
if(tail >= SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_canRxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatBackrestHeaterCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1)
{
cy_stc_canfd_msg_t message;
uint8_t bufferIndex;
uint8_t dataIndex;
if(identifier > 0x7FFu)
{
g_bspSeatBackrestHeaterCanLastTxStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_BACKREST_HEATER_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(dataIndex = 2u;
dataIndex < CY_CANFD_MESSAGE_DATA_BUFFER_SIZE;
dataIndex++)
{
message.dataConfig.data[dataIndex] = 0u;
}
for(bufferIndex = 0u;
bufferIndex < SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_BACKREST_HEATER_CAN_TYPE,
bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatBackrestHeaterCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_BACKREST_HEATER_CAN_TYPE,
bufferIndex,
&message);
g_bspSeatBackrestHeaterCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatBackrestHeaterCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatBackrestHeaterCanTxBusyCount++;
return false;
}
static bool BspSeatBackrestHeaterCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_BACKREST_HEATER_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_BACKREST_HEATER_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_BACKREST_HEATER_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_BACKREST_HEATER_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(
SEAT_BACKREST_HEATER_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatBackrestHeaterCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_BACKREST_HEATER_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_BACKREST_HEATER_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t transceiverConfig =
{
.outVal = SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_RX_PORT,
SEAT_BACKREST_HEATER_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_TX_PORT,
SEAT_BACKREST_HEATER_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN,
&transceiverConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN) ==
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatBackrestHeaterCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_BACKREST_HEATER_CAN_TYPE);
}
static void BspSeatBackrestHeaterCan_RxCallback(
bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatBackrestHeaterCanRxCount++;
g_bspSeatBackrestHeaterCanLastRxId = message->idConfig.identifier;
head = s_canRxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_canRxQueueTail)
{
g_bspSeatBackrestHeaterCanRxQueueOverflowCount++;
return;
}
s_canRxQueue[head].identifier = message->idConfig.identifier;
s_canRxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_canRxQueue[head].data0 = message->dataConfig.data[0];
s_canRxQueue[head].data1 = message->dataConfig.data[1];
s_canRxQueueHead = nextHead;
}
static bool BspSeatBackrestHeater_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ) ||
((periClockHz % SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ;
if((divider == 0u) || (divider > 65536u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_BACKREST_HEATER_PWM_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_BACKREST_HEATER_PWM_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static uint32_t BspSeatBackrestHeater_DutyToCompare(uint32_t dutyPercent)
{
return (SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS * dutyPercent) / 100u;
}
static void BspSeatBackrestHeater_SetPinGpioLow(void)
{
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
Cy_GPIO_SetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM);
Cy_GPIO_SetDriveSel(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
}
@@ -0,0 +1,62 @@
#ifndef BSP_SEAT_BACKREST_HEATER_H
#define BSP_SEAT_BACKREST_HEATER_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_backrest_heater_can_frame_t;
/* Force the BTS7010 IN path to GPIO low before PWM initialization. */
bool BspSeatBackrestHeater_ForceOff(void);
/* Configure TCPWM1 counter 65 while P16.7 remains a strong GPIO low. */
bool BspSeatBackrestHeater_InitPwm(void);
/* Start a non-zero PWM duty, then connect P16.7 to TCPWM line 65. */
bool BspSeatBackrestHeater_Start(uint32_t dutyPercent);
/* Change a running non-zero duty. Use FailSafeOff for a true 0% output. */
bool BspSeatBackrestHeater_SetDutyPercent(uint32_t dutyPercent);
/* Disable TCPWM and restore P16.7 to strong GPIO low. */
void BspSeatBackrestHeater_FailSafeOff(void);
bool BspSeatBackrestHeater_IsRunning(void);
uint32_t BspSeatBackrestHeater_GetDutyPercent(void);
/*
* Start-path diagnostic for bench/HIL fault isolation.
* 0 = no start failure, 1 = duty/compare write, 2 = counter did not run,
* 3 = P16.7 HSIOM switch/readback.
*/
extern volatile uint32_t g_bspSeatBackrestHeaterStartFailStage;
/* Initialize the proven CAN4/TJA1044 board interface at 500 kbit/s. */
bool BspSeatBackrestHeaterCan_Init(void);
/* Pop one received standard Classic CAN frame from the interrupt-safe queue. */
bool BspSeatBackrestHeaterCan_Read(
bsp_seat_backrest_heater_can_frame_t *frame);
/* Send one 8-byte standard Classic CAN frame. */
bool BspSeatBackrestHeaterCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1);
/* Watch-window diagnostics for CAN hardware integration. */
extern volatile uint32_t g_bspSeatBackrestHeaterCanInitStatus;
extern volatile uint32_t g_bspSeatBackrestHeaterCanRxCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanTxCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanTxBusyCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastRxId;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastTxId;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastTxStatus;
#endif /* BSP_SEAT_BACKREST_HEATER_H */
@@ -0,0 +1,332 @@
#include "bsp_seat_current_adc.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_current_adc_config.h"
#define SEAT_CURRENT_ADC_DIV_ROUND_UP(a, b) (((a) + (b) - 1u) / (b))
typedef struct
{
volatile stc_GPIO_PRT_t *gpioPort;
uint32_t gpioPin;
en_hsiom_sel_t gpioHsiom;
volatile stc_PASS_SAR_CH_t *adcChannel;
cy_en_adc_pin_address_t pinAddress;
} bsp_seat_current_adc_profile_t;
static const bsp_seat_current_adc_profile_t s_profiles[] =
{
{
SEAT_CURRENT_M0_PORT,
SEAT_CURRENT_M0_PIN,
SEAT_CURRENT_M0_GPIO_HSIOM,
SEAT_CURRENT_M0_CHANNEL,
SEAT_CURRENT_M0_PIN_ADDRESS,
},
{
SEAT_CURRENT_M1_PORT,
SEAT_CURRENT_M1_PIN,
SEAT_CURRENT_M1_GPIO_HSIOM,
SEAT_CURRENT_M1_CHANNEL,
SEAT_CURRENT_M1_PIN_ADDRESS,
},
{
SEAT_CURRENT_M2_PORT,
SEAT_CURRENT_M2_PIN,
SEAT_CURRENT_M2_GPIO_HSIOM,
SEAT_CURRENT_M2_CHANNEL,
SEAT_CURRENT_M2_PIN_ADDRESS,
},
{
SEAT_CURRENT_M3_PORT,
SEAT_CURRENT_M3_PIN,
SEAT_CURRENT_M3_GPIO_HSIOM,
SEAT_CURRENT_M3_CHANNEL,
SEAT_CURRENT_M3_PIN_ADDRESS,
},
{
SEAT_CURRENT_HEATER_PORT,
SEAT_CURRENT_HEATER_PIN,
SEAT_CURRENT_HEATER_GPIO_HSIOM,
SEAT_CURRENT_HEATER_CHANNEL,
SEAT_CURRENT_HEATER_PIN_ADDRESS,
},
{
SEAT_CURRENT_FAN_PORT,
SEAT_CURRENT_FAN_PIN,
SEAT_CURRENT_FAN_GPIO_HSIOM,
SEAT_CURRENT_FAN_CHANNEL,
SEAT_CURRENT_FAN_PIN_ADDRESS,
},
};
typedef char bsp_seat_current_adc_profile_count_must_match[
((sizeof(s_profiles) / sizeof(s_profiles[0])) ==
(uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT) ? 1 : -1];
static bool s_initialized = false;
static bool s_sampleActive = false;
static uint16_t s_lastRaw[BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT];
static bool s_lastRawValid[BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT];
static bool BspSeatCurrentAdc_ConfigClock(uint32_t *actualClockHz);
static bool BspSeatCurrentAdc_WaitDone(
volatile stc_PASS_SAR_CH_t *channel);
static void BspSeatCurrentAdc_ClearStatus(
volatile stc_PASS_SAR_CH_t *channel);
bool BspSeatCurrentAdc_Init(void)
{
uint32_t actualClockHz;
uint32_t samplingCycles;
uint32_t index;
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_ANALOG,
.hsiom = HSIOM_SEL_GPIO,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_adc_config_t adcConfig =
{
.preconditionTime = 0u,
.powerupTime = 0u,
.enableIdlePowerDown = false,
.msbStretchMode = CY_ADC_MSB_STRETCH_MODE_1CYCLE,
.enableHalfLsbConv = false,
.sarMuxEnable = true,
.adcEnable = true,
.sarIpEnable = true,
};
cy_stc_adc_channel_config_t channelConfig =
{
.triggerSelection = CY_ADC_TRIGGER_OFF,
.channelPriority = 0u,
.preenptionType = CY_ADC_PREEMPTION_FINISH_RESUME,
.isGroupEnd = true,
.doneLevel = CY_ADC_DONE_LEVEL_PULSE,
.pinAddress = CY_ADC_PIN_ADDRESS_AN0,
.portAddress = SEAT_CURRENT_ADC_PORT_ADDRESS,
.extMuxSelect = 0u,
.extMuxEnable = true,
.preconditionMode = CY_ADC_PRECONDITION_MODE_OFF,
.overlapDiagMode = CY_ADC_OVERLAP_DIAG_MODE_OFF,
.sampleTime = 0u,
.calibrationValueSelect = CY_ADC_CALIBRATION_VALUE_REGULAR,
.postProcessingMode = CY_ADC_POST_PROCESSING_MODE_NONE,
.resultAlignment = CY_ADC_RESULT_ALIGNMENT_RIGHT,
.signExtention = CY_ADC_SIGN_EXTENTION_UNSIGNED,
.averageCount = 0u,
.rightShift = 0u,
.rangeDetectionMode = CY_ADC_RANGE_DETECTION_MODE_INSIDE_RANGE,
.rangeDetectionLoThreshold = 0u,
.rangeDetectionHiThreshold = SEAT_CURRENT_ADC_FULL_SCALE,
.mask.grpDone = true,
.mask.grpCancelled = false,
.mask.grpOverflow = false,
.mask.chRange = false,
.mask.chPulse = false,
.mask.chOverflow = false,
};
if(s_initialized)
{
return true;
}
for(index = 0u;
index < (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT;
index++)
{
pinConfig.hsiom = s_profiles[index].gpioHsiom;
if(Cy_GPIO_Pin_Init(s_profiles[index].gpioPort,
s_profiles[index].gpioPin,
&pinConfig) != CY_GPIO_SUCCESS)
{
return false;
}
s_lastRaw[index] = 0u;
s_lastRawValid[index] = false;
}
if(!BspSeatCurrentAdc_ConfigClock(&actualClockHz))
{
return false;
}
samplingCycles = (uint32_t)SEAT_CURRENT_ADC_DIV_ROUND_UP(
(SEAT_CURRENT_ADC_SAMPLE_TIME_MIN_NS * (uint64_t)actualClockHz),
1000000000ull);
if(samplingCycles == 0u)
{
samplingCycles = 1u;
}
channelConfig.sampleTime = (uint16_t)samplingCycles;
Cy_Adc_DeInit(SEAT_CURRENT_ADC_INSTANCE);
if(Cy_Adc_Init(SEAT_CURRENT_ADC_INSTANCE,
&adcConfig) != CY_ADC_SUCCESS)
{
return false;
}
for(index = 0u;
index < (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT;
index++)
{
channelConfig.pinAddress = s_profiles[index].pinAddress;
if(Cy_Adc_Channel_Init(s_profiles[index].adcChannel,
&channelConfig) != CY_ADC_SUCCESS)
{
Cy_Adc_DeInit(SEAT_CURRENT_ADC_INSTANCE);
return false;
}
Cy_Adc_Channel_Enable(s_profiles[index].adcChannel);
}
s_initialized = true;
return true;
}
bool BspSeatCurrentAdc_Sample(bsp_seat_current_adc_signal_t signal,
uint16_t *raw)
{
uint32_t index = (uint32_t)signal;
uint16_t sample = 0u;
cy_stc_adc_ch_status_t resultStatus = { false };
volatile stc_PASS_SAR_CH_t *channel;
bool success = false;
if(!s_initialized || s_sampleActive ||
(index >= (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT))
{
return false;
}
s_sampleActive = true;
channel = s_profiles[index].adcChannel;
BspSeatCurrentAdc_ClearStatus(channel);
Cy_Adc_Channel_SoftwareTrigger(channel);
if(BspSeatCurrentAdc_WaitDone(channel) &&
(Cy_Adc_Channel_GetResult(channel,
&sample,
&resultStatus) == CY_ADC_SUCCESS) &&
resultStatus.valid)
{
sample = (uint16_t)(sample & SEAT_CURRENT_ADC_FULL_SCALE);
s_lastRaw[index] = sample;
s_lastRawValid[index] = true;
if(raw != NULL)
{
*raw = sample;
}
success = true;
}
BspSeatCurrentAdc_ClearStatus(channel);
s_sampleActive = false;
return success;
}
bool BspSeatCurrentAdc_GetLastRaw(bsp_seat_current_adc_signal_t signal,
uint16_t *raw)
{
uint32_t index = (uint32_t)signal;
if((raw == NULL) ||
(index >= (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT) ||
!s_lastRawValid[index])
{
return false;
}
*raw = s_lastRaw[index];
return true;
}
static bool BspSeatCurrentAdc_ConfigClock(uint32_t *actualClockHz)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup = Cy_SysClk_GetClockGroup(SEAT_CURRENT_ADC_PCLK);
if((actualClockHz == NULL) ||
(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS))
{
return false;
}
divider = SEAT_CURRENT_ADC_DIV_ROUND_UP(
periClockHz, SEAT_CURRENT_ADC_OPERATION_FREQ_MAX_HZ);
if((divider == 0u) || (divider > 65536u))
{
return false;
}
if(Cy_SysClk_PeriphDisableDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(
SEAT_CURRENT_ADC_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphEnableDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
*actualClockHz = periClockHz / divider;
return true;
}
static bool BspSeatCurrentAdc_WaitDone(
volatile stc_PASS_SAR_CH_t *channel)
{
uint32_t timeout = SEAT_CURRENT_ADC_POLL_TIMEOUT;
cy_stc_adc_interrupt_source_t status = { false };
do
{
(void)Cy_Adc_Channel_GetInterruptStatus(channel, &status);
if(status.grpDone)
{
return true;
}
timeout--;
} while(timeout > 0u);
return false;
}
static void BspSeatCurrentAdc_ClearStatus(
volatile stc_PASS_SAR_CH_t *channel)
{
cy_stc_adc_interrupt_source_t clearSource =
{
.grpDone = true,
.grpCancelled = true,
.grpOverflow = true,
.chRange = false,
.chPulse = false,
.chOverflow = true,
};
(void)Cy_Adc_Channel_ClearInterruptStatus(channel, &clearSource);
}
@@ -0,0 +1,27 @@
#ifndef BSP_SEAT_CURRENT_ADC_H
#define BSP_SEAT_CURRENT_ADC_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
BSP_SEAT_CURRENT_ADC_MOTOR0 = 0,
BSP_SEAT_CURRENT_ADC_MOTOR1,
BSP_SEAT_CURRENT_ADC_MOTOR2,
BSP_SEAT_CURRENT_ADC_MOTOR3,
BSP_SEAT_CURRENT_ADC_HEATER,
BSP_SEAT_CURRENT_ADC_FAN,
BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT
} bsp_seat_current_adc_signal_t;
/* Initializes PASS0 SAR1 once and enables every shared seat-current channel. */
bool BspSeatCurrentAdc_Init(void);
/* Main-loop polling API. It must not be called from an ISR. */
bool BspSeatCurrentAdc_Sample(bsp_seat_current_adc_signal_t signal,
uint16_t *raw);
bool BspSeatCurrentAdc_GetLastRaw(bsp_seat_current_adc_signal_t signal,
uint16_t *raw);
#endif /* BSP_SEAT_CURRENT_ADC_H */
@@ -0,0 +1,659 @@
#include "bsp_seat_fan.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_fan_config.h"
static bool BspSeatFan_ConfigClock(void);
static uint32_t BspSeatFan_DutyToCompare(uint32_t dutyPercent);
static void BspSeatFan_SetPinGpioLow(volatile stc_GPIO_PRT_t *port,
uint32_t pin,
en_hsiom_sel_t gpioHsiom);
static bool BspSeatCan_ConfigClock(void);
static bool BspSeatCan_ConfigPins(void);
static void BspSeatCan_IrqHandler(void);
static void BspSeatCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
volatile uint32_t g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatCanRxCount = 0u;
volatile uint32_t g_bspSeatCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatCanTxCount = 0u;
volatile uint32_t g_bspSeatCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatCanLastRxId = 0u;
volatile uint32_t g_bspSeatCanLastTxId = 0u;
volatile uint32_t g_bspSeatCanLastTxStatus = (uint32_t)CY_CANFD_SUCCESS;
static volatile bsp_seat_can_frame_t s_canRxQueue[SEAT_FAN_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_canRxQueueHead = 0u;
static volatile uint8_t s_canRxQueueTail = 0u;
volatile uint32_t g_bspSeatFanStartFailStage = 0u;
static const cy_stc_id_filter_t s_canStandardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_CONTROL_29E_ID,
SEAT_FAN_CAN_CONTROL_29E_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_CONFIG_2AA_ID,
SEAT_FAN_CAN_CONFIG_2AA_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_SWITCH_42A_ID,
SEAT_FAN_CAN_SWITCH_42A_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_FAN_CAN_IRQN,
.intIdx = SEAT_FAN_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = NULL,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_canStandardIdFilters) / sizeof(s_canStandardIdFilters[0]),
.sidFilter = s_canStandardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = SEAT_FAN_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_FAN_CAN_TX_BUFFER_COUNT,
};
static const cy_stc_tcpwm_pwm_config_t s_seatFanPwmConfig =
{
.pwmMode = CY_TCPWM_PWM_MODE_PWM,
.clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1,
.debug_pause = false,
.deadTime = 0u,
.deadTimeComp = 0u,
.runMode = CY_TCPWM_PWM_CONTINUOUS,
.countDirection = CY_TCPWM_COUNTER_COUNT_UP,
.cc0MatchMode = CY_TCPWM_PWM_TR_CTRL2_CLEAR,
.overflowMode = CY_TCPWM_PWM_TR_CTRL2_SET,
.underflowMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.cc1MatchMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.period = SEAT_FAN_PWM_PERIOD_TICKS - 1u,
.period_buff = 0u,
.enablePeriodSwap = false,
.enableLineSelSwap = false,
.compare0 = SEAT_FAN_PWM_PERIOD_TICKS,
.compare0_buff = 0u,
.enableCompare0Swap = false,
.compare1 = 0u,
.compare1_buff = 0u,
.enableCompare1Swap = false,
.interruptSources = CY_TCPWM_INT_NONE,
.invertPWMOut = 0u,
.invertPWMOutN = 0u,
.killMode = CY_TCPWM_PWM_STOP_ON_KILL,
.switchInputMode = CY_TCPWM_INPUT_LEVEL,
.switchInput = 0u,
.reloadInputMode = CY_TCPWM_INPUT_LEVEL,
.reloadInput = 0u,
.startInputMode = CY_TCPWM_INPUT_LEVEL,
.startInput = 0u,
.kill0InputMode = CY_TCPWM_INPUT_LEVEL,
.kill0Input = 0u,
.kill1InputMode = CY_TCPWM_INPUT_LEVEL,
.kill1Input = 0u,
.countInputMode = CY_TCPWM_INPUT_LEVEL,
.countInput = 1u,
.immediateKill = true,
.pwmOnDisable = CY_TCPWM_PWM_OUT_MODE_LOW,
.trigger0EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
.trigger1EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
};
bool BspSeatFan_ForceOff(void)
{
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_FAN_PWM_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
cy_en_gpio_status_t pwmPinStatus;
cy_en_gpio_status_t unusedPinStatus;
pwmPinStatus = Cy_GPIO_Pin_Init(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
&pinConfig);
pinConfig.hsiom = SEAT_FAN_UNUSED_GPIO_HSIOM;
unusedPinStatus = Cy_GPIO_Pin_Init(SEAT_FAN_UNUSED_PORT,
SEAT_FAN_UNUSED_PIN,
&pinConfig);
/* Write the output latches again after configuration for an explicit safe state. */
Cy_GPIO_Write(SEAT_FAN_PWM_PORT, SEAT_FAN_PWM_PIN, 0u);
Cy_GPIO_Write(SEAT_FAN_UNUSED_PORT, SEAT_FAN_UNUSED_PIN, 0u);
return ((pwmPinStatus == CY_GPIO_SUCCESS) &&
(unusedPinStatus == CY_GPIO_SUCCESS));
}
bool BspSeatFan_InitPwm(void)
{
if(!BspSeatFan_ConfigClock())
{
return false;
}
Cy_Tcpwm_Pwm_Disable(SEAT_FAN_PWM_TCPWM);
if(Cy_Tcpwm_Pwm_Init(SEAT_FAN_PWM_TCPWM,
&s_seatFanPwmConfig) != CY_RET_SUCCESS)
{
return false;
}
return ((Cy_Tcpwm_Pwm_GetPeriod(SEAT_FAN_PWM_TCPWM) ==
(SEAT_FAN_PWM_PERIOD_TICKS - 1u)) &&
(Cy_Tcpwm_Pwm_GetCompare0(SEAT_FAN_PWM_TCPWM) ==
SEAT_FAN_PWM_PERIOD_TICKS));
}
bool BspSeatFan_Start(uint32_t dutyPercent)
{
g_bspSeatFanStartFailStage = 0u;
if(!BspSeatFan_SetDutyPercent(dutyPercent))
{
g_bspSeatFanStartFailStage = 1u;
return false;
}
/*
* P11.1 is still GPIO low here. Starting from PERIOD makes the first
* counter clock generate overflow immediately, which asserts LINE high
* for the 100% startup setting before the pin is switched to TCPWM.
*/
Cy_GPIO_Write(SEAT_FAN_PWM_PORT, SEAT_FAN_PWM_PIN, 0u);
Cy_Tcpwm_Pwm_SetCounter(SEAT_FAN_PWM_TCPWM,
SEAT_FAN_PWM_PERIOD_TICKS - 1u);
Cy_Tcpwm_Pwm_Enable(SEAT_FAN_PWM_TCPWM);
Cy_Tcpwm_TriggerStart(SEAT_FAN_PWM_TCPWM);
/* Keep the pin at GPIO low until the 2 MHz counter has started and overflowed. */
Cy_SysTick_DelayInUs(2u);
if((Cy_Tcpwm_Pwm_GetStatus(SEAT_FAN_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) == 0u)
{
g_bspSeatFanStartFailStage = 2u;
BspSeatFan_FailSafeOff();
return false;
}
Cy_GPIO_SetDriveSel(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_SetHSIOM(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
SEAT_FAN_PWM_HSIOM);
if(Cy_GPIO_GetHSIOM(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN) != SEAT_FAN_PWM_HSIOM)
{
g_bspSeatFanStartFailStage = 3u;
BspSeatFan_FailSafeOff();
return false;
}
return true;
}
bool BspSeatFan_SetDutyPercent(uint32_t dutyPercent)
{
uint32_t compare;
if(dutyPercent > 100u)
{
return false;
}
compare = BspSeatFan_DutyToCompare(dutyPercent);
Cy_Tcpwm_Pwm_SetCompare0(SEAT_FAN_PWM_TCPWM, compare);
return (Cy_Tcpwm_Pwm_GetCompare0(SEAT_FAN_PWM_TCPWM) == compare);
}
void BspSeatFan_FailSafeOff(void)
{
Cy_Tcpwm_Pwm_Disable(SEAT_FAN_PWM_TCPWM);
BspSeatFan_SetPinGpioLow(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
SEAT_FAN_PWM_GPIO_HSIOM);
BspSeatFan_SetPinGpioLow(SEAT_FAN_UNUSED_PORT,
SEAT_FAN_UNUSED_PIN,
SEAT_FAN_UNUSED_GPIO_HSIOM);
}
bool BspSeatCan_Init(void)
{
cy_en_canfd_status_t canStatus;
if(!BspSeatCan_ConfigClock() || !BspSeatCan_ConfigPins())
{
g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_FAN_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_FAN_CAN_TYPE, &s_canConfig);
g_bspSeatCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatCan_Read(bsp_seat_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_canRxQueueTail;
if(tail == s_canRxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_canRxQueue[tail].identifier;
frame->data0 = s_canRxQueue[tail].data0;
frame->data1 = s_canRxQueue[tail].data1;
frame->dlc = s_canRxQueue[tail].dlc;
tail++;
if(tail >= SEAT_FAN_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_canRxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1)
{
cy_stc_canfd_msg_t message;
uint8_t bufferIndex;
uint8_t dataIndex;
if(identifier > 0x7FFu)
{
g_bspSeatCanLastTxStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_FAN_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(dataIndex = 2u;
dataIndex < CY_CANFD_MESSAGE_DATA_BUFFER_SIZE;
dataIndex++)
{
message.dataConfig.data[dataIndex] = 0u;
}
for(bufferIndex = 0u;
bufferIndex < SEAT_FAN_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_FAN_CAN_TYPE, bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_FAN_CAN_TYPE,
bufferIndex,
&message);
g_bspSeatCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatCanTxBusyCount++;
return false;
}
static bool BspSeatCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_FAN_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_FAN_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_FAN_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_FAN_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_FAN_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_FAN_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_FAN_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t transceiverConfig =
{
.outVal = SEAT_FAN_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_FAN_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_RX_PORT,
SEAT_FAN_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_TX_PORT,
SEAT_FAN_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN,
&transceiverConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN,
SEAT_FAN_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN) ==
SEAT_FAN_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_FAN_CAN_TYPE);
}
static void BspSeatCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatCanRxCount++;
g_bspSeatCanLastRxId = message->idConfig.identifier;
head = s_canRxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_FAN_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_canRxQueueTail)
{
g_bspSeatCanRxQueueOverflowCount++;
return;
}
s_canRxQueue[head].identifier = message->idConfig.identifier;
s_canRxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_canRxQueue[head].data0 = message->dataConfig.data[0];
s_canRxQueue[head].data1 = message->dataConfig.data[1];
s_canRxQueueHead = nextHead;
}
static bool BspSeatFan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_FAN_COUNTER_CLOCK_HZ) ||
((periClockHz % SEAT_FAN_COUNTER_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_FAN_COUNTER_CLOCK_HZ;
if((divider == 0u) || (divider > 65536u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_FAN_PWM_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_FAN_PWM_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static uint32_t BspSeatFan_DutyToCompare(uint32_t dutyPercent)
{
return (SEAT_FAN_PWM_PERIOD_TICKS * dutyPercent) / 100u;
}
static void BspSeatFan_SetPinGpioLow(volatile stc_GPIO_PRT_t *port,
uint32_t pin,
en_hsiom_sel_t gpioHsiom)
{
/* Set the latch low before changing HSIOM to avoid a transient high. */
Cy_GPIO_Write(port, pin, 0u);
Cy_GPIO_SetHSIOM(port, pin, gpioHsiom);
Cy_GPIO_SetDriveSel(port, pin, CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(port, pin, CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_Write(port, pin, 0u);
}
@@ -0,0 +1,52 @@
#ifndef BSP_SEAT_FAN_H
#define BSP_SEAT_FAN_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_can_frame_t;
/* Configure P11.1 and P11.2 as strong GPIO outputs at logic low. */
bool BspSeatFan_ForceOff(void);
/* Configure the peripheral clock and PWM0-60 while P11.1 remains GPIO low. */
bool BspSeatFan_InitPwm(void);
/* Start PWM and only then connect P11.1 to the TCPWM line output. */
bool BspSeatFan_Start(uint32_t dutyPercent);
/* Change the active PWM compare value. Valid range is 0 through 100 percent. */
bool BspSeatFan_SetDutyPercent(uint32_t dutyPercent);
/* Disable TCPWM and restore both known fan-control pins to GPIO low. */
void BspSeatFan_FailSafeOff(void);
/* Read-only HIL diagnostic: 0=none, 1=compare, 2=counter, 3=HSIOM. */
extern volatile uint32_t g_bspSeatFanStartFailStage;
/* Initialize the proven CAN4/TJA1044 board interface at 500 kbit/s. */
bool BspSeatCan_Init(void);
/* Pop one received standard Classic CAN frame from the interrupt-safe queue. */
bool BspSeatCan_Read(bsp_seat_can_frame_t *frame);
/* Send one 8-byte standard Classic CAN frame. */
bool BspSeatCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1);
/* Watch-window diagnostics for hardware integration. */
extern volatile uint32_t g_bspSeatCanInitStatus;
extern volatile uint32_t g_bspSeatCanRxCount;
extern volatile uint32_t g_bspSeatCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatCanTxCount;
extern volatile uint32_t g_bspSeatCanTxBusyCount;
extern volatile uint32_t g_bspSeatCanLastRxId;
extern volatile uint32_t g_bspSeatCanLastTxId;
extern volatile uint32_t g_bspSeatCanLastTxStatus;
#endif /* BSP_SEAT_FAN_H */
@@ -0,0 +1,400 @@
#include "bsp_seat_move_can.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "motor_control.h"
#include "seat_move_config.h"
static bool BspSeatMoveCan_ConfigClock(void);
static bool BspSeatMoveCan_ConfigPins(void);
static void BspSeatMoveCan_IrqHandler(void);
static void BspSeatMoveCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
static void BspSeatMoveCan_ErrorCallback(cy_en_canfd_bus_error_t error);
volatile uint32_t g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatMoveCanRxCount = 0u;
volatile uint32_t g_bspSeatMoveCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatMoveCanTxCount = 0u;
volatile uint32_t g_bspSeatMoveCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatMoveCanLastRxId = 0u;
volatile uint32_t g_bspSeatMoveCanLastTxId = 0u;
volatile uint32_t g_bspSeatMoveCanLastTxStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatMoveCanBusOff = 0u;
static volatile bsp_seat_move_can_frame_t
s_rxQueue[SEAT_MOVE_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_rxQueueHead = 0u;
static volatile uint8_t s_rxQueueTail = 0u;
static const cy_stc_id_filter_t s_standardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_MOVE_CAN_COMMAND_ID,
SEAT_MOVE_CAN_COMMAND_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_MOVE_CAN_IRQN,
.intIdx = SEAT_MOVE_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatMoveCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = BspSeatMoveCan_ErrorCallback,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_standardIdFilters) / sizeof(s_standardIdFilters[0]),
.sidFilter = s_standardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = SEAT_MOVE_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_MOVE_CAN_TX_BUFFER_COUNT,
};
bool BspSeatMoveCan_Init(void)
{
cy_en_canfd_status_t canStatus;
s_rxQueueHead = 0u;
s_rxQueueTail = 0u;
g_bspSeatMoveCanBusOff = 0u;
if(!BspSeatMoveCan_ConfigClock() || !BspSeatMoveCan_ConfigPins())
{
g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatMoveCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_MOVE_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_MOVE_CAN_TYPE, &s_canConfig);
g_bspSeatMoveCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatMoveCan_Read(bsp_seat_move_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_rxQueueTail;
if(tail == s_rxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_rxQueue[tail].identifier;
frame->data0 = s_rxQueue[tail].data0;
frame->data1 = s_rxQueue[tail].data1;
frame->dlc = s_rxQueue[tail].dlc;
tail++;
if(tail >= SEAT_MOVE_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_rxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatMoveCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1)
{
cy_stc_canfd_msg_t message = { 0 };
uint8_t bufferIndex;
if((identifier > 0x7FFu) || (g_bspSeatMoveCanBusOff != 0u))
{
g_bspSeatMoveCanLastTxStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_MOVE_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(bufferIndex = 0u;
bufferIndex < SEAT_MOVE_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_MOVE_CAN_TYPE, bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatMoveCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_MOVE_CAN_TYPE, bufferIndex, &message);
g_bspSeatMoveCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatMoveCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatMoveCanTxBusyCount++;
return false;
}
static bool BspSeatMoveCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_MOVE_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_MOVE_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_MOVE_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_MOVE_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_MOVE_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatMoveCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_MOVE_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_MOVE_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t enableConfig =
{
.outVal = SEAT_MOVE_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_MOVE_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_RX_PORT,
SEAT_MOVE_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_TX_PORT,
SEAT_MOVE_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN,
&enableConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN,
SEAT_MOVE_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN) ==
SEAT_MOVE_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatMoveCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_MOVE_CAN_TYPE);
}
static void BspSeatMoveCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatMoveCanRxCount++;
g_bspSeatMoveCanLastRxId = message->idConfig.identifier;
if((message->idConfig.identifier == SEAT_MOVE_CAN_COMMAND_ID) &&
(message->dataConfig.dataLengthCode == SEAT_MOVE_CAN_DLC) &&
(((uint8_t)(message->dataConfig.data[0] & 0xFFu)) == 0x09u) &&
(((uint8_t)((message->dataConfig.data[0] >> 8u) & 0xFFu)) == 0x10u) &&
(message->dataConfig.data[1] == 0u))
{
Motor_RequestAbortAllFromIsr(
(uint16_t)(message->dataConfig.data[0] >> 16u));
}
head = s_rxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_MOVE_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_rxQueueTail)
{
g_bspSeatMoveCanRxQueueOverflowCount++;
return;
}
s_rxQueue[head].identifier = message->idConfig.identifier;
s_rxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_rxQueue[head].data0 = message->dataConfig.data[0];
s_rxQueue[head].data1 = message->dataConfig.data[1];
s_rxQueueHead = nextHead;
}
static void BspSeatMoveCan_ErrorCallback(cy_en_canfd_bus_error_t error)
{
if(error == CY_CANFD_BUSOFF)
{
g_bspSeatMoveCanBusOff = 1u;
}
}
@@ -0,0 +1,29 @@
#ifndef BSP_SEAT_MOVE_CAN_H
#define BSP_SEAT_MOVE_CAN_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_move_can_frame_t;
bool BspSeatMoveCan_Init(void);
bool BspSeatMoveCan_Read(bsp_seat_move_can_frame_t *frame);
bool BspSeatMoveCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1);
extern volatile uint32_t g_bspSeatMoveCanInitStatus;
extern volatile uint32_t g_bspSeatMoveCanRxCount;
extern volatile uint32_t g_bspSeatMoveCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatMoveCanTxCount;
extern volatile uint32_t g_bspSeatMoveCanTxBusyCount;
extern volatile uint32_t g_bspSeatMoveCanLastRxId;
extern volatile uint32_t g_bspSeatMoveCanLastTxId;
extern volatile uint32_t g_bspSeatMoveCanLastTxStatus;
extern volatile uint32_t g_bspSeatMoveCanBusOff;
#endif /* BSP_SEAT_MOVE_CAN_H */
@@ -0,0 +1,155 @@
#ifndef SEAT_BACKREST_HEATER_CONFIG_H
#define SEAT_BACKREST_HEATER_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* Schematic-confirmed backrest-heater path:
* P16.7 / KBJRD -> R126 (1 kOhm) -> BTS7010-1EPA IN -> backrest heater.
* The rev_d SDK maps P16.7 to TCPWM1 line 65.
*/
#define SEAT_BACKREST_HEATER_PWM_PORT GPIO_PRT16
#define SEAT_BACKREST_HEATER_PWM_PIN (7u)
#define SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM P16_7_GPIO
#define SEAT_BACKREST_HEATER_PWM_HSIOM P16_7_TCPWM1_LINE65
#define SEAT_BACKREST_HEATER_PWM_TCPWM TCPWM1_GRP0_CNT65
#define SEAT_BACKREST_HEATER_PWM_PCLK PCLK_TCPWM1_CLOCKS65
#define SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER (3u)
#define SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ (2000000u)
#define SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ (100u)
#define SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS \
(SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ / \
SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ)
/* Three open-loop power levels. Tune only after thermal validation. */
#define SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT (30u)
#define SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT (60u)
#define SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT (90u)
#define SEAT_BACKREST_HEATER_DEFAULT_LEVEL (0u)
#define SEAT_BACKREST_HEATER_TASK_PERIOD_US (1000u)
/*
* Do not sample at the 10 ms PWM period. Equal periods phase-lock the ADC
* to one PWM point and can report zero current continuously for a healthy
* low-duty heater. Seven milliseconds walks through every PWM phase while
* remaining slow enough for the existing SAR ADC service.
*/
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (7u)
#define SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS (3u)
#define SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS (600000u)
/*
* Vehicle CAN4 wiring proven by D:\wcl\can and Seat_Fan_demo:
* P3.7 RX, P3.6 TX, P3.1 high enables the board TJA1044 circuit.
*/
#define SEAT_BACKREST_HEATER_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_BACKREST_HEATER_CAN_RX_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_RX_PIN (7u)
#define SEAT_BACKREST_HEATER_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_BACKREST_HEATER_CAN_TX_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_TX_PIN (6u)
#define SEAT_BACKREST_HEATER_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_BACKREST_HEATER_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_BACKREST_HEATER_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_BACKREST_HEATER_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER (0u)
#define SEAT_BACKREST_HEATER_CAN_CLOCK_HZ (40000000u)
#define SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT (4u)
#define SEAT_BACKREST_HEATER_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH (8u)
/* Classic CAN matrix messages, 11-bit IDs, 8-byte payloads, Intel bit order. */
#define SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID (0x29Eu)
#define SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID (0x2AAu)
#define SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID (0x3CEu)
#define SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID (0x42Au)
#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID (0x4FAu)
#define SEAT_BACKREST_HEATER_CAN_DLC (8u)
#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_PERIOD_MS (100u)
#define SEAT_BACKREST_HEATER_CAN_ACCEPT_42A_COMMAND (1u)
#define SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR (2u)
/* Safety watchdog and retained C-SPY session latch, derived from Seat_Fan_demo. */
#define SEAT_BACKREST_HEATER_WDT_TIMEOUT_TICKS (3200u)
#define SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX (3u)
#define SEAT_BACKREST_HEATER_DEBUG_ACTIVE_MAGIC (0x48444247u) /* "HDBG" */
#define SEAT_BACKREST_HEATER_DEBUG_EXIT_MAGIC (0x484F4646u) /* "HOFF" */
/*
* Schematic-confirmed diagnostic current-sense path:
* KBJRD-AD -> P12.2 / ADC[1]_6. The BTS7010 IS pin uses a 1.2 kOhm
* sense resistor and the schematic annotates K=5000. The calculated current
* is for bench visibility only; production thresholds require calibration.
*/
#define SEAT_BACKREST_HEATER_SENSE_ENABLE (1u)
#define SEAT_BACKREST_HEATER_SENSE_PORT GPIO_PRT12
#define SEAT_BACKREST_HEATER_SENSE_PIN (2u)
#define SEAT_BACKREST_HEATER_SENSE_GPIO_HSIOM P12_2_GPIO
#define SEAT_BACKREST_HEATER_SENSE_RESISTOR_OHM (1200u)
#define SEAT_BACKREST_HEATER_SENSE_KILIS (5000u)
/* BTS7010 control facts confirmed on the power-driver schematic page. */
#define SEAT_BACKREST_HEATER_BTS7010_IN_ACTIVE_HIGH (1u)
#define SEAT_BACKREST_HEATER_BTS7010_DEN_MCU_CONTROLLED (0u)
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat_Controller_demo requires CYT4BFBCJE rev_d headers."
#endif
#if ((SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ % \
SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ) != 0u)
#error "The heater counter clock must divide evenly by the PWM frequency."
#endif
#if ((SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS == 0u) || \
(SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS > 65535u))
#error "The heater PWM period does not fit TCPWM1 group 0 counter 65."
#endif
#if ((SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT == 0u) || \
(SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT >= \
SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT) || \
(SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT >= \
SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT) || \
(SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT > 100u))
#error "Heater duties must be ordered LOW < MEDIUM < HIGH <= 100."
#endif
#if (SEAT_BACKREST_HEATER_DEFAULT_LEVEL > 3u)
#error "Default heater level must be 0 (off) through 3 (high)."
#endif
#if ((SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID > 0x7FFu))
#error "Seat heating CAN IDs must be 11-bit standard identifiers."
#endif
#if (SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH < 5u)
#error "The heater CAN receive queue must hold at least five frames."
#endif
#if ((SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR == 0u) || \
(SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR > 3u))
#error "Default heater CAN gear must be in the range 1 through 3."
#endif
#if ((SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS == 0u) || \
(SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS == 0u) || \
(SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS == 0u))
#error "Heater sample period and maximum active time must be non-zero."
#endif
#if (SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX >= SRSS_BACKUP_NUM_BREG)
#error "The selected heater backup register is not present on this device."
#endif
#endif /* SEAT_BACKREST_HEATER_CONFIG_H */
@@ -0,0 +1,71 @@
#ifndef SEAT_CURRENT_ADC_CONFIG_H
#define SEAT_CURRENT_ADC_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* PASS0 SAR1 is shared by all seat-current measurements. The schematic
* connects VREFH and the DRV8718 AREF rail to filtered 5 V, so raw-to-voltage
* calculations must use 5000 mV rather than the GPIO-domain 3.3 V supply.
*
* Logical motor mapping:
* M0 horizontal : ZYTTKZDY-AD, P10.6, ADC[1]_2
* M1 seat pan : JXBKZDY-AD, P12.1, ADC[1]_5
* M2 backrest : ZYKBKZDY-AD, P10.7, ADC[1]_3
* M3 legrest : ZYGDKZDY-AD, P10.5, ADC[1]_1
* heater current : KBJRD-AD, P12.2, ADC[1]_6
* fan current : FSDY-AD, P12.6, ADC[1]_10
*/
#define SEAT_CURRENT_ADC_INSTANCE PASS0_SAR1
#define SEAT_CURRENT_ADC_PCLK PCLK_PASS0_CLOCK_SAR1
#define SEAT_CURRENT_ADC_DIVIDER_NUMBER (4u)
#define SEAT_CURRENT_ADC_REFERENCE_MV (5000u)
#define SEAT_CURRENT_ADC_FULL_SCALE (4095u)
#define SEAT_CURRENT_ADC_OPERATION_FREQ_MAX_HZ (13340000u)
#define SEAT_CURRENT_ADC_SAMPLE_TIME_MIN_NS (600ull)
#define SEAT_CURRENT_ADC_POLL_TIMEOUT (100000u)
#define SEAT_CURRENT_M0_PORT GPIO_PRT10
#define SEAT_CURRENT_M0_PIN (6u)
#define SEAT_CURRENT_M0_GPIO_HSIOM P10_6_GPIO
#define SEAT_CURRENT_M0_CHANNEL PASS0_SAR1_CH2
#define SEAT_CURRENT_M0_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN2
#define SEAT_CURRENT_M1_PORT GPIO_PRT12
#define SEAT_CURRENT_M1_PIN (1u)
#define SEAT_CURRENT_M1_GPIO_HSIOM P12_1_GPIO
#define SEAT_CURRENT_M1_CHANNEL PASS0_SAR1_CH5
#define SEAT_CURRENT_M1_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN5
#define SEAT_CURRENT_M2_PORT GPIO_PRT10
#define SEAT_CURRENT_M2_PIN (7u)
#define SEAT_CURRENT_M2_GPIO_HSIOM P10_7_GPIO
#define SEAT_CURRENT_M2_CHANNEL PASS0_SAR1_CH3
#define SEAT_CURRENT_M2_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN3
#define SEAT_CURRENT_M3_PORT GPIO_PRT10
#define SEAT_CURRENT_M3_PIN (5u)
#define SEAT_CURRENT_M3_GPIO_HSIOM P10_5_GPIO
#define SEAT_CURRENT_M3_CHANNEL PASS0_SAR1_CH1
#define SEAT_CURRENT_M3_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN1
#define SEAT_CURRENT_HEATER_PORT GPIO_PRT12
#define SEAT_CURRENT_HEATER_PIN (2u)
#define SEAT_CURRENT_HEATER_GPIO_HSIOM P12_2_GPIO
#define SEAT_CURRENT_HEATER_CHANNEL PASS0_SAR1_CH6
#define SEAT_CURRENT_HEATER_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN6
#define SEAT_CURRENT_FAN_PORT GPIO_PRT12
#define SEAT_CURRENT_FAN_PIN (6u)
#define SEAT_CURRENT_FAN_GPIO_HSIOM P12_6_GPIO
#define SEAT_CURRENT_FAN_CHANNEL PASS0_SAR1_CH10
#define SEAT_CURRENT_FAN_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN10
#define SEAT_CURRENT_ADC_PORT_ADDRESS CY_ADC_PORT_ADDRESS_SARMUX0
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat current ADC mapping requires CYT4BFBCJE rev_d headers."
#endif
#endif /* SEAT_CURRENT_ADC_CONFIG_H */
@@ -0,0 +1,139 @@
#ifndef SEAT_FAN_CONFIG_H
#define SEAT_FAN_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* Backrest fan:
* P11.1 -> KBFSDY-PWM-IN -> BTS7030 IN0 -> backrest fan.
* The schematic name "PWM0-60" maps to TCPWM1 group 0 counter 60 in SDL.
*/
#define SEAT_FAN_PWM_PORT GPIO_PRT11
#define SEAT_FAN_PWM_PIN (1u)
#define SEAT_FAN_PWM_GPIO_HSIOM P11_1_GPIO
#define SEAT_FAN_PWM_HSIOM P11_1_TCPWM1_LINE60
#define SEAT_FAN_PWM_TCPWM TCPWM1_GRP0_CNT60
#define SEAT_FAN_PWM_PCLK PCLK_TCPWM1_CLOCKS60
/*
* Seat-cushion fan output is unused in this first version and must stay low:
* P11.2 -> ZDFSDY-PWM-IN.
*/
#define SEAT_FAN_UNUSED_PORT GPIO_PRT11
#define SEAT_FAN_UNUSED_PIN (2u)
#define SEAT_FAN_UNUSED_GPIO_HSIOM P11_2_GPIO
#define SEAT_FAN_PWM_DIVIDER_NUMBER (0u)
#define SEAT_FAN_COUNTER_CLOCK_HZ (2000000u)
#define SEAT_FAN_PWM_FREQUENCY_HZ (100u)
#define SEAT_FAN_PWM_PERIOD_TICKS \
(SEAT_FAN_COUNTER_CLOCK_HZ / SEAT_FAN_PWM_FREQUENCY_HZ)
#define SEAT_FAN_STARTUP_DUTY_PERCENT (100u)
#define SEAT_FAN_RUNNING_DUTY_PERCENT (70u)
#define SEAT_FAN_STARTUP_TIME_US (2000000u)
#define SEAT_FAN_DEBUG_POLL_TIME_US (1000u)
#define SEAT_FAN_WDT_TIMEOUT_TICKS (3200u)
/* CAN-controlled application timing and the three physical fan levels. */
#define SEAT_FAN_TASK_PERIOD_US (1000u)
#define SEAT_FAN_STARTUP_TIME_MS (SEAT_FAN_STARTUP_TIME_US / 1000u)
#define SEAT_FAN_GEAR1_DUTY_PERCENT (50u)
#define SEAT_FAN_GEAR2_DUTY_PERCENT (70u)
#define SEAT_FAN_GEAR3_DUTY_PERCENT (100u)
#define SEAT_FAN_DEFAULT_GEAR (2u)
/*
* Vehicle CAN4 wiring proven by D:\wcl\can:
* P3.7 RX, P3.6 TX, P3.1 high enables the board TJA1044 circuit.
*/
#define SEAT_FAN_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_FAN_CAN_RX_PORT GPIO_PRT3
#define SEAT_FAN_CAN_RX_PIN (7u)
#define SEAT_FAN_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_FAN_CAN_TX_PORT GPIO_PRT3
#define SEAT_FAN_CAN_TX_PIN (6u)
#define SEAT_FAN_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_FAN_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_FAN_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_FAN_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_FAN_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_FAN_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_FAN_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_FAN_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_FAN_CAN_DIVIDER_NUMBER (0u)
#define SEAT_FAN_CAN_CLOCK_HZ (40000000u)
#define SEAT_FAN_CAN_TX_BUFFER_COUNT (4u)
#define SEAT_FAN_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_FAN_CAN_RX_QUEUE_DEPTH (8u)
/* Classic CAN matrix messages, 11-bit IDs, 8-byte payloads, Intel bit order. */
#define SEAT_FAN_CAN_CONTROL_29E_ID (0x29Eu)
#define SEAT_FAN_CAN_CONFIG_2AA_ID (0x2AAu)
#define SEAT_FAN_CAN_SWITCH_42A_ID (0x42Au)
#define SEAT_FAN_CAN_FEEDBACK_4FA_ID (0x4FAu)
#define SEAT_FAN_CAN_DLC (8u)
#define SEAT_FAN_CAN_FEEDBACK_PERIOD_MS (100u)
#define SEAT_FAN_CAN_ACCEPT_42A_COMMAND (1u)
/* FSDY-AD is P12.6 / ADC[1]_10 on the shared seat-current ADC. */
#define SEAT_FAN_CURRENT_SAMPLE_MS (10u)
/*
* BREG3 retains the debug-session state through debug/XRES/WDT resets.
* DEBUG_ACTIVE is written by firmware as soon as it observes C_DEBUGEN, so a
* debugger-induced reset cannot accidentally restart the fan even if the
* C-SPY exit hook was skipped. DEBUG_EXIT is written when detach is observed
* or by the C-SPY exit hook. A genuine POR releases either marker.
*/
#define SEAT_FAN_DEBUG_SESSION_BREG_INDEX (3u)
#define SEAT_FAN_DEBUG_ACTIVE_MAGIC (0x44424741u) /* ASCII "DBGA" */
#define SEAT_FAN_DEBUG_EXIT_MAGIC (0x5346414Eu) /* ASCII "SFAN" */
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat_Controller_demo must be built for CYT4BFBCJE with the rev_d headers."
#endif
#if ((SEAT_FAN_COUNTER_CLOCK_HZ % SEAT_FAN_PWM_FREQUENCY_HZ) != 0u)
#error "The TCPWM counter clock must divide evenly by the requested PWM frequency."
#endif
#if ((SEAT_FAN_PWM_PERIOD_TICKS == 0u) || (SEAT_FAN_PWM_PERIOD_TICKS > 65535u))
#error "The PWM period does not fit TCPWM1 group 0 counter 60."
#endif
#if ((SEAT_FAN_STARTUP_TIME_US % SEAT_FAN_DEBUG_POLL_TIME_US) != 0u)
#error "The startup duration must be an integer number of debug polling periods."
#endif
#if ((SEAT_FAN_STARTUP_TIME_US % 1000u) != 0u)
#error "The fan startup time must be an integer number of milliseconds."
#endif
#if ((SEAT_FAN_GEAR1_DUTY_PERCENT > SEAT_FAN_GEAR2_DUTY_PERCENT) || \
(SEAT_FAN_GEAR2_DUTY_PERCENT > SEAT_FAN_GEAR3_DUTY_PERCENT) || \
(SEAT_FAN_GEAR3_DUTY_PERCENT > 100u))
#error "Fan gear duty percentages must be monotonic and no greater than 100."
#endif
#if ((SEAT_FAN_DEFAULT_GEAR == 0u) || (SEAT_FAN_DEFAULT_GEAR > 3u))
#error "The default physical fan gear must be in the range 1 through 3."
#endif
#if ((SEAT_FAN_CAN_CONTROL_29E_ID > 0x7FFu) || \
(SEAT_FAN_CAN_CONFIG_2AA_ID > 0x7FFu) || \
(SEAT_FAN_CAN_SWITCH_42A_ID > 0x7FFu) || \
(SEAT_FAN_CAN_FEEDBACK_4FA_ID > 0x7FFu))
#error "Seat ventilation CAN IDs must be 11-bit standard identifiers."
#endif
#if (SEAT_FAN_CAN_RX_QUEUE_DEPTH < 4u)
#error "The CAN receive queue must hold at least four frames."
#endif
#if (SEAT_FAN_DEBUG_SESSION_BREG_INDEX >= SRSS_BACKUP_NUM_BREG)
#error "The selected backup register is not present on this device."
#endif
#endif /* SEAT_FAN_CONFIG_H */
@@ -0,0 +1,150 @@
#ifndef SEAT_MOVE_CONFIG_H
#define SEAT_MOVE_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/* This configuration is intentionally tied to the timed-only IAR target. */
#if !defined(PASSENGER_SEAT_TIMED_ONLY)
#error "Build this source only from Passenger_Seat_Timed_Controller_demo."
#endif
#define SEAT_MOVE_MOTOR_COUNT (4u)
#define SEAT_MOVE_ACTIVE_MOTOR_MASK (0x0Fu)
#define SEAT_MOVE_TASK_PERIOD_US (1000u)
#define SEAT_MOVE_START_STAGGER_MS (100u)
#define SEAT_MOVE_MAX_CONCURRENT_MOTORS (4u)
/* Timed-only CAN limits. ParamA is duration in ms. */
#define SEAT_TIMED_MIN_DURATION_MS (300u)
#define SEAT_TIMED_M0_MAX_DURATION_MS (30000u) /* horizontal */
#define SEAT_TIMED_M1_MAX_DURATION_MS (20000u) /* seat-pan */
#define SEAT_TIMED_M2_MAX_DURATION_MS (30000u) /* backrest */
#define SEAT_TIMED_M3_MAX_DURATION_MS (25000u) /* legrest */
#define SEAT_TIMED_LEVEL_MIN_X10 (300u)
#define SEAT_TIMED_LEVEL_MAX_X10 (1000u)
/* Logical MotorID and connected DRV8718 channel are identical. */
#define SEAT_MOVE_LOGICAL_M0_PHYSICAL_MODE (0u)
#define SEAT_MOVE_LOGICAL_M1_PHYSICAL_MODE (1u)
#define SEAT_MOVE_LOGICAL_M2_PHYSICAL_MODE (2u)
#define SEAT_MOVE_LOGICAL_M3_PHYSICAL_MODE (3u)
/* Retained by the verified motor manager; timed requests provide duty. */
#define SEAT_MOVE_M0_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M1_DEFAULT_TIMEOUT_MS (8000u)
#define SEAT_MOVE_M2_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (30000u)
#define SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT (50u)
#define SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT (50u)
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (4u)
/* CAN4, classic CAN, 500 kbit/s. */
#define SEAT_MOVE_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_MOVE_CAN_RX_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_RX_PIN (7u)
#define SEAT_MOVE_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_MOVE_CAN_TX_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_TX_PIN (6u)
#define SEAT_MOVE_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_MOVE_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_MOVE_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_MOVE_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_MOVE_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_MOVE_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_MOVE_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_MOVE_CAN_DIVIDER_NUMBER (0u)
#define SEAT_MOVE_CAN_CLOCK_HZ (40000000u)
#define SEAT_MOVE_CAN_TX_BUFFER_COUNT (8u)
#define SEAT_MOVE_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_MOVE_CAN_RX_QUEUE_DEPTH (16u)
#define SEAT_MOVE_CAN_DLC (8u)
#define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
#define SEAT_MOVE_CAN_MOTOR_STATUS_ID (0x302u)
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_FIRMWARE_ID_ID (0x307u)
#define SEAT_MOVE_FIRMWARE_REVISION (15u)
#define SEAT_MOVE_FIRMWARE_BUILD_ID (17u)
#define SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS (100u)
#define SEAT_MOVE_MOTOR_STATUS_POLL_MS (25u)
#define SEAT_MOVE_DIAGNOSTIC_POLL_MS (25u)
#define SEAT_MOVE_FIRMWARE_ID_PERIOD_MS (500u)
/* Current-aware stall detection remains active for timed movement. */
#define SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS (10u)
#define SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS (100u)
#define SEAT_MOVE_MOTOR_CURRENT_BASELINE_FILTER_SHIFT (3u)
#define SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS (200u)
#define SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS (5000u)
#define SEAT_MOVE_M0_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M1_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M2_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M3_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_MOTOR_CURRENT_MAX_ERRORS (3u)
/* Debug-detach and watchdog safety. HIL registers remain internal to the
* reused safety manager; this image exposes no HIL CAN command/filter. */
#define SEAT_MOVE_DEBUG_SESSION_BREG_INDEX (3u)
#define SEAT_MOVE_DEBUG_ACTIVE_MAGIC (0x4D4F5641u)
#define SEAT_MOVE_DEBUG_EXIT_MAGIC (0x4D4F5645u)
#define SEAT_MOVE_WDT_TIMEOUT_TICKS (3200u)
#define SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX (0u)
#define SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX (1u)
#define SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX (2u)
#define SEAT_MOVE_HIL_WDT_MARKER (0x48494C36u)
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Timed passenger-seat firmware requires CYT4BFBCJE rev_d."
#endif
#if (SEAT_MOVE_ACTIVE_MOTOR_MASK != 0x0Fu)
#error "All four passenger-seat motor outputs must be enabled."
#endif
#if (SEAT_MOVE_MAX_CONCURRENT_MOTORS == 0u) || \
(SEAT_MOVE_MAX_CONCURRENT_MOTORS > SEAT_MOVE_MOTOR_COUNT)
#error "Concurrent motor budget must be between one and four."
#endif
#if ((SEAT_TIMED_MIN_DURATION_MS == 0u) || \
(SEAT_TIMED_M0_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M1_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M2_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M3_MAX_DURATION_MS > 65535u))
#error "Timed-command duration limits must fit ParamA."
#endif
#if ((SEAT_MOVE_CAN_COMMAND_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_SYSTEM_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_FIRMWARE_ID_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif
#if (SEAT_MOVE_FIRMWARE_REVISION > 0x0Fu) || \
(SEAT_MOVE_FIRMWARE_BUILD_ID > 0xFFu)
#error "Firmware identity fields exceed their CAN encoding."
#endif
#if ((SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS == 0u) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS < \
SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS) || \
(SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS == 0u) || \
(SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS <= \
SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_ERRORS == 0u))
#error "Motor current/stall timing configuration is invalid."
#endif
#endif /* SEAT_MOVE_CONFIG_H */
@@ -0,0 +1,25 @@
# Build information — 2026-09-16
- Project: `Passenger_Seat_Timed_Controller_demo.ewp`
- Configuration: `rev_d`
- IAR command-line builder: 9.2.2.10770
- IAR ARM compiler/linker: 9.40.1.364
- Result: 0 errors, 0 warnings
- Read-only code: 40,626 bytes
- Read-only data: 1,977 bytes
- Read-write data: 6,182 bytes
Artifacts:
| File | SHA-256 |
|---|---|
| `IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.hex` | `7B47002B7B1A882A90717CE2727D60349B6C58CF0C1E95E48094C85EDB376CDC` |
| `IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.out` | `B0C740CB2BE02008BC03803B0A3422D817ABD1CF2BF977156294FB04BBDFAB7F` |
The linker map contains `SeatMotor_MoveTimedDuty` as the sole linked
`SeatMotor_Move*`/calibration/homing entry point. No dynamic-mode, position
store, or thermal-controller application symbol is linked. The HEX contains
the `PST1`, revision 15, build 17 identity payload.
This result is a compile/link verification only. No target was flashed and no
physical actuator movement was performed during this build.
@@ -0,0 +1,88 @@
# 副驾座椅四轴定时控制固件
本目录是从 `Seat_Controller_demo` 独立出的副驾定时控制版本。原工程保持不变;本工程的 CAN 应用层只允许四轴定时运动,不提供位置、相对位移、回零、标定、动态模式或热控能力。
## IAR 工程
- 工程文件:`IAR/Passenger_Seat_Timed_Controller_demo.ewp`
- 目标器件:Infineon TRAVEO T2G `CYT4BFBCJE`,`rev_d`
- IAR 工程宏:`PASSENGER_SEAT_TIMED_ONLY=1`
- HEX 输出:`Passenger_Seat_Timed_Controller_demo.hex`
- 固件身份帧:CAN `0x307`,字节 0..3 为 ASCII `PST1`,Build ID 为 `17`
工程复用原固件已验证的 DRV8718、Hall、电流检测、堵转/端点保护、看门狗和调试器退出安全逻辑。加热和风扇应用没有链接进固件;其 BSP 只用于启动及异常路径中强制输出关闭。
## 四轴能力
| MotorID | 机构 | 最短时间 | 最长时间 | 实际占空比限制 |
|---:|---|---:|---:|---:|
| 0 | 水平前后 | 300 ms | 30000 ms | 50%..95% |
| 1 | 坐垫升降 | 300 ms | 20000 ms | 50%..95% |
| 2 | 靠背角度 | 300 ms | 30000 ms | 50%..95% |
| 3 | 腿托升降 | 300 ms | 25000 ms | 固定上限 50% |
四轴可并发,启动间隔至少 100 ms。同一轴收到更新的定时命令时,先停止该轴,再执行新命令;不影响其他轴。定时运动不依赖绝对位置有效性,但端点、堵转、电流和驱动故障保护始终生效。
## CAN 命令
CAN4 使用经典 CAN、标准 11 位 ID、500 kbit/s。接收过滤器只接受 `0x300`,DLC 必须为 8。
`0x300` 的 8 字节按小端编码:
| 字节 | 字段 |
|---|---|
| B0 | Opcode |
| B1 | Control:bit1:0 MotorID,bit3:2 Direction,bit5:4 ProtocolVersion=1,bit7:6=0 |
| B3:B2 | Sequence,uint16 |
| B5:B4 | ParamA,uint16 |
| B7:B6 | ParamB,uint16 |
支持的 Opcode:
| Opcode | 功能 | Direction | ParamA | ParamB |
|---:|---|---|---|---|
| `0x03` | 定时运动 | `1` 向远端,`2` 向近端 | 时间,ms | 请求占空比 ×10,范围 300..1000 |
| `0x08` | 停止单轴 | `0` | `0` | `0` |
| `0x09` | 停止全部 | `0`,MotorID=`0` | `0` | `0` |
| `0x0C` | 查询单轴状态 | `0` | `0` | `0` |
| `0x0D` | 清除单轴任务故障 | `0` | `0xC33C` | `0` |
请求占空比会被安全范围约束:M0..M2 最终为 50%..95%,M3 不超过 50%。`STOP_ALL` 不受序列号新旧限制,可用于随时重新同步和急停;其他命令要求 16 位 Sequence 按环形序列递增,完全相同的重复帧返回 Duplicate。
以下运动 Opcode 固定返回 `BAD_OPCODE(3)`:
- `0x01` 绝对位置
- `0x02` 相对位置
- `0x0A` 行程标定
- `0x0B` 已知行程回零
- `0x0E` 启动动态模式
- `0x0F` 停止动态模式
热控和 HIL 自愈帧不在硬件接收过滤器中,`0x306` 动态模式状态也不再发送。
## 状态反馈
保留与原座椅协议兼容的诊断帧:
- `0x301`:系统状态、ACK、拒绝原因、活动/故障轴掩码
- `0x302`:单轴任务状态;定时运行状态值为 `3`
- `0x303`:Hall、速度和故障诊断
- `0x304`:电流诊断
- `0x307`:固件身份 `PST1`
`0x301` 中动态模式位和标定位始终为 0。位置有效位及百分比只作为 Hall 诊断信息返回,不用于目标位置控制。
## 构建与校验
1. 用 IAR Embedded Workbench for Arm 打开 `IAR/Passenger_Seat_Timed_Controller_demo.ewp`。
2. 选择 `rev_d` 配置并 Build。
3. 烧录前通过 `0x307` 确认目标固件身份为 `PST1`。
4. 烧录和台架运动测试期间保证座椅机构无人员/障碍物,先验证 `STOP_ALL`,再逐轴低时长测试。
仓库静态校验:
```text
python firmware/t2g/Passenger_Seat_Timed_Controller_demo/Tests/audit_timed_only.py
```
该脚本检查 IAR XML、工程引用、定时版编译宏、CAN 接收过滤器、允许的 Opcode,以及位置/动态/热控模块未被链接。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H
#include <stdbool.h>
#include <stdint.h>
#define MOTOR_API_SUCCESS (0u)
#define MOTOR_API_FAILURE (1u)
#define MOTOR_PHYSICAL_VALUE_SCALE (100ul)
/*
* 2026-07-30 endpoint-to-endpoint range calibration. Bench verification
* established identity mapping: logical M0..M3 use physical mode 0..3.
*/
#define MOTOR_MODE0_DEFAULT_MAX_HALL_COUNT (480ul) /* horizontal */
#define MOTOR_MODE0_MAX_POSITION_MM_X100 (21500ul)
#define MOTOR_MODE1_DEFAULT_MAX_HALL_COUNT (374ul) /* seat pan */
#define MOTOR_MODE1_MAX_VERTICAL_MM_X100 (5600ul)
#define MOTOR_MODE1_MAX_HORIZONTAL_MM_X100 (5100ul)
/* Compatibility name used by the generic scalar position API. */
#define MOTOR_MODE1_MAX_ANGLE_DEG_X100 \
MOTOR_MODE1_MAX_VERTICAL_MM_X100
#define MOTOR_MODE2_DEFAULT_MAX_HALL_COUNT (917ul) /* backrest */
#define MOTOR_MODE2_MAX_ANGLE_DEG_X100 (6800ul)
#define MOTOR_MODE3_DEFAULT_MAX_HALL_COUNT (647ul) /* legrest */
#define MOTOR_MODE3_MAX_ANGLE_DEG_X100 (7200ul)
/* Compatibility name used by the generic scalar position API. */
#define MOTOR_MODE3_MAX_POSITION_MM_X100 \
MOTOR_MODE3_MAX_ANGLE_DEG_X100
#define MOTOR_SPEED_VALUE_SCALE (100ul)
#define MOTOR_SPEED_MIN_DUTY_PERCENT (50u)
#define MOTOR_SPEED_MAX_DUTY_PERCENT (95u)
#define MOTOR_SPEED_DUTY_STEP_PERCENT (5u)
#define MOTOR_MODE0_MAX_SPEED_PPS_X100 (8600ul)
#define MOTOR_MODE1_MAX_SPEED_PPS_X100 (6100ul)
#define MOTOR_MODE2_MAX_SPEED_PPS_X100 (9000ul)
#define MOTOR_MODE3_MAX_SPEED_PPS_X100 (8600ul)
typedef enum
{
MOTOR_MODE_0_LINEAR = 0,
MOTOR_MODE_1_ANGLE,
MOTOR_MODE_2_ANGLE,
MOTOR_MODE_3_LINEAR_TRACK,
MOTOR_MODE_COUNT
} motor_mode_t;
typedef enum
{
MOTOR_DIRECTION_STOPPED = 0,
MOTOR_DIRECTION_TOWARD_FAR,
MOTOR_DIRECTION_TOWARD_NEAR
} motor_direction_t;
typedef enum
{
MOTOR_EVENT_NONE = 0,
MOTOR_EVENT_TARGET_REACHED,
MOTOR_EVENT_NEAR_ENDPOINT,
MOTOR_EVENT_FAR_ENDPOINT,
MOTOR_EVENT_STALL,
MOTOR_EVENT_TIMEOUT,
MOTOR_EVENT_ABORTED,
MOTOR_EVENT_DRIVER_FAULT,
MOTOR_EVENT_CALIBRATION_FAILED
} motor_event_t;
typedef enum
{
MOTOR_CALIBRATE_FIND_NEAR = 0,
MOTOR_CALIBRATE_FIND_FAR,
MOTOR_CALIBRATE_FIND_BOTH
} motor_calibration_action_t;
typedef struct
{
motor_mode_t mode;
bool positionValid;
motor_direction_t direction;
motor_event_t lastEvent;
uint32_t hallCount;
uint32_t maxHallCount;
uint32_t valueX100;
uint32_t maxValueX100;
uint32_t positionMmX100;
uint32_t horizontalMmX100;
uint32_t verticalMmX100;
uint32_t angleDegX100;
uint32_t percentX100;
} motor_position_info_t;
typedef struct
{
bool reverseAtEndpoint;
bool reverseAtStall;
} motor_safety_options_t;
typedef struct
{
bool nearEndpointFound;
bool farEndpointFound;
uint32_t measuredMaxHallCount;
} motor_calibration_result_t;
typedef struct
{
motor_mode_t mode;
bool enabled;
uint32_t durationMs;
uint32_t travelValueX100;
uint32_t plannedHallPulses;
uint32_t targetPulsesPerSecondX100;
uint32_t actualPulsesPerSecondX100;
uint8_t initialDutyPercent;
uint8_t currentDutyPercent;
} motor_speed_info_t;
typedef struct
{
motor_mode_t mode;
bool sampleValid;
bool baselineValid;
bool highCurrent;
bool hallActivityLow;
bool calibrating;
uint16_t raw;
uint16_t baselineRaw;
uint16_t deltaRaw;
uint16_t stallThresholdRaw;
uint32_t sampleAgeMs;
} motor_current_info_t;
typedef struct
{
uint32_t positionHallCount;
uint32_t maxHallCount;
uint32_t maxSpeedPulsesPerSecondX100;
uint32_t rawRisingEdges;
bool positionValid;
} motor_persistent_state_t;
/* Hardware initialization. SystemInit() and global IRQ enable remain owned by main. */
uint8_t Motor_Init(void);
/* Immediate GPIO-only safe state, also valid before Motor_Init(). */
void Motor_ForceSafeOff(void);
/* The Hall timebase is a precise 2 ms hardware interrupt. */
uint32_t Motor_GetTimeMs(void);
void Motor_SetWatchdogServiceEnabled(bool enabled);
/*
* Physical values use fixed point x100: 215.00 mm is 21500 and
* 68.00 degrees is 6800. positionValid is restored from a verified clean
* record or becomes true after a known endpoint has been found.
*
* Ownership contract: call all control APIs and Motor_ServiceAll from one
* main-loop/owner context. They are not ISR-safe, thread-safe, or reentrant.
* The four motors may still run concurrently after sequential non-blocking
* start calls from that owner.
*/
uint8_t Motor_GetPositionInfo(motor_mode_t mode, motor_position_info_t *info);
uint8_t Motor_GetCurrentInfo(motor_mode_t mode, motor_current_info_t *info);
/* Each mode has independent endpoint and stall reverse switches. */
uint8_t Motor_SetSafetyOptions(motor_mode_t mode,
const motor_safety_options_t *options);
/* Open-loop primitives used by the standardized application controller. */
uint8_t Motor_SetFixedDutyPercent(motor_mode_t mode, uint8_t dutyPercent);
uint8_t Motor_StartOpenLoop(motor_mode_t mode, motor_direction_t direction);
bool Motor_IsMoving(motor_mode_t mode);
/* Complete a commanded stop plus bounded Hall-coast tracking before a
* safety decision that requires the mechanism to be physically settled. */
bool Motor_WaitUntilStopped(motor_mode_t mode);
/*
* Restore/store Hall-domain state. These calls are accepted only while the
* selected motor is stopped. max speed uses Hall rising edges/s x100.
*/
uint8_t Motor_ValidatePersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_ApplyPersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_GetPersistentState(motor_mode_t mode,
motor_persistent_state_t *state);
/* Bench HIL support: invalidate only the stopped axis' absolute-position
* confidence while preserving its measured Hall count and confirmed range.
* Production command handling never calls this API. */
uint8_t Motor_InvalidatePosition(motor_mode_t mode);
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100);
/*
* Pair an owner-context command with cancellation published asynchronously.
* An acknowledge succeeds when no cancellation is pending, or when sequence
* exactly matches it. It fails if a newer cancellation has already arrived.
*/
uint8_t Motor_AcknowledgeCommand(motor_mode_t mode, uint16_t sequence);
bool Motor_IsCommandCurrent(motor_mode_t mode, uint16_t sequence);
/*
* Start is non-blocking and permits all four modes to move concurrently.
* Wait and MoveToValue return 0 only after the target is reached; endpoint,
* stall, invalid position, invalid target, and timeout return 1.
* Replacing a target still ahead in the same direction does not restart PWM
* or refresh safety/timeout clocks. A direction change waits at least 100 ms
* and until Hall has been quiet for 100 ms. Crossing a target stops PWM once;
* residual Hall pulses remain in the global position and may leave the final
* count beyond the target. No automatic reverse correction is performed.
* If a safety reverse is enabled, Wait remains blocked until that reverse has
* stopped at its next safety event or at the absolute operation timeout.
*/
uint8_t Motor_StartMoveToValue(motor_mode_t mode, uint32_t targetValueX100);
uint8_t Motor_WaitForMove(motor_mode_t mode);
uint8_t Motor_MoveToValue(motor_mode_t mode, uint32_t targetValueX100);
uint8_t Motor_StopMode(motor_mode_t mode);
/*
* Configure closed-loop speed from a planned travel amount and duration.
* durationMs is milliseconds. travelValueX100 is mm x100 for mode 0 and
* degrees x100 for modes 1/2, and mm x100 for mode 3. This configures speed
* only; use an existing move API to start motion. After each real PWM start
* the controller waits
* 500 ms, then evaluates a 500 ms rolling Hall window every 100 ms. A speed
* error outside +/-10 percent changes that motor's duty by one 5 percent step.
*/
uint8_t Motor_SetSpeedPlan(motor_mode_t mode,
uint32_t durationMs,
uint32_t travelValueX100);
uint8_t Motor_DisableSpeedControl(motor_mode_t mode);
uint8_t Motor_GetSpeedInfo(motor_mode_t mode, motor_speed_info_t *info);
/*
* Calibration is blocking for the selected mode; other active modes continue.
* A new range is committed only after validation. Failure restores the old
* range, preserves observed motion, and invalidates absolute position because
* physical travel cannot be rolled back. Each leg requires Hall coast
* tracking to settle before/after travel; quiet wait is bounded to five seconds.
* legTimeoutMs is the independent motion timeout applied to each endpoint leg.
*/
uint8_t Motor_Calibrate(motor_mode_t mode,
motor_calibration_action_t action,
uint32_t legTimeoutMs,
motor_calibration_result_t *result);
/* Application override used to keep CAN/status/safety services alive while
* the selected motor executes the blocking endpoint-calibration primitive. */
void Motor_CalibrationServiceHook(void);
/* Call continuously from the main loop; the ISR still kills PWM immediately. */
void Motor_ServiceAll(void);
/* ISR-safe immediate PWM kill; bridge Hi-Z is completed by Motor_ServiceAll. */
void Motor_RequestAbortMaskFromIsr(uint8_t motorMask, uint16_t sequence);
void Motor_RequestAbortAllFromIsr(uint16_t sequence);
/* Irreversible until reset; unlike STOP, no sequence acknowledge can clear it. */
void Motor_LatchSafetyInhibitFromIsr(uint16_t sequence);
void Motor_StopAll(void);
/* A latched driver fault requires a power cycle before motion is accepted. */
bool Motor_IsDriverFaultLatched(void);
uint8_t Motor0_GetPosition(motor_position_info_t *info);
uint8_t Motor1_GetPosition(motor_position_info_t *info);
uint8_t Motor2_GetAngle(motor_position_info_t *info);
uint8_t Motor3_GetAngle(motor_position_info_t *info);
uint8_t Motor0_MoveToPositionMmX100(uint32_t positionMmX100);
uint8_t Motor1_MoveToVerticalMmX100(uint32_t verticalMmX100);
uint8_t Motor2_MoveToAngleDegX100(uint32_t angleDegX100);
uint8_t Motor3_MoveToAngleDegX100(uint32_t angleDegX100);
uint8_t Motor0_SetSpeedPlan(uint32_t durationMs, uint32_t distanceMmX100);
uint8_t Motor1_SetSpeedPlan(uint32_t durationMs, uint32_t verticalMmX100);
uint8_t Motor2_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100);
uint8_t Motor3_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100);
#endif /* MOTOR_CONTROL_H */
@@ -0,0 +1,185 @@
/* C-SPY host-side safety hook for the timed-only passenger-seat controller. */
execUserSetup()
{
/* A new Download and Debug session releases a previous debug-exit latch. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
__message "Passenger_Seat_Timed_Controller_demo: C-SPY setup cleared the debug-session latch.\n";
}
execUserExit()
{
__var value;
__var safe;
/* Retain safe-off across debug/XRES/WDT resets. */
__writeMemory32(0x4D4F5645, 0x4027100C, "Memory");
/* Assert DRVOFF=P8.1 before touching PWM or pin muxing. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Disable TCPWM1 group-0 counters IN1/IN2/IN3/IN4 and Hall timebase. */
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
/* Disable fan TCPWM60 and heater TCPWM65 before changing their muxes. */
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
/* Force P11.1/P11.2 fan paths and P16.7 heater path to GPIO low. */
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
value = __readMemory32(0x403000B0, "Memory");
__writeMemory32(value & 0xFFE0E0FF, 0x403000B0, "Memory");
value = __readMemory32(0x40300104, "Memory");
__writeMemory32(value & 0xE0FFFFFF, 0x40300104, "Memory");
value = __readMemory32(0x403105C4, "Memory");
__writeMemory32((value & 0xFFFFF00F) | 0x00000660,
0x403105C4, "Memory");
value = __readMemory32(0x40310844, "Memory");
__writeMemory32((value & 0x0FFFFFFF) | 0x60000000,
0x40310844, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* Preload IN1=P1.0, IN2/IN3=P5.4/P5.5 and IN4=P8.0 low. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
/* Select GPIO for every DRV8718 PWM input. */
value = __readMemory32(0x40300010, "Memory");
__writeMemory32(value & 0xFFFFFFE0, 0x40300010, "Memory");
value = __readMemory32(0x40300054, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300054, "Memory");
value = __readMemory32(0x40300080, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300080, "Memory");
/* Strong output, input buffer off (CY_GPIO_DM_STRONG_IN_OFF = 0x6). */
value = __readMemory32(0x403100C4, "Memory");
__writeMemory32((value & 0xFFFFFFF0) | 0x00000006,
0x403100C4, "Memory");
value = __readMemory32(0x403102C4, "Memory");
__writeMemory32((value & 0xFF00FFFF) | 0x00660000,
0x403102C4, "Memory");
value = __readMemory32(0x40310444, "Memory");
__writeMemory32((value & 0xFFFFFF00) | 0x00000066,
0x40310444, "Memory");
/* Full drive strength for the same output pins. */
value = __readMemory32(0x403100CC, "Memory");
__writeMemory32(value & 0xFFFCFFFF, 0x403100CC, "Memory");
value = __readMemory32(0x403102CC, "Memory");
__writeMemory32(value & 0xF0FFFFFF, 0x403102CC, "Memory");
value = __readMemory32(0x4031044C, "Memory");
__writeMemory32(value & 0xFFF0FFFF, 0x4031044C, "Memory");
/* Reassert the final physical levels after mux/drive configuration. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Verify the retained latch, every PWM counter, every PWM HSIOM field and
every physical output latch before C-SPY releases the target. */
safe = 1;
value = __readMemory32(0x4027100C, "Memory");
if(value != 0x4D4F5645)
{
safe = 0;
}
value = __readMemory32(0x40580600, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580680, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580700, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580980, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580A00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40581E00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40582080, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40310580, "Memory");
if(value & 0x00000006)
{
safe = 0;
}
value = __readMemory32(0x40310800, "Memory");
if(value & 0x00000080)
{
safe = 0;
}
value = __readMemory32(0x40300010, "Memory");
if(value & 0x0000001F)
{
safe = 0;
}
value = __readMemory32(0x40300054, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40300080, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40310080, "Memory");
if(value & 0x00000001)
{
safe = 0;
}
value = __readMemory32(0x40310280, "Memory");
if(value & 0x00000030)
{
safe = 0;
}
value = __readMemory32(0x40310400, "Memory");
if((value & 0x00000003) != 0x00000002)
{
safe = 0;
}
if(safe == 0)
{
__message "Passenger_Seat_Timed_Controller_demo ERROR: C-SPY safe-off register verification failed.\n";
}
else
{
__message "Passenger_Seat_Timed_Controller_demo: C-SPY exit forced motor, fan and heater outputs low.\n";
}
}
@@ -0,0 +1,213 @@
/* C-SPY program-only safety hook used by the authorized backend flasher.
* It forces every output safe but deliberately clears the debug-session
* marker so --leave_target_running can boot normal firmware afterwards. */
execUserSetup()
{
__var value;
/* Make the pre-download target safe before the flash loader or the new
application can run. The post-session block below repeats and verifies
the same invariants when C-SPY reaches the normal exit hook. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* A new Download and Debug session releases a previous debug-exit latch. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
__message "Passenger_Seat_Timed_Controller_demo: C-SPY setup forced outputs safe and cleared the debug-session latch.\n";
}
execUserExit()
{
__var value;
__var safe;
/* This is a program-only session, not an interactive debugger detach. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
/* Assert DRVOFF=P8.1 before touching PWM or pin muxing. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Disable TCPWM1 group-0 counters IN1/IN2/IN3/IN4 and Hall timebase. */
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
/* Disable fan TCPWM60 and heater TCPWM65 before changing their muxes. */
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
/* Force P11.1/P11.2 fan paths and P16.7 heater path to GPIO low. */
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
value = __readMemory32(0x403000B0, "Memory");
__writeMemory32(value & 0xFFE0E0FF, 0x403000B0, "Memory");
value = __readMemory32(0x40300104, "Memory");
__writeMemory32(value & 0xE0FFFFFF, 0x40300104, "Memory");
value = __readMemory32(0x403105C4, "Memory");
__writeMemory32((value & 0xFFFFF00F) | 0x00000660,
0x403105C4, "Memory");
value = __readMemory32(0x40310844, "Memory");
__writeMemory32((value & 0x0FFFFFFF) | 0x60000000,
0x40310844, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* Preload IN1=P1.0, IN2/IN3=P5.4/P5.5 and IN4=P8.0 low. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
/* Select GPIO for every DRV8718 PWM input. */
value = __readMemory32(0x40300010, "Memory");
__writeMemory32(value & 0xFFFFFFE0, 0x40300010, "Memory");
value = __readMemory32(0x40300054, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300054, "Memory");
value = __readMemory32(0x40300080, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300080, "Memory");
/* Strong output, input buffer off (CY_GPIO_DM_STRONG_IN_OFF = 0x6). */
value = __readMemory32(0x403100C4, "Memory");
__writeMemory32((value & 0xFFFFFFF0) | 0x00000006,
0x403100C4, "Memory");
value = __readMemory32(0x403102C4, "Memory");
__writeMemory32((value & 0xFF00FFFF) | 0x00660000,
0x403102C4, "Memory");
value = __readMemory32(0x40310444, "Memory");
__writeMemory32((value & 0xFFFFFF00) | 0x00000066,
0x40310444, "Memory");
/* Full drive strength for the same output pins. */
value = __readMemory32(0x403100CC, "Memory");
__writeMemory32(value & 0xFFFCFFFF, 0x403100CC, "Memory");
value = __readMemory32(0x403102CC, "Memory");
__writeMemory32(value & 0xF0FFFFFF, 0x403102CC, "Memory");
value = __readMemory32(0x4031044C, "Memory");
__writeMemory32(value & 0xFFF0FFFF, 0x4031044C, "Memory");
/* Reassert the final physical levels after mux/drive configuration. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Verify the retained latch, every PWM counter, every PWM HSIOM field and
every physical output latch before C-SPY releases the target. */
safe = 1;
value = __readMemory32(0x4027100C, "Memory");
if(value != 0x00000000)
{
safe = 0;
}
value = __readMemory32(0x40580600, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580680, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580700, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580980, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580A00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40581E00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40582080, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40310580, "Memory");
if(value & 0x00000006)
{
safe = 0;
}
value = __readMemory32(0x40310800, "Memory");
if(value & 0x00000080)
{
safe = 0;
}
value = __readMemory32(0x40300010, "Memory");
if(value & 0x0000001F)
{
safe = 0;
}
value = __readMemory32(0x40300054, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40300080, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40310080, "Memory");
if(value & 0x00000001)
{
safe = 0;
}
value = __readMemory32(0x40310280, "Memory");
if(value & 0x00000030)
{
safe = 0;
}
value = __readMemory32(0x40310400, "Memory");
if((value & 0x00000003) != 0x00000002)
{
safe = 0;
}
if(safe == 0)
{
__message "Passenger_Seat_Timed_Controller_demo ERROR: C-SPY safe-off register verification failed.\n";
}
else
{
__message "Passenger_Seat_Timed_Controller_demo: program-only exit forced all outputs low and released the debug latch.\n";
}
}
@@ -0,0 +1,109 @@
from __future__ import annotations
import re
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EWP = ROOT / "IAR" / "Passenger_Seat_Timed_Controller_demo.ewp"
APP = ROOT / "App" / "seat_timed_application.c"
CAN_BSP = ROOT / "Bsp" / "bsp_seat_move_can.c"
CONFIG = ROOT / "Config" / "seat_move_config.h"
class TimedOnlyFirmwareAudit(unittest.TestCase):
def test_iar_project_is_valid_xml_and_local_references_exist(self) -> None:
tree = ET.parse(EWP)
project_dir = EWP.parent
missing: list[str] = []
for element in tree.findall(".//file/name"):
value = element.text or ""
if not value.startswith("$PROJ_DIR$"):
continue
relative = value.removeprefix("$PROJ_DIR$\\").replace("\\", "/")
if not (project_dir / relative).resolve().is_file():
missing.append(value)
self.assertEqual([], missing)
def test_iar_target_declares_timed_only_and_excludes_other_apps(self) -> None:
text = EWP.read_text(encoding="utf-8")
self.assertIn("PASSENGER_SEAT_TIMED_ONLY=1", text)
self.assertIn("seat_timed_application.c", text)
self.assertIn("Passenger_Seat_Timed_Controller_demo.hex", text)
for forbidden in (
"seat_move_application",
"seat_dynamic_mode",
"seat_move_store",
"seat_controller_thermal",
"..\\App\\seat_backrest_heater.c</name>",
"bsp_seat_backrest_heater_adc",
):
self.assertNotIn(forbidden, text)
def test_can_hardware_filter_accepts_only_command_id(self) -> None:
text = CAN_BSP.read_text(encoding="utf-8")
block = re.search(
r"s_standardIdFilters\[\]\s*=\s*\{(?P<body>.*?)\};",
text,
re.DOTALL,
)
self.assertIsNotNone(block)
body = block.group("body")
self.assertEqual(2, body.count("SEAT_MOVE_CAN_COMMAND_ID"))
self.assertNotRegex(body, r"0x(?:29E|2AA|3CE|42A|2F0)u")
self.assertNotIn("HIL_SELF_HEAL", body)
def test_application_exposes_only_timed_motion_dispatch(self) -> None:
text = APP.read_text(encoding="utf-8")
self.assertIn("SeatMotor_MoveTimedDuty", text)
for forbidden_call in (
"SeatMotor_MoveAbsolute",
"SeatMotor_MoveRelative",
"SeatMotor_CalibrateRange",
"SeatMotor_HomeKnownRange",
"SeatDynamicMode_",
"SeatMoveStore_",
"SeatControllerThermal_",
):
self.assertNotIn(forbidden_call, text)
accepted = set(
re.findall(
r"case\s+(SEAT_TIMED_OPCODE_[A-Z_]+)\s*:",
text[text.index("static bool SeatTimed_ParseCommand") :
text.index("static bool SeatTimed_ParametersValid")],
)
)
self.assertEqual(
{
"SEAT_TIMED_OPCODE_MOVE_TIMED",
"SEAT_TIMED_OPCODE_STOP_MOTOR",
"SEAT_TIMED_OPCODE_STOP_ALL",
"SEAT_TIMED_OPCODE_QUERY_STATUS",
"SEAT_TIMED_OPCODE_CLEAR_FAULT",
},
accepted,
)
def test_axis_specific_duration_limits_are_present(self) -> None:
text = CONFIG.read_text(encoding="utf-8")
expected = {
"SEAT_TIMED_MIN_DURATION_MS": "300",
"SEAT_TIMED_M0_MAX_DURATION_MS": "30000",
"SEAT_TIMED_M1_MAX_DURATION_MS": "20000",
"SEAT_TIMED_M2_MAX_DURATION_MS": "30000",
"SEAT_TIMED_M3_MAX_DURATION_MS": "25000",
}
for name, value in expected.items():
self.assertRegex(
text,
rf"#define\s+{name}\s+\({value}u\)",
)
self.assertNotIn("SEAT_MOVE_CAN_DYNAMIC_STATUS_ID", text)
self.assertNotIn("SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID", text)
if __name__ == "__main__":
unittest.main(verbosity=2)
+10 -2
View File
@@ -2,7 +2,14 @@
`Seat_Controller_demo/` is the repository-owned IAR project used by the real `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 seat-bench executor. It contains the 2026-07-30 heater, ventilation, four-axis
motion, SMV8 position memory, Hall feedback and ADC current-diagnostic code. time/position motion, SMV8 position memory, Hall feedback and ADC
current-diagnostic code.
`Passenger_Seat_Timed_Controller_demo/` is the separately buildable
passenger-seat variant. Its CAN application exposes four-axis timed movement
only; position, homing, calibration, dynamic-mode and thermal commands are not
linked into the application. See its `Docs/README_Passenger_Seat_Timed_Controller.md`
for the command contract and generated HEX identity (`PST1`).
The matching Cypress 7.9.0 build dependencies are vendored beside it: The matching Cypress 7.9.0 build dependencies are vendored beside it:
@@ -10,7 +17,8 @@ The matching Cypress 7.9.0 build dependencies are vendored beside it:
firmware/t2g/ firmware/t2g/
├── common/ ├── common/
├── tviibh8m/ ├── tviibh8m/
└── Seat_Controller_demo/ ├── Seat_Controller_demo/
└── Passenger_Seat_Timed_Controller_demo/
``` ```
This preserves the IAR project's `../../common` and `../../tviibh8m` paths and This preserves the IAR project's `../../common` and `../../tviibh8m` paths and
@@ -13,6 +13,7 @@
#include "bsp_seat_backrest_heater.h" #include "bsp_seat_backrest_heater.h"
#include "bsp_seat_fan.h" #include "bsp_seat_fan.h"
#include "seat_controller_thermal.h" #include "seat_controller_thermal.h"
#include "seat_feature_profile.h"
volatile uint32_t g_seatMoveResetReason = 0u; volatile uint32_t g_seatMoveResetReason = 0u;
volatile uint32_t g_seatMoveInitResult = MOTOR_API_FAILURE; volatile uint32_t g_seatMoveInitResult = MOTOR_API_FAILURE;
@@ -59,11 +60,13 @@ int main(void)
} }
Motor_SetWatchdogServiceEnabled(true); Motor_SetWatchdogServiceEnabled(true);
#if SEAT_FEATURE_THERMAL
if(!SeatControllerThermal_Init()) if(!SeatControllerThermal_Init())
{ {
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER); SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
SeatMove_HoldSafeOff(); SeatMove_HoldSafeOff();
} }
#endif
if(!SeatMoveApplication_Init()) if(!SeatMoveApplication_Init())
{ {
SeatMove_HoldSafeOff(); SeatMove_HoldSafeOff();
@@ -72,7 +75,9 @@ int main(void)
for(;;) for(;;)
{ {
SeatMoveApplication_Service(); SeatMoveApplication_Service();
#if SEAT_FEATURE_THERMAL
SeatControllerThermal_Service1ms(); SeatControllerThermal_Service1ms();
#endif
if(SafetyManager_IsInhibited()) if(SafetyManager_IsInhibited())
{ {
Motor_ForceSafeOff(); Motor_ForceSafeOff();
@@ -7,6 +7,7 @@
#include "seat_backrest_heater.h" #include "seat_backrest_heater.h"
#include "seat_backrest_heater_config.h" #include "seat_backrest_heater_config.h"
#include "seat_fan_config.h" #include "seat_fan_config.h"
#include "seat_feature_profile.h"
#define SEAT_CONTROLLER_THERMAL_CONTROL_ID (0x29Eu) #define SEAT_CONTROLLER_THERMAL_CONTROL_ID (0x29Eu)
#define SEAT_CONTROLLER_THERMAL_CONFIG_ID (0x2AAu) #define SEAT_CONTROLLER_THERMAL_CONFIG_ID (0x2AAu)
@@ -247,6 +248,7 @@ static void SeatControllerThermal_HandleControl29e(
fanGear = (uint8_t)((frame->data0 >> 16u) & 0x0Fu); fanGear = (uint8_t)((frame->data0 >> 16u) & 0x0Fu);
heaterGear = (uint8_t)((frame->data0 >> 20u) & 0x0Fu); heaterGear = (uint8_t)((frame->data0 >> 20u) & 0x0Fu);
#if SEAT_FEATURE_VENTILATION
if(fanOn == 2u) if(fanOn == 2u)
{ {
SeatControllerThermal_StopFan(1u); SeatControllerThermal_StopFan(1u);
@@ -266,7 +268,14 @@ static void SeatControllerThermal_HandleControl29e(
g_seatControllerThermalInvalidCount++; g_seatControllerThermalInvalidCount++;
SeatControllerThermal_StopFan(0u); SeatControllerThermal_StopFan(0u);
} }
#else
(void)fanOn;
(void)fanMode;
(void)fanGear;
SeatControllerThermal_StopFan(1u);
#endif
#if SEAT_FEATURE_HEATING
if(heaterOn == 2u) if(heaterOn == 2u)
{ {
SeatControllerThermal_StopHeater(1u); SeatControllerThermal_StopHeater(1u);
@@ -286,6 +295,11 @@ static void SeatControllerThermal_HandleControl29e(
g_seatControllerThermalInvalidCount++; g_seatControllerThermalInvalidCount++;
SeatControllerThermal_StopHeater(0u); SeatControllerThermal_StopHeater(0u);
} }
#else
(void)heaterOn;
(void)heaterGear;
SeatControllerThermal_StopHeater(1u);
#endif
} }
static void SeatControllerThermal_HandleConfig2aa( static void SeatControllerThermal_HandleConfig2aa(
@@ -308,16 +322,23 @@ static void SeatControllerThermal_HandleConfig2aa(
g_seatControllerThermalInvalidCount++; g_seatControllerThermalInvalidCount++;
return; return;
} }
#if SEAT_FEATURE_VENTILATION
s_fanConfiguredMaxGear = (fanConfig == 1u) ? 1u : s_fanConfiguredMaxGear = (fanConfig == 1u) ? 1u :
((fanConfig == 2u) ? 2u : 3u); ((fanConfig == 2u) ? 2u : 3u);
#endif
#if SEAT_FEATURE_HEATING
s_heaterConfiguredMaxLevel = (heaterConfig == 1u) ? 1u : s_heaterConfiguredMaxLevel = (heaterConfig == 1u) ? 1u :
((heaterConfig == 2u) ? 2u : 3u); ((heaterConfig == 2u) ? 2u : 3u);
#endif
#if SEAT_FEATURE_VENTILATION
if(s_fanRunning && if(s_fanRunning &&
!SeatControllerThermal_RequestFan(s_fanLastVehicleGear)) !SeatControllerThermal_RequestFan(s_fanLastVehicleGear))
{ {
g_seatControllerThermalOutputFaultStage = 4u; g_seatControllerThermalOutputFaultStage = 4u;
SeatControllerThermal_LatchOutputFault(); SeatControllerThermal_LatchOutputFault();
} }
#endif
#if SEAT_FEATURE_HEATING
if(g_seatBackrestHeaterAppliedLevel != if(g_seatBackrestHeaterAppliedLevel !=
SEAT_BACKREST_HEATER_LEVEL_OFF) SEAT_BACKREST_HEATER_LEVEL_OFF)
{ {
@@ -327,11 +348,13 @@ static void SeatControllerThermal_HandleConfig2aa(
SeatControllerThermal_LatchOutputFault(); SeatControllerThermal_LatchOutputFault();
} }
} }
#endif
} }
static void SeatControllerThermal_HandleMode3ce( static void SeatControllerThermal_HandleMode3ce(
const bsp_seat_move_can_frame_t *frame) const bsp_seat_move_can_frame_t *frame)
{ {
#if SEAT_FEATURE_HEATING
uint8_t mode; uint8_t mode;
if(frame->dlc != SEAT_FAN_CAN_DLC) if(frame->dlc != SEAT_FAN_CAN_DLC)
{ {
@@ -347,6 +370,10 @@ static void SeatControllerThermal_HandleMode3ce(
g_seatControllerThermalInvalidCount++; g_seatControllerThermalInvalidCount++;
SeatControllerThermal_StopHeater(0u); SeatControllerThermal_StopHeater(0u);
} }
#else
(void)frame;
SeatControllerThermal_StopHeater(1u);
#endif
} }
static void SeatControllerThermal_HandleSwitch42a( static void SeatControllerThermal_HandleSwitch42a(
@@ -363,6 +390,7 @@ static void SeatControllerThermal_HandleSwitch42a(
} }
fanSwitch = (uint8_t)(frame->data0 & 0x03u); fanSwitch = (uint8_t)(frame->data0 & 0x03u);
heaterSwitch = (uint8_t)((frame->data0 >> 2u) & 0x03u); heaterSwitch = (uint8_t)((frame->data0 >> 2u) & 0x03u);
#if SEAT_FEATURE_VENTILATION
if(fanSwitch == 1u) if(fanSwitch == 1u)
{ {
if(!SeatControllerThermal_RequestFan(s_fanLastVehicleGear)) if(!SeatControllerThermal_RequestFan(s_fanLastVehicleGear))
@@ -375,6 +403,11 @@ static void SeatControllerThermal_HandleSwitch42a(
{ {
SeatControllerThermal_StopFan(1u); SeatControllerThermal_StopFan(1u);
} }
#else
(void)fanSwitch;
SeatControllerThermal_StopFan(1u);
#endif
#if SEAT_FEATURE_HEATING
if(heaterSwitch == 1u) if(heaterSwitch == 1u)
{ {
if(!SeatControllerThermal_RequestHeater(s_heaterLastVehicleGear)) if(!SeatControllerThermal_RequestHeater(s_heaterLastVehicleGear))
@@ -387,10 +420,15 @@ static void SeatControllerThermal_HandleSwitch42a(
{ {
SeatControllerThermal_StopHeater(1u); SeatControllerThermal_StopHeater(1u);
} }
#else
(void)heaterSwitch;
SeatControllerThermal_StopHeater(1u);
#endif
} }
static bool SeatControllerThermal_RequestFan(uint8_t vehicleGear) static bool SeatControllerThermal_RequestFan(uint8_t vehicleGear)
{ {
#if SEAT_FEATURE_VENTILATION
uint8_t physicalGear = (vehicleGear >= 3u) ? 3u : vehicleGear; uint8_t physicalGear = (vehicleGear >= 3u) ? 3u : vehicleGear;
if((vehicleGear == 0u) || (vehicleGear > 5u)) if((vehicleGear == 0u) || (vehicleGear > 5u))
{ {
@@ -418,10 +456,15 @@ static bool SeatControllerThermal_RequestFan(uint8_t vehicleGear)
return BspSeatFan_SetDutyPercent(s_fanTargetDutyPercent); return BspSeatFan_SetDutyPercent(s_fanTargetDutyPercent);
} }
return true; return true;
#else
(void)vehicleGear;
return false;
#endif
} }
static bool SeatControllerThermal_RequestHeater(uint8_t vehicleGear) static bool SeatControllerThermal_RequestHeater(uint8_t vehicleGear)
{ {
#if SEAT_FEATURE_HEATING
uint8_t physicalLevel = (vehicleGear >= 3u) ? 3u : vehicleGear; uint8_t physicalLevel = (vehicleGear >= 3u) ? 3u : vehicleGear;
if((vehicleGear == 0u) || (vehicleGear > 5u) || !s_heaterModeValid) if((vehicleGear == 0u) || (vehicleGear > 5u) || !s_heaterModeValid)
{ {
@@ -440,6 +483,10 @@ static bool SeatControllerThermal_RequestHeater(uint8_t vehicleGear)
g_seatControllerHeaterFeedbackRaw = physicalLevel + 1u; g_seatControllerHeaterFeedbackRaw = physicalLevel + 1u;
s_feedbackPending = true; s_feedbackPending = true;
return true; return true;
#else
(void)vehicleGear;
return false;
#endif
} }
static void SeatControllerThermal_StopFan(uint8_t feedbackRaw) static void SeatControllerThermal_StopFan(uint8_t feedbackRaw)
@@ -8,6 +8,7 @@
#include "safety_manager.h" #include "safety_manager.h"
#include "seat_controller_thermal.h" #include "seat_controller_thermal.h"
#include "seat_dynamic_mode.h" #include "seat_dynamic_mode.h"
#include "seat_feature_profile.h"
#include "seat_motor_manager.h" #include "seat_motor_manager.h"
#include "seat_move_config.h" #include "seat_move_config.h"
#include "seat_move_store.h" #include "seat_move_store.h"
@@ -336,7 +337,9 @@ void SeatMoveApplication_Service(void)
} }
else else
{ {
#if SEAT_FEATURE_THERMAL
SeatControllerThermal_HandleFrame(&frame); SeatControllerThermal_HandleFrame(&frame);
#endif
} }
} }
SeatMove_ServiceDispatcher(); SeatMove_ServiceDispatcher();
@@ -363,7 +366,9 @@ void Motor_CalibrationServiceHook(void)
} }
else else
{ {
#if SEAT_FEATURE_THERMAL
SeatControllerThermal_HandleFrame(&frame); SeatControllerThermal_HandleFrame(&frame);
#endif
} }
} }
if(g_bspSeatMoveCanBusOff != 0u) if(g_bspSeatMoveCanBusOff != 0u)
@@ -533,15 +538,28 @@ static bool SeatMove_ParseCommand(const bsp_seat_move_can_frame_t *frame,
{ {
case SEAT_MOVE_OPCODE_MOVE_ABSOLUTE: case SEAT_MOVE_OPCODE_MOVE_ABSOLUTE:
case SEAT_MOVE_OPCODE_MOVE_RELATIVE: case SEAT_MOVE_OPCODE_MOVE_RELATIVE:
#if SEAT_FEATURE_POSITION_CONTROL
break;
#else
*reject = SEAT_MOVE_REJECT_BAD_OPCODE;
return false;
#endif
case SEAT_MOVE_OPCODE_MOVE_TIMED: case SEAT_MOVE_OPCODE_MOVE_TIMED:
case SEAT_MOVE_OPCODE_STOP_MOTOR: case SEAT_MOVE_OPCODE_STOP_MOTOR:
case SEAT_MOVE_OPCODE_STOP_ALL: case SEAT_MOVE_OPCODE_STOP_ALL:
break;
case SEAT_MOVE_OPCODE_CALIBRATE_RANGE: case SEAT_MOVE_OPCODE_CALIBRATE_RANGE:
case SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE: case SEAT_MOVE_OPCODE_HOME_KNOWN_RANGE:
case SEAT_MOVE_OPCODE_QUERY_STATUS:
case SEAT_MOVE_OPCODE_CLEAR_FAULT:
case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE: case SEAT_MOVE_OPCODE_START_DYNAMIC_MODE:
case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE: case SEAT_MOVE_OPCODE_STOP_DYNAMIC_MODE:
#if SEAT_FEATURE_POSITION_CONTROL
break;
#else
*reject = SEAT_MOVE_REJECT_BAD_OPCODE;
return false;
#endif
case SEAT_MOVE_OPCODE_QUERY_STATUS:
case SEAT_MOVE_OPCODE_CLEAR_FAULT:
break; break;
default: default:
*reject = SEAT_MOVE_REJECT_BAD_OPCODE; *reject = SEAT_MOVE_REJECT_BAD_OPCODE;
@@ -0,0 +1,27 @@
#ifndef SEAT_FEATURE_PROFILE_H
#define SEAT_FEATURE_PROFILE_H
/*
* Repository default is the complete controller. The workbench build script
* replaces this header only inside its isolated staging tree so every OTA
* task receives the exact capability profile selected by the user.
*/
#define SEAT_FEATURE_POSITION_CONTROL (1)
#define SEAT_FEATURE_VENTILATION (1)
#define SEAT_FEATURE_HEATING (1)
#if ((SEAT_FEATURE_POSITION_CONTROL != 0) && \
(SEAT_FEATURE_POSITION_CONTROL != 1))
#error "SEAT_FEATURE_POSITION_CONTROL must be 0 or 1"
#endif
#if ((SEAT_FEATURE_VENTILATION != 0) && (SEAT_FEATURE_VENTILATION != 1))
#error "SEAT_FEATURE_VENTILATION must be 0 or 1"
#endif
#if ((SEAT_FEATURE_HEATING != 0) && (SEAT_FEATURE_HEATING != 1))
#error "SEAT_FEATURE_HEATING must be 0 or 1"
#endif
#define SEAT_FEATURE_THERMAL \
((SEAT_FEATURE_VENTILATION != 0) || (SEAT_FEATURE_HEATING != 0))
#endif /* SEAT_FEATURE_PROFILE_H */
@@ -2359,6 +2359,9 @@
<file> <file>
<name>$PROJ_DIR$\..\Config\seat_move_config.h</name> <name>$PROJ_DIR$\..\Config\seat_move_config.h</name>
</file> </file>
<file>
<name>$PROJ_DIR$\..\Config\seat_feature_profile.h</name>
</file>
<file> <file>
<name>$PROJ_DIR$\..\Config\seat_backrest_heater_config.h</name> <name>$PROJ_DIR$\..\Config\seat_backrest_heater_config.h</name>
</file> </file>
@@ -1,12 +1,12 @@
:020000041000EA :020000041000EA
:100000000000022801EC00103DF4001055F1001032 :1000000000000228F5EB0010E1F30010F9F00010F9
:1000100000000000000000000000000000000000E0 :1000100000000000000000000000000000000000E0
:1000200000000000000000000000000015F20010B9 :10002000000000000000000000000000B9F1001016
:10003000000000000000000041F4001045F4001032 :100030000000000000000000E5F30010E9F30010EC
:10004000CDE50010F1E5001015E6001039E60010CE :10004000CDE50010F1E5001015E6001039E60010CE
:100050005DE6001081E60010A5E60010C9E600107C :100050005DE6001081E60010A5E60010C9E600107C
:1000600049F400104DF4001051F4001055F4001044 :10006000EDF30010F1F30010F5F30010F9F30010B8
:1000700059F400105DF4001061F4001065F40010F4 :10007000FDF3001001F4001005F4001009F4001065
:1000800000B5010009B2002905D40121C006C00E47 :1000800000B5010009B2002905D40121C006C00E47
:100090008140C648016000BD00B5010009B20029D9 :100090008140C648016000BD00B5010009B20029D9
:1000A00005D40121C006C00E8140C148016000BDD9 :1000A00005D40121C006C00E8140C148016000BDD9
@@ -78,7 +78,7 @@
:1004C000060091204000FFF779FFF106C90E009168 :1004C000060091204000FFF779FFF106C90E009168
:1004D00071090B051B0D5B1E2200D2B20321C0B2B5 :1004D00071090B051B0D5B1E2200D2B20321C0B2B5
:1004E000FFF7F8FED3BD80B51300A74A13600023C1 :1004E000FFF7F8FED3BD80B51300A74A13600023C1
:1004F000137203F048FE01BD24F3001000B5C0B232 :1004F000137203F048FE01BDC8F2001000B5C0B28F
:10050000042801D2012000E00020C0B200BD00B5E7 :10050000042801D2012000E00020C0B200BD00B5E7
:10051000401A00B2012801DB012000E00020C0B237 :10051000401A00B2012801DB012000E00020C0B237
:1005200000BD00B5BE490978002901D000201FE0B8 :1005200000BD00B5BE490978002901D000201FE0B8
@@ -313,8 +313,8 @@
:101370002000FFF723F801220321C0B2FFF762F833 :101370002000FFF723F801220321C0B2FFF762F833
:10138000884C200003F0A2FE00228749200003F0D1 :10138000884C200003F0A2FE00228749200003F0D1
:1013900063FD2000FEF720FF13BD0000FC1B0028AA :1013900063FD2000FEF720FF13BD0000FC1B0028AA
:1013A00034E700108CEB0010C21C00283CEE00104B :1013A00034E7001080EB0010C21C0028B8EC0010DD
:1013B0002CF30010CD100010F2B5060000273C0001 :1013B000D0F20010CD100010F2B5060000273C005E
:1013C0000098002801D1002077E0380000990880BB :1013C0000098002801D1002077E0380000990880BB
:1013D00003F0AEFBCD490978002903D003F0ACFB44 :1013D00003F0AEFBCD490978002903D003F0ACFB44
:1013E00000206AE00121C94A117003F0A5FB6D4D90 :1013E00000206AE00121C94A117003F0A5FB6D4D90
@@ -343,9 +343,9 @@
:10155000FFF7D5FF002806D068460078E4B2A04225 :10155000FFF7D5FF002806D068460078E4B2A04225
:1015600001D1012000E00020C0B232BD80B5B44AF4 :1015600001D1012000E00020C0B232BD80B5B44AF4
:1015700000211160017C1172C168806802F003FED5 :1015700000211160017C1172C168806802F003FED5
:1015800001BD00006012002808F00010000658405D :1015800001BD000060120028ACEF001000065840BA
:1015900080065840000758408009584000B4C404F1 :1015900080065840000758408009584000B4C404F1
:1015A0000024F400000064408CEE0010F8B5012621 :1015A0000024F4000000644030EE0010F8B501267D
:1015B0003000002468460470564804700121A14898 :1015B0003000002468460470564804700121A14898
:1015C000FEF7B8FDC24F01213800FEF7B3FD9E4D76 :1015C000FEF7B8FDC24F01213800FEF7B3FD9E4D76
:1015D000280003F003FE01219C48FEF7ABFD280024 :1015D000280003F003FE01219C48FEF7ABFD280024
@@ -1109,12 +1109,12 @@
:104530009BB21B0A9BB2174C14401B02FF221202B3 :104530009BB21B0A9BB2174C14401B02FF221202B3
:104540001A4022430B78052B05D10979C9B2FF2304 :104540001A4022430B78052B05D10979C9B2FF2304
:104550009A43114304E04978FF239A43C9B21143B7 :104550009A43114304E04978FF239A43C9B21143B7
:10456000016300221000F2BD0CF40010FFFFFBFFFE :10456000016300221000F2BDB0F30010FFFFFBFF5B
:10457000FFFFFCFFFFFFFFF8FFFFFFBFFFFBFFFF99 :10457000FFFFFCFFFFFFFFF8FFFFFFBFFFFBFFFF99
:10458000FFFDFFFFFFFEFFFFFFCFFFFF10F4001056 :10458000FFFDFFFFFFFEFFFFFFCFFFFFB4F30010B3
:10459000FFFF00FFFF00FFFF14F4001018F40010ED :10459000FFFF00FFFF00FFFFB8F30010BCF30010A7
:1045A000FFF3FFFFFFFCFFFF1CF4001020F40010DE :1045A000FFF3FFFFFFFCFFFFC0F30010C4F3001098
:1045B00024F400100168802212060A43026070474A :1045B000C8F300100168802212060A4302607047A7
:1045C000016849004908016070474068704701610F :1045C000016849004908016070474068704701610F
:1045D00070470069704781607047006A704700004B :1045D00070470069704781607047006A704700004B
:1045E00030B4434351435D18010C0C00130C5C4381 :1045E00030B4434351435D18010C0C00130C5C4381
@@ -1169,7 +1169,7 @@
:1048F000142060432818007C787103A914206043B9 :1048F000142060432818007C787103A914206043B9
:104900002818C06802F0EAFC0028E7D0300002F066 :104900002818C06802F0EAFC0028E7D0300002F066
:10491000E1FC002003E0012071490870012019B07A :10491000E1FC002003E0012071490870012019B07A
:10492000F0BD000024F3001000000440FF00FFFF72 :10492000F0BD0000C8F2001000000440FF00FFFFCF
:10493000FF0000FFF8B584B00E00C0B20290002561 :10493000FF0000FFF8B584B00E00C0B20290002561
:1049400028006946088004212A0003AC200002F0F8 :1049400028006946088004212A0003AC200002F0F8
:1049500033FC2C0062480078002806D0674F387876 :1049500033FC2C0062480078002806D0674F387876
@@ -1191,16 +1191,16 @@
:104A500001D0002018E0631E042201212800FFF786 :104A500001D0002018E0631E042201212800FFF786
:104A6000EFFD002801D000200EE0042201212800E3 :104A6000EFFD002801D000200EE0042201212800E3
:104A7000FFF771FE002801D0002005E0009821001A :104A7000FFF771FE002801D0002005E0009821001A
:104A8000FFF7C0FB38600120F2BD0000FCF200100F :104A8000FFF7C0FB38600120F2BD0000A0F200106B
:104A90007CB505001D4C062100226E46300002F058 :104A90007CB505001D4C062100226E46300002F058
:104AA0008BFB6946280002F011FE6846007800285A :104AA0008BFB6946280002F011FE6846007800285A
:104AB00003D1641E002CF4D101E0012000E00020AD :104AB00003D1641E002CF4D101E0012000E00020AD
:104AC00076BD000014F10010E0B511490CC9083999 :104AC00076BD0000B8F00010E0B511490CC90839F6
:104AD00069460CC10839694602F0A2FD07BD000015 :104AD00069460CC10839694602F0A2FD07BD000015
:104AE000B71C0028901B0028A81B00282CEA0010E7 :104AE000B71C0028901B0028A81B002820EA0010F3
:104AF000FFC99A3B00CA9A3B00109040B81C00289E :104AF000FFC99A3B00CA9A3B00109040B81C00289E
:104B00005F8DCB00608DCB0001000100A08601000D :104B00005F8DCB00608DCB0001000100A08601000D
:104B100004F300100000000000000000000000008E :104B1000A8F20010000000000000000000000000EB
:104B2000801C800803D0401C801EFCD100BF704751 :104B2000801C800803D0401C801EFCD100BF704751
:104B3000EFF3108072B6704780F3108870474030F2 :104B3000EFF3108072B6704780F3108870474030F2
:104B4000016810220A430260704740300168082261 :104B4000016810220A430260704740300168082261
@@ -1234,10 +1234,10 @@
:104D0000A54394000C2222402A43C264174A146827 :104D0000A54394000C2222402A43C264174A146827
:104D10004C228A5C1A400725AC4322434D240C5D8B :104D10004C228A5C1A400725AC4322434D240C5D8B
:104D200023407024A2431C01702323401343436596 :104D200023407024A2431C01702323401343436596
:104D3000497D816700221000F2BD0000F8F30010E9 :104D3000497D816700221000F2BD00009CF3001045
:104D4000FFFFFBFFFFFFFCFFFFFFFFF8FFFFFFBFC1 :104D4000FFFFFBFFFFFFFCFFFFFFFFF8FFFFFFBFC1
:104D5000FCF30010FFFF00FFFF00FFFF00F4001056 :104D5000A0F30010FFFF00FFFF00FFFFA4F300100F
:104D600004F40010FFF3FFFFFFFCFFFF08F4001046 :104D6000A8F30010FFF3FFFFFFFCFFFFACF3001000
:104D70000168802212060A43026070477830016899 :104D70000168802212060A43026070477830016899
:104D800001220A4302607047C16F0800C007C00FCC :104D800001220A4302607047C16F0800C007C00FCC
:104D90007047010070310A68012313430B60006FF4 :104D90007047010070310A68012313430B60006FF4
@@ -1246,7 +1246,7 @@
:104DC0000B79002B01D00122FFE763005B08D207BB :104DC0000B79002B01D00122FFE763005B08D207BB
:104DD0001A430B4B0024095F89005A5000E0064833 :104DD0001A430B4B0024095F89005A5000E0064833
:104DE00030BD00B5074A1368002B03D0126800B22B :104DE00030BD00B5074A1368002B03D0126800B22B
:104DF0008000115000BD000001005600F4F30010C7 :104DF0008000115000BD00000100560098F3001023
:104E0000008020402412002870B4103A03D378C9DF :104E0000008020402412002870B4103A03D378C9DF
:104E1000103A78C0FBD2530701D318C918C001D586 :104E1000103A78C0FBD2530701D318C918C001D586
:104E200008C908C0D20704D30B880380891C801CE2 :104E200008C908C0D20704D30B880380891C801CE2
@@ -1296,8 +1296,8 @@
:1050E0000021C1239B00C15080239B00C25081221C :1050E0000021C1239B00C15080239B00C25081221C
:1050F00092008150124A8150154A8150114A8150C4 :1050F00092008150124A8150154A8150114A8150C4
:10510000114A8150134A8150134A81507047000060 :10510000114A8150134A8150134A81507047000060
:105110000140AA00DCF30010E0F30010E4F30010FB :105110000140AA0080F3001084F3001088F300100F
:10512000E8F30010FF3FFFFFFFFEFFFFFFFFFFFC64 :105120008CF30010FF3FFFFFFFFEFFFFFFFFFFFCC0
:10513000FFFDFFFFFFFBFFFFFFF7FFFF0F00000377 :10513000FFFDFFFFFFFBFFFFFFF7FFFF0F00000377
:10514000C80E0000C80F0000880F0000EFCDAB00B4 :10514000C80E0000C80F0000880F0000EFCDAB00B4
:10515000880E0000080F0000480F0000F8B504009A :10515000880E0000080F0000480F0000F8B504009A
@@ -1313,7 +1313,7 @@
:1051F00002F0DAFE0020E075E52040000A49086070 :1051F00002F0DAFE0020E075E52040000A49086070
:10520000002002F031FF2C0030681049FEF7FAFF51 :10520000002002F031FF2C0030681049FEF7FAFF51
:10521000002122000023FFF7E3F9FFF79FFF70BD95 :10521000002122000023FFF7E3F9FFF79FFF70BD95
:10522000181B00288CED0010F01B002810E000E097 :10522000181B0028A0ED0010F01B002810E000E083
:10523000BE1C0028FFFFFF00010000FFFFFFFFFF73 :10523000BE1C0028FFFFFF00010000FFFFFFFFFF73
:1052400014E000E0941C002818E000E040420F0049 :1052400014E000E0941C002818E000E040420F0049
:1052500043490A680860704742490A680860704715 :1052500043490A680860704742490A680860704715
@@ -1402,8 +1402,8 @@
:1057800000B5114B1870114B1970002A05D0117813 :1057800000B5114B1870114B1970002A05D0117813
:105790000F4B197091880F4A1180C0B2002803D0B6 :105790000F4B197091880F4A1180C0B2002803D0B6
:1057A00001209949087002E0002097490870012003 :1057A00001209949087002E0002097490870012003
:1057B0009649087000BD0000AC1A002834F40010AF :1057B0009649087000BD0000AC1A0028D8F300100C
:1057C000D0F2001038F40010D71C0028D81C002894 :1057C00074F20010DCF30010D71C0028D81C00284D
:1057D000D91C0028B01C0028F8B50500002D05D004 :1057D000D91C0028B01C0028F8B50500002D05D004
:1057E0000C00002C02D01600002E01D1002039E060 :1057E0000C00002C02D01600002E01D1002039E060
:1057F0000A2100222700380001F0DEFC68682070D2 :1057F0000A2100222700380001F0DEFC68682070D2
@@ -1866,7 +1866,7 @@
:107480009205C02000041040084300998860E08AFB :107480009205C02000041040084300998860E08AFB
:10749000390C090480B20843218B82B208041043DE :10749000390C090480B20843218B82B208041043DE
:1074A0000099C86021001A31009800F055F801E0F9 :1074A0000099C86021001A31009800F055F801E0F9
:1074B00020480190019805B0F0BD00004CF3001089 :1074B00020480190019805B0F0BD0000F0F20010E6
:1074C000FFFEFFFFFFFDFFFFFFFBFFFFFFFFFFDFF3 :1074C000FFFEFFFFFFFDFFFFFFFBFFFFFFFFFFDFF3
:1074D0003830016801220A43026070473830016881 :1074D0003830016801220A43026070473830016881
:1074E00001229143016070473C30016801220A4348 :1074E00001229143016070473C30016801220A4348
@@ -1874,8 +1874,8 @@
:1075000014D05B6A1C000C80190F0C00E407E40F18 :1075000014D05B6A1C000C80190F0C00E407E40F18
:107510001470990F0C00E407E40F9470590F0C00DD :107510001470990F0C00E407E40F9470590F0C00DD
:10752000E407E40F5470D90FD17000E0014810BD9A :10752000E407E40F5470D90FD17000E0014810BD9A
:10753000FFFFFFBF0100DA0050F3001054F300100A :10753000FFFFFFBF0100DA00F4F20010F8F20010C4
:1075400058F300105CF3001060F30010FFFCFFFF25 :10754000FCF2001000F3001004F30010FFFCFFFF3A
:10755000FFF7FFFFFFF8FFFFF0B50A000023190057 :10755000FFF7FFFFFFF8FFFFF0B50A000023190057
:107560006F4C2468002A49D05579002D01D001269E :107560006F4C2468002A49D05579002D01D001269E
:1075700000E01E006B4D2540B4028026F60034402A :1075700000E01E006B4D2540B4028026F60034402A
@@ -1905,8 +1905,8 @@
:1076F000E407E40FCC7053081C00E407E40F4C705F :1076F000E407E40FCC7053081C00E407E40F4C705F
:1077000013001C00E407E40F0C7092081300DB0761 :1077000013001C00E407E40F0C7092081300DB0761
:10771000DB0F8B7000E0014810BD00000100DA00B3 :10771000DB0F8B7000E0014810BD00000100DA00B3
:1077200064F30010FFFBFFFFFFFDFFFFFFFEFFFF05 :1077200008F30010FFFBFFFFFFFDFFFFFFFEFFFF61
:1077300068F3001000B5012027490A68520802408A :107730000CF3001000B5012027490A6852080240E6
:10774000002A01D0022001E00968084000BD274856 :10774000002A01D0022001E00968084000BD274856
:1077500001680800C007C00F704700B5C21E022AAA :1077500001680800C007C00F704700B5C21E022AAA
:1077600001D3012002E0C01E0860002000BD1CB54E :1077600001D3012002E0C01E0860002000BD1CB54E
@@ -1929,7 +1929,7 @@
:107870008007800F32BD0000401626400F48006888 :107870008007800F32BD0000401626400F48006888
:10788000000AC0B270470E480068000AC0B27047D4 :10788000000AC0B270470E480068000AC0B27047D4
:10789000000000000019264010002740181B002897 :10789000000000000019264010002740181B002897
:1078A000D4EC00105B080000F01B002840122640BA :1078A00008ED00105B080000F01B00284012264085
:1078B00004480068000AC0B2704700001010204061 :1078B00004480068000AC0B2704700001010204061
:1078C0000C1020400810204038B50400072C0ED3BF :1078C0000C1020400810204038B50400072C0ED3BF
:1078D000124D18221249280000F066FB0020E875BE :1078D000124D18221249280000F066FB0020E875BE
@@ -1937,7 +1937,7 @@
:1078F000A10040584007400F010009B2042907D1F8 :1078F000A10040584007400F010009B2042907D1F8
:107900000448A1004058C106C90E80204000084329 :107900000448A1004058C106C90E80204000084329
:1079100000B232BD0001264000122640181B00288C :1079100000B232BD0001264000122640181B00288C
:1079200030ED0010F01B00281E4909688A03920BF5 :1079200054ED0010F01B00281E4909688A03920BD1
:107930000260090E0A00D207D20FC2711749096806 :107930000260090E0A00D207D20FC2711749096806
:107940000A00D204D20C82800A00120CD2B2028148 :107940000A00D204D20C82800A00120CD2B2028148
:10795000090CC9B24181264909680F220B00134066 :10795000090CC9B24181264909680F220B00134066
@@ -2030,7 +2030,7 @@
:107EC000002805D002280BD006D303280CD00FE0E1 :107EC000002805D002280BD006D303280CD00FE0E1
:107ED00000982860002032BD009840082860F9E72B :107ED00000982860002032BD009840082860F9E72B
:107EE000009880082860F5E70098C0082860F1E74E :107EE000009880082860F5E70098C0082860F1E74E
:107EF000002028600320EEE7181B002830ED00105A :107EF000002028600320EEE7181B002854ED001036
:107F000063090000F01B0028001626407F090000CE :107F000063090000F01B0028001626407F090000CE
:107F1000001926401CB5040069460020FFF7AFFF9A :107F1000001926401CB5040069460020FFF7AFFF9A
:107F20000100C9B2002903D000212160C0B209E0DC :107F20000100C9B2002903D000212160C0B209E0DC
@@ -2352,7 +2352,7 @@
:1092E0000C482A00D2B2805CFFF724FE002808D088 :1092E0000C482A00D2B2805CFFF724FE002808D088
:1092F00068463221405C002803D020000124AC40A5 :1092F00068463221405C002803D020000124AC40A5
:1093000004436D1C2800C0B20428E8D32000C0B27A :1093000004436D1C2800C0B20428E8D32000C0B27A
:109310000FB030BD28F40010C027090000B5017857 :109310000FB030BDCCF30010C027090000B50178B4
:109320000A290ED0417804290BD04178062908D0AB :109320000A290ED0417804290BD04178062908D0AB
:109330004178072905D04178082902D040780928CA :109330004178072905D04178082902D040780928CA
:1093400001D1012000E00020C0B200BDF1B590B015 :1093400001D1012000E00020C0B200BDF1B590B015
@@ -2378,7 +2378,7 @@
:10948000691848832B895A2270682100C9B24900A3 :10948000691848832B895A2270682100C9B24900A3
:10949000415A8B482700FFB2C05DFFF719FC002836 :10949000415A8B482700FFB2C05DFFF719FC002836
:1094A000E1D1002000E00120F2BD0000FFFF00003C :1094A000E1D1002000E00120F2BD0000FFFF00003C
:1094B0009CF00010781A0028F8B590B00700002F33 :1094B00040F00010781A0028F8B590B00700002F8F
:1094C00004D008003B7BC0B2984201D30020BEE02C :1094C00004D008003B7BC0B2984201D30020BEE02C
:1094D000B8680B00DBB218246343C0180190904EAB :1094D000B8680B00DBB218246343C0180190904EAB
:1094E00002207070F17001980078B07032610198BC :1094E00002207070F17001980078B07032610198BC
@@ -2411,13 +2411,13 @@
:10969000641C2000C0B2042810D22B89B27D2000A7 :10969000641C2000C0B2042810D22B89B27D2000A7
:1096A000C0B240002818418D1E482700FFB2C05D9F :1096A000C0B240002818418D1E482700FFB2C05D9F
:1096B000FFF70EFB0028EBD1002000E00120F2BDF7 :1096B000FFF70EFB0028EBD1002000E00120F2BDF7
:1096C00028F4001038B5164D2C89641C200080B297 :1096C000CCF3001038B5164D2C89641C200080B2F4
:1096D000F8F7A0FFF8F750FE200080B2FEF756FF23 :1096D000F8F7A0FFF8F750FE200080B2FEF756FF23
:1096E0002C8103206870F9F724FB286131BD38B55F :1096E0002C8103206870F9F724FB286131BD38B55F
:1096F0000A000B4BD978491C0C00057BE4B2AC4244 :1096F0000A000B4BD978491C0C00057BE4B2AC4244
:1097000003D30021DC88641CDC80C9B2FFF7D4FEDF :1097000003D30021DC88641CDC80C9B2FFF7D4FEDF
:10971000002801D1FFF7D6FF31BD000030750000F1 :10971000002801D1FFF7D6FF31BD000030750000F1
:10972000781A002828F40010F8B5C0B0002001F025 :10972000781A0028CCF30010F8B5C0B0002001F082
:1097300046FD20A9002000F098FA070069460120A4 :1097300046FD20A9002000F098FA070069460120A4
:1097400000F093FA04003800C0B2022801D10125CC :1097400000F093FA04003800C0B2022801D10125CC
:1097500000E000252000C0B2022801D1012600E06F :1097500000E000252000C0B2022801D1012600E06F
@@ -2591,7 +2591,7 @@
:10A1D0001500EDB22D015D196D6865601500EDB2D9 :10A1D0001500EDB22D015D196D6865601500EDB2D9
:10A1E0002D015D19AD68A5601500EDB22D015B195B :10A1E0002D015D19AD68A5601500EDB22D015B195B
:10A1F0001B7B2373521C1300DBB2102B00D30022F5 :10A1F0001B7B2373521C1300DBB2102B00D30022F5
:10A200000A70FAF799FC012032BD000024F3001017 :10A200000A70FAF799FC012032BD0000C8F2001074
:10A2100000000440FF00FFFFFF0000FFF1B594B015 :10A2100000000440FF00FFFFFF0000FFF1B594B015
:10A220000D0016005021002422006F463800FCF774 :10A220000D0016005021002422006F463800FCF774
:10A23000C3FF149880210901884203D24E48006868 :10A23000C3FF149880210901884203D24E48006868
@@ -2639,7 +2639,7 @@
:10A4D00059188C60027070BD2817002800B5C0B2F2 :10A4D00059188C60027070BD2817002800B5C0B2F2
:10A4E000002802D101200F49086000BDE01B0028B0 :10A4E000002802D101200F49086000BDE01B0028B0
:10A4F000DC1B0028D01B0028D41B0028005A620255 :10A4F000DC1B0028D01B0028D41B0028005A620255
:10A500006CEF0010A0EF0010D4EF0010800131407C :10A5000010EF001044EF001078EF00108001314090
:10A51000C81B0028D81B0028B91C0028BA1C00281A :10A51000C81B0028D81B0028B91C0028BA1C00281A
:10A52000CC1B0028E41B002800B5BFF34F8F3D482B :10A52000CC1B0028E41B002800B5BFF34F8F3D482B
:10A53000AE490860BFF34F8F00BFFDE7AC490A6822 :10A53000AE490860BFF34F8F00BFFDE7AC490A6822
@@ -2875,9 +2875,9 @@
:10B39000D2B26946002002F027F900280CD0094DEE :10B39000D2B26946002002F027F900280CD0094DEE
:10B3A000182209492800FCF7FFFDEC75F520074934 :10B3A000182209492800FCF7FFFDEC75F520074934
:10B3B00008600020FCF758FE05B0F0BD00E100E099 :10B3B00008600020FCF758FE05B0F0BD00E100E099
:10B3C0001CED00E0181B0028E8ED0010F01B002821 :10B3C0001CED00E0181B0028ECED0010F01B00281D
:10B3D0008BB100100800244000F4244000F5244004 :10B3D0008BB100100800244000F4244000F5244004
:10B3E000BB1C0028BC1C002814F3001000B500286A :10B3E000BB1C0028BC1C0028B8F2001000B50028C7
:10B3F00001D0002901D1A04808E0A04A904206D11E :10B3F00001D0002901D1A04808E0A04A904206D11E
:10B4000000200880802080014860002000BD9C4A08 :10B4000000200880802080014860002000BD9C4A08
:10B41000904205D18020800102000A804860F4E754 :10B41000904205D18020800102000A804860F4E754
@@ -2930,7 +2930,7 @@
:10B70000AD5DA4B2ADB2AC42EBD370BD000452400B :10B70000AD5DA4B2ADB2AC42EBD370BD000452400B
:10B7100000065240000852400000544000025440CD :10B7100000065240000852400000544000025440CD
:10B7200000045440000654400008544058080028C3 :10B7200000045440000654400008544058080028C3
:10B730000CF3001001085240301B0028F1B5A8B0EE :10B73000B0F2001001085240301B0028F1B5A8B04B
:10B740000020009000202898FFF7BAFE0700002F85 :10B740000020009000202898FFF7BAFE0700002F85
:10B7500029D0289E306D97490140002902D02898B1 :10B7500029D0289E306D97490140002902D02898B1
:10B76000FFF73EFF0125306DC00C284000281CD19A :10B76000FFF73EFF0125306DC00C284000281CD19A
@@ -2970,7 +2970,7 @@
:10B98000009A120A14400C43046078680028D3D04F :10B98000009A120A14400C43046078680028D3D04F
:10B9900013AA012101207B689847CDE70648289922 :10B9900013AA012101207B689847CDE70648289922
:10B9A000BC22895809094907490F415C891C0020BC :10B9A000BC22895809094907490F415C891C0020BC
:10B9B000B4E70000EEE0F73F0CF30010F1B594B0EF :10B9B000B4E70000EEE0F73FB0F20010F1B594B04C
:10B9C000D34800681290D34800681190D2480768A5 :10B9C000D34800681290D34800681190D2480768A5
:10B9D000D2480668D24800681090D24800680F909C :10B9D000D2480668D24800681090D24800680F909C
:10B9E000D14800680490D14800680E90D0480068A3 :10B9E000D14800680490D14800680E90D0480068A3
@@ -3024,15 +3024,15 @@
:10BCE000EB589B0814339B049B0C11409A001040A6 :10BCE000EB589B0814339B049B0C11409A001040A6
:10BCF0000843C0216850802252021498000C0004AE :10BCF0000843C0216850802252021498000C0004AE
:10BD00006946898A4018801806998B08002125E029 :10BD00006946898A4018801806998B08002125E029
:10BD10006CF3001070F3001074F3001078F300104F :10BD100010F3001014F3001018F300101CF30010BF
:10BD20007CF3001080F3001084F3001088F30010FF :10BD200020F3001024F3001028F300102CF300106F
:10BD30008CF3001090F3001094F3001098F30010AF :10BD300030F3001034F3001038F300103CF300101F
:10BD40009CF30010A0F30010A4F30010A8F300105F :10BD400040F3001044F3001048F300104CF30010CF
:10BD5000ACF3001000260660001D491C9942F9D37F :10BD500050F3001000260660001D491C9942F9D3DB
:10BD600001990120084302210143CF480840217D69 :10BD600001990120084302210143CF480840217D69
:10BD7000012901D1012100E00021CC4B0340480200 :10BD7000012901D1012100E00021CC4B0340480200
:10BD80008021890008401843217D01290CD101211F :10BD80008021890008401843217D01290CD101211F
:10BD90000BE000BFB0F30010B4F30010010062002C :10BD90000BE000BF54F3001058F3001001006200E4
:10BDA000FFFF00FF0300FFFF0021C14B034008021B :10BDA000FFFF00FF0300FFFF0021C14B034008021B
:10BDB000802149000840184380218843402188435E :10BDB000802149000840184380218843402188435E
:10BDC0002021884310218843042188430190019851 :10BDC0002021884310218843042188430190019851
@@ -3046,7 +3046,7 @@
:10BE4000E67F0E40F027B8433701F0263E40064318 :10BE4000E67F0E40F027B8433701F0263E40064318
:10BE50002020205C01400F20864331432220205CBB :10BE50002020205C01400F20864331432220205CBB
:10BE6000012811D1012610E0FFFF80FFFCFF000038 :10BE6000012811D1012610E0FFFF80FFFCFF000038
:10BE7000FFF8FFFFFFFFFF800CF30010FFFFFFC084 :10BE7000FFF8FFFFFFFFFF80B0F20010FFFFFFC0E1
:10BE8000FFFFC0FFFFFFFFBF00268F480840F105FE :10BE8000FFFFC0FFFFFFFFBF00268F480840F105FE
:10BE90008026360431400143E9602320215C1940AB :10BE90008026360431400143E9602320215C1940AB
:10BEA0000B9E8A4830400E02FE21C90131400143F9 :10BEA0000B9E8A4830400E02FE21C90131400143F9
@@ -3126,7 +3126,7 @@
:10C34000684600788540D0203D500020F2BD0000B6 :10C34000684600788540D0203D500020F2BD0000B6
:10C35000301B002801006200FFFFFF1F030A0006D8 :10C35000301B002801006200FFFFFF1F030A0006D8
:10C36000330A0000FFFFFFDFFFFFFFBFFFFF7FFF7C :10C36000330A0000FFFFFFDFFFFFFFBFFFFF7FFF7C
:10C37000FFFFF0FFFFFFDFFFFFFFEFFF0CF30010F9 :10C37000FFFFF0FFFFFFDFFFFFFFEFFFB0F2001056
:10C3800010B50200002A03D00800C0B2202801DB4B :10C3800010B50200002A03D00800C0B2202801DB4B
:10C39000002021E000200123CC241459CC401C4073 :10C39000002021E000200123CC241459CC401C4073
:10C3A000002C09D0D4201058C8401840002801D1D2 :10C3A000002C09D0D4201058C8401840002801D1D2
@@ -3229,7 +3229,7 @@
:10C9B0002000F7F705FE4D492000F7F77BFC002823 :10C9B0002000F7F705FE4D492000F7F77BFC002823
:10C9C00001D000200FE02000F7F707FE4849884219 :10C9C00001D000200FE02000F7F707FE4849884219
:10C9D00007D12000F7F7FDFD4649884201D101202B :10C9D00007D12000F7F7FDFD4649884201D101202B
:10C9E00000E00020C0B210BD24F3001070B5424C2E :10C9E00000E00020C0B210BDC8F2001070B5424C8B
:10C9F0000021216000F046F8002803D101202060CA :10C9F0000021216000F046F8002803D101202060CA
:10CA000000203AE0374E002201213000FFF76FFE90 :10CA000000203AE0374E002201213000FFF76FFE90
:10CA1000354D37492800F7F7DEFD2800F7F7CAFD46 :10CA1000354D37492800F7F7DEFD2800F7F7CAFD46
@@ -3245,7 +3245,7 @@
:10CAB00032BD10B50C48F7F783FD0A4C0022012166 :10CAB00032BD10B50C48F7F783FD0A4C0022012166
:10CAC000200000F07BF800220221200000F076F820 :10CAC000200000F07BF800220221200000F076F820
:10CAD00010BD0000FF0000FF00FFFF00FFFCFFFF94 :10CAD00010BD0000FF0000FF00FFFF00FFFCFFFF94
:10CAE00038EF001080053140001E584018EB001050 :10CAE000DCEE001080053140001E58400CEB0010B9
:10CAF0001F4E0000204E0000C01B0028F8B5002586 :10CAF0001F4E0000204E0000C01B0028F8B5002586
:10CB000000956846FBF706FA002801D0002045E0B2 :10CB000000956846FBF706FA002801D0002045E0B2
:10CB1000274C0098A04205D300982100F7F772FB3C :10CB1000274C0098A04205D300982100F7F772FB3C
@@ -3316,7 +3316,7 @@
:10CF20001AE0534C2000F7F74BFB52492000F7F76B :10CF20001AE0534C2000F7F74BFB52492000F7F76B
:10CF3000C1F9002801D000200EE02000F7F74DFBDA :10CF3000C1F9002801D000200EE02000F7F74DFBDA
:10CF40004D49884206D12000F7F743FB002801D164 :10CF40004D49884206D12000F7F743FB002801D164
:10CF5000012000E00020C0B210BD000024F300104A :10CF5000012000E00020C0B210BD0000C8F20010A7
:10CF600070B5464C0021216000F046F8002803D13E :10CF600070B5464C0021216000F046F8002803D13E
:10CF70000120206000203AE03B4E002207213000D3 :10CF70000120206000203AE03B4E002207213000D3
:10CF8000FFF777FE3A4D3C492800F7F724FB2800CD :10CF8000FFF777FE3A4D3C492800F7F724FB2800CD
@@ -3333,8 +3333,8 @@
:10D03000FF0000FF80B50E48F7F7C2FA00F07AF85B :10D03000FF0000FF80B50E48F7F7C2FA00F07AF85B
:10D0400000200A49086001BD80B50948F7F7BDFA1C :10D0400000200A49086001BD80B50948F7F7BDFA1C
:10D05000C10B0800C007C00F02BD000000FFFF00A9 :10D05000C10B0800C007C00F02BD000000FFFF00A9
:10D06000FFFCFFFF04EF001000083140B81B002850 :10D06000FFFCFFFFA8EE001000083140B81B0028AD
:10D0700080205840A4EA00101F4E0000BC1B00286E :10D070008020584098EA00101F4E0000BC1B00287A
:10D08000F8B5002500956846FAF744FF002801D05E :10D08000F8B5002500956846FAF744FF002801D05E
:10D09000002045E0354C0098A04205D300982100BF :10D09000002045E0354C0098A04205D300982100BF
:10D0A000F7F7B0F8002901D0002039E000982100FE :10D0A000F7F7B0F8002901D0002039E000982100FE
@@ -3388,11 +3388,11 @@
:10D3A000300029001022F7F749FD00F017F8012896 :10D3A000300029001022F7F749FD00F017F8012896
:10D3B00003D1032008E0022006E000F00FF8002867 :10D3B00003D1032008E0022006E000F00FF8002867
:10D3C00001D1002000E00420FEBD0000181B002851 :10D3C00001D1002000E00420FEBD0000181B002851
:10D3D00024E90010F01B00280010224010B5274C53 :10D3D000A4E90010F01B00280010224010B5274CD3
:10D3E00010212000FAF762FE2068000F0007A0213C :10D3E00010212000FAF762FE2068000F0007A0213C
:10D3F0000906884201D1002009E02068000F0007DB :10D3F0000906884201D1002009E02068000F0007DB
:10D40000F0210906884201D1012000E0022010BD70 :10D40000F0210906884201D1012000E0022010BD70
:10D41000ECF30010F0F30010000020400000224068 :10D4100090F3001094F30010000020400000224020
:10D42000FFFFFF0F38B50400FFF7D8FF0500280005 :10D42000FFFFFF0F38B50400FFF7D8FF0500280005
:10D43000C0B2022801D142480DE0002C04D02000E7 :10D43000C0B2022801D142480DE0002C04D02000E7
:10D4400027491022F7F7FAFCEDB2002D01D1002098 :10D4400027491022F7F7FAFCEDB2002D01D1002098
@@ -3411,7 +3411,7 @@
:10D5100001D1012600E002262800F040C00705D511 :10D5100001D1012600E002262800F040C00705D511
:10D5200009480168002901D0006880472A0CA9B287 :10D5200009480168002901D0006880472A0CA9B287
:10D530002000FFF746FE70BDFFFFFF00801B0028A4 :10D530002000FFF746FE70BDFFFFFF00801B0028A4
:10D54000FFFFFF0F1CF30010E81B0028E5D40010BC :10D54000FFFFFF0FC0F20010E81B0028E5D4001019
:10D550000000204050480068410B0800C007C00F81 :10D550000000204050480068410B0800C007C00F81
:10D5600070474D480068010B0800C007C00F7047A6 :10D5600070474D480068010B0800C007C00F7047A6
:10D5700000B54A48012102680A43026002680A4075 :10D5700000B54A48012102680A43026002680A4075
@@ -3445,7 +3445,7 @@
:10D73000294820188021490288420FD201200EE09A :10D73000294820188021490288420FD201200EE09A
:10D740008020000255496118814201D2012006E083 :10D740008020000255496118814201D2012006E083
:10D7500053496118814201D2012000E0002010BD30 :10D7500053496118814201D2012000E0002010BD30
:10D76000FFFF00FF7CF2001010B50400FFF7F9FE88 :10D76000FFFF00FF20F2001010B50400FFF7F9FEE4
:10D77000002810D1F02000062018FE21C90388429D :10D77000002810D1F02000062018FE21C90388429D
:10D7800001D2002026E0654820188021C902884285 :10D7800001D2002026E0654820188021C902884285
:10D790001FD200201EE0FE208003F0210906611840 :10D790001FD200201EE0FE208003F0210906611840
@@ -3504,7 +3504,7 @@
:10DAE00015001E003800FFF7EFFF00280DD12900B8 :10DAE00015001E003800FFF7EFFF00280DD12900B8
:10DAF0003800FFF7E5FF31003800FFF7E3FF2100B2 :10DAF0003800FFF7E5FF31003800FFF7E3FF2100B2
:10DB00003800FFF7D5FF002000E00248F2BD00001A :10DB00003800FFF7D5FF002000E00248F2BD00001A
:10DB1000D8F3001001008A00F8B584B0F7F715FCBF :10DB10007CF3001001008A00F8B584B0F7F715FC1B
:10DB2000BA49012708680322904338430860B8487F :10DB2000BA49012708680322904338430860B8487F
:10DB30000A6802400A60B7490A6803239A433A43D5 :10DB30000A6802400A60B7490A6803239A433A43D5
:10DB40000A600A6802400A60B3490A6803239A43DC :10DB40000A600A6802400A60B3490A6803239A43DC
@@ -3589,8 +3589,8 @@
:10E030004018FA218900F6F7E5F828602868C0033F :10E030004018FA218900F6F7E5F828602868C0033F
:10E040001749086007B030BD041626400C19264059 :10E040001749086007B030BD041626400C19264059
:10E050001C1926404016264044162640141226401D :10E050001C1926404016264044162640141226401D
:10E06000E0F2001040122640080020400804204042 :10E0600084F200104012264008002040080420409E
:10E070000815264064F20010181B00286CEC0010F4 :10E070000815264008F20010181B002860EC00105C
:10E08000F01B0028941C0028981C00283F420F0019 :10E08000F01B0028941C0028981C00283F420F0019
:10E0900040420F00A01C00289C1C0028E703000041 :10E0900040420F00A01C00289C1C0028E703000041
:10E0A000A41C0028F3B595B0170000210D0008004E :10E0A000A41C0028F3B595B0170000210D0008004E
@@ -3672,7 +3672,7 @@
:10E5600000210120FFF7ECFE0020FFF7DDFE012077 :10E5600000210120FFF7ECFE0020FFF7DDFE012077
:10E57000FFF7DAFE10BD000000E100E000E400E07B :10E57000FFF7DAFE10BD000000E100E000E400E07B
:10E580001CED00E000132040FFFFF7FF8013204048 :10E580001CED00E000132040FFFFF7FF8013204048
:10E59000A013204044F3001064EC001068EC00105D :10E59000A0132040E8F2001058EC00105CEC0010D2
:10E5A00080000000001D00280000001008ED00E0C1 :10E5A00080000000001D00280000001008ED00E0C1
:10E5B00000B5010009B2002905D40121C006C00E32 :10E5B00000B5010009B2002905D40121C006C00E32
:10E5C00081404A48016000BD00B5FEE780B548487B :10E5C00081404A48016000BD00B5FEE780B548487B
@@ -3694,23 +3694,23 @@
:10E6C0000620FFF775FF01BD80B51848006818489F :10E6C0000620FFF775FF01BD80B51848006818489F
:10E6D0000068C10F002905D007498005800D800022 :10E6D0000068C10F002905D007498005800D800022
:10E6E000085880470720FFF763FF01BD80E200E084 :10E6E000085880470720FFF763FF01BD80E200E084
:10E6F000B8F300100011204048090028BCF30010B6 :10E6F0005CF30010001120404809002860F300106E
:10E7000004112040C0F3001008112040C4F3001091 :10E700000411204064F300100811204068F3001049
:10E710000C112040C8F3001010112040CCF3001061 :10E710000C1120406CF300101011204070F3001019
:10E7200014112040D0F3001018112040D4F3001031 :10E720001411204074F300101811204078F30010E9
:10E730001C11204034F1001090F200108002314092 :10E730001C112040D8F0001034F20010800231404B
:10E740000400000008000000800658403A01000064 :10E740000400000008000000800658403A01000064
:10E7500000093140020000000A0F0D07E00100002F :10E7500000093140020000000A0F0D07E00100002F
:10E76000FC53000098210000F4010000E8030000C1 :10E76000FC53000098210000F4010000E8030000C1
:10E77000F4010000C0F10010A0F20010800231404E :10E77000F401000064F1001044F200108002314006
:10E780000500000008000000000758403B010000A1 :10E780000500000008000000000758403B010000A1
:10E7900000053140010000000AF0D0707601000051 :10E7900000053140010000000AF0D0707601000051
:10E7A000E0150000D41700005E010000E80300003F :10E7A000E0150000D41700005E010000E80300003F
:10E7B000F4010000DCF10010B0F200100004314060 :10E7B000F401000080F1001054F200100004314018
:10E7C00000000000080000008009584040010000DF :10E7C00000000000080000008009584040010000DF
:10E7D0000007314000000000090F0D0795030000FD :10E7D0000007314000000000090F0D0795030000FD
:10E7E000901A000028230000F4010000E803000054 :10E7E000901A000028230000F4010000E803000054
:10E7F000F4010000F8F10010C0F200108000314078 :10E7F000F40100009CF1001064F200108000314030
:10E800000000000008000000000658403901000028 :10E800000000000008000000000658403901000028
:10E81000000631400500000009F0D07087020000BA :10E81000000631400500000009F0D07087020000BA
:10E82000201C000098210000F4010000D007000027 :10E82000201C000098210000F4010000D007000027
@@ -3729,169 +3729,169 @@
:10E8F00075FCECE7F41B0028281200280648006885 :10E8F00075FCECE7F41B0028281200280648006885
:10E9000070470020C04304490860044908600020A3 :10E9000070470020C04304490860044908600020A3
:10E91000034908607047000000182640041826408C :10E91000034908607047000000182640041826408C
:10E9200008102640443A5C6875616E672E787565FC :10E9200008102640F1B401684118436858080A18D5
:10E930007765695C4769745C436F676E6974697673 :10E9300000988068DB0701D54B46C01891422DD066
:10E94000655F4167656E745C2E636F6465785F74A4 :10E940000D78491C032403262E4002D10E78F61CB4
:10E950006D705C736561742D6669726D7761726547 :10E95000491C2B090F2B07D10B780F33491C03E0FF
:10E960002D6275696C642D3031363932303434396A :10E960000F78491C0770401C761EF9D1002BE5D0AA
:10E97000363564343762306139303435613463613F :10E970000E78491CAD082C40032C01D10C78491CA1
:10E98000646131636231385C636F6D6D6F6E5C73AF :10E9800024023419654244190025AE1E9E42D5D09A
:10E9900072635C647269766572735C6970635C63F0 :10E990002678641C0670401C6D1CF6E700980C304D
:10E9A000795F6970635F6472762E6800F1B4016804 :10E9A000F2BC7047443A5C50726F6A6563745C35C0
:10E9B0004118436858080A1800988068DB0701D599 :10E9B00031315C4E375765625C2E636F6465785FFA
:10E9C0004B46C01891422DD00D78491C03240326D4 :10E9C000746D705C736561742D6669726D776172C8
:10E9D0002E4002D10E78F61C491C2B090F2B07D1B3 :10E9D000652D6275696C642D326530363863373762
:10E9E0000B780F33491C03E00F78491C0770401C5B :10E9E00063306635346431386233393863383339EB
:10E9F000761EF9D1002BE5D00E78491CAD082C40CD :10E9F00063663830633463625C636F6D6D6F6E5C49
:10EA0000032C01D10C78491C2402341965424419A5 :10EA00007372635C647269766572735C6970635C6F
:10EA10000025AE1E9E42D5D02678641C0670401C90 :10EA100063795F6970635F6472762E68000000003E
:10EA20006D1CF6E700980C30F2BC704700053140D1 :10EA20000005314006000000000000008018904002
:10EA30000600000000000000801890400200000066 :10EA3000020000000006314001000000000000005C
:10EA40000006314001000000000000004019904025 :10EA4000401990400500000000053140070000001B
:10EA50000500000000053140070000000000000034 :10EA500000000000C0189040030000000005314095
:10EA6000C018904003000000000531400500000080 :10EA60000500000000000000401890400100000078
:10EA700000000000401890400100000000063140F6 :10EA700000063140020000000000000080199040B4
:10EA80000200000000000000801990400600000015 :10EA80000600000000063140060000000000000003
:10EA9000000631400600000000000000801A90408F :10EA9000801A90400A0000000400000000000000FE
:10EAA0000A00000004000000000000000000000058 :10EAA0000000000000000000010003031F4E0000F2
:10EAB00000000000010003031F4E000000000000E2 :10EAB0000000000000000000000000000000000056
:10EAC0000000000000000000000000000000000046 :10EAC0000000000000000000000000000000000046
:10EAD0000000000000000000000000000000000036 :10EAD0000000000000000000020300000000000031
:10EAE000000000000203000000000000030000001E :10EAE0000300000000000000030000000000000020
:10EAF0000000000003000000000000000300000010 :10EAF0000300000000000000030000000000000010
:10EB000000000000030000000000000003000000FF :10EB000003000000010000000102000004000000FA
:10EB100001000000010200000400000000000000ED :10EB100000000000000000000000000001000303EE
:10EB20000000000000000000010003031F4E000071 :10EB20001F4E00000000000000000000204E00000A
:10EB30000000000000000000204E00000000000067 :10EB300000000000000000000000000000000000D5
:10EB400000000000000000000000000000000000C5 :10EB400000000000000000000000000002030000C0
:10EB500000000000000000000203000000000000B0 :10EB500000000000030000000000000003000000AF
:10EB6000030000000000000003000000000000009F :10EB6000000000000300000000000000030000009F
:10EB7000030000000000000003000000000000008F :10EB7000000000000300000001000000010200008E
:10EB8000030000000100000001020000040000007A :10EB80000400000000000000000000000000000081
:10EB9000000000000000000000000000010003036E :10EB9000010003036300000000000000000000000B
:10EBA00063000000000000000000000032000000D0 :10EBA0003200000000000000000000000000000033
:10EBB0000000000000000000000000000000000055 :10EBB0000000000000000000000000000000000055
:10EBC0000000000000000000000000000203000040 :10EBC000020300000000000003000000000000003D
:10EBD000000000000300000000000000030000002F :10EBD000030000000000000003000000000000002F
:10EBE000000000000300000000000000030000001F :10EBE000030000000000000003000000010000001E
:10EBF000000000000300000001000000010200000E :10EBF0000102000072B6104810490860BFF34F8F41
:10EC000072B6104810490860BFF34F8F0120C0044E :10EC00000120C0040E490A6802430A600D490A68DF
:10EC10000E490A6802430A600D490A6802430A6005 :10EC100002430A600C490A6802430A60002000218E
:10EC20000C490A6802430A60002000210D4A03C211 :10EC20000D4A03C26A45FCD1084FB847084FB847A0
:10EC30006A45FCD1084FB847084FB847084FB84756 :10EC3000084FB847FEE700000000001008ED00E0B4
:10EC4000FEE700000000001008ED00E00013204087 :10EC40000013204080132040A0132040B9E300109F
:10EC500080132040A0132040B9E30010CDF0001035 :10EC500071F00010D5F1001000FF0128FFFF01281E
:10EC600031F2001000FF0128FFFF0128443A5C68E0 :10EC6000443A5C50726F6A6563745C3531315C4E56
:10EC700075616E672E7875657765695C4769745C48 :10EC7000375765625C6669726D776172655C743284
:10EC8000436F676E69746976655F4167656E745C32 :10EC8000675C747669696268386D5C7372635C7323
:10EC90006669726D776172655C7432675C747669FF :10EC9000797374656D5C7265765F645C73797374A7
:10ECA000696268386D5C7372635C73797374656DE7 :10ECA000656D5F747669696268386D5F636D307039
:10ECB0005C7265765F645C73797374656D5F74769E :10ECB0006C75732E630000009F0F000000000000C1
:10ECC00069696268386D5F636D30706C75732E634F :10ECC0000000000000000000000000000000030041
:10ECD00000000000443A5C6875616E672E787565C7 :10ECD0000000000000000000000000000003000031
:10ECE0007765695C4769745C436F676E69746976C0 :10ECE000000000000300000000000000030000001E
:10ECF000655F4167656E745C6669726D77617265A8 :10ECF000000000000300000000000000030000000E
:10ED00005C7432675C747669696268386D5C7372D2 :10ED00000100000000000000443A5C50726F6A6528
:10ED1000635C647269766572735C737973636C6B40 :10ED100063745C3531315C4E375765625C6669728D
:10ED20005C63795F737973636C6B2E68000000001D :10ED20006D776172655C7432675C7476696962687C
:10ED3000443A5C6875616E672E7875657765695CC5 :10ED3000386D5C7372635C647269766572735C7360
:10ED40004769745C436F676E69746976655F416794 :10ED40007973636C6B5C63795F737973636C6B2E3F
:10ED5000656E745C6669726D776172655C7432674A :10ED500068000000443A5C50726F6A6563745C3509
:10ED60005C747669696268386D5C7372635C647246 :10ED600031315C4E375765625C6669726D776172EE
:10ED700069766572735C737973636C6B5C63795FDE :10ED7000655C7432675C747669696268386D5C736F
:10ED8000737973636C6B2E6300000000443A5C6817 :10ED800072635C647269766572735C737973636CC9
:10ED900075616E672E7875657765695C4769745C27 :10ED90006B5C63795F737973636C6B2E6300000047
:10EDA000436F676E69746976655F4167656E745C11 :10EDA000443A5C50726F6A6563745C3531315C4E15
:10EDB0006669726D776172655C7432675C636F6DF2 :10EDB000375765625C6669726D776172655C743243
:10EDC0006D6F6E5C7372635C647269766572735C9E :10EDC000675C636F6D6D6F6E5C7372635C647269B8
:10EDD0007379737469636B5C63795F737973746956 :10EDD000766572735C7379737469636B5C63795F76
:10EDE000636B2E6300000000443A5C6875616E67D7 :10EDE0007379737469636B2E63000000443A5C505E
:10EDF0002E7875657765695C4769745C436F676EEB :10EDF000726F6A6563745C3531315C4E375765629A
:10EE000069746976655F4167656E745C6669726D89 :10EE00005C6669726D776172655C7432675C636FB2
:10EE1000776172655C7432675C636F6D6D6F6E5C99 :10EE10006D6D6F6E5C7372635C6D775C666C617355
:10EE20007372635C6D775C666C6173685C63795F59 :10EE2000685C63795F6D775F666C6173682E630001
:10EE30006D775F666C6173682E6300009F0F000042 :10EE300001000000000000000000000010000000C1
:10EE400000000000000000000000000000000000C2 :10EE400010000000100000000100000100000000A0
:10EE500000000300000000000000000000000000AF :10EE500000000000000000000000000000000000B2
:10EE6000000300000000000003000000000000009C :10EE600000000000000000000000000070B401235A
:10EE7000030000000000000003000000000000008C :10EE7000002413E00168001D194202D04D466918B4
:10EE8000030000000100000000000000010000007D :10EE8000491E0C60091D121F042AFAD20D009607B4
:10EE90000000000000000000100000001000000052 :10EE900001D50C80AD1C1A4000D02C700268001DFA
:10EEA0001000000001000001000000000000000050 :10EEA000002AE7D170BC7047000000000600000097
:10EEB0000000000000000000000000000000000052 :10EEB0000000000000000000000000000000000052
:10EEC000000000000000000070B40123002413E0E3 :10EEC0000000000000000000000000000000000042
:10EED0000168001D194202D04D466918491E0C6098 :10EED0000000000000000000000000000000000032
:10EEE000091D121F042AFAD20D00960701D50C80C5 :10EEE000060000000000000000000000000000001C
:10EEF000AD1C1A4000D02C700268001D002AE7D11A :10EEF0000000000000000000000000000000000012
:10EF000070BC704700000000060000000000000018 :10EF00000000000000000000000000000000000001
:10EF100000000000000000000000000000000000F1 :10EF100000000000080000001500000000000000D4
:10EF200000000000000000000000000000000000E1 :10EF200000000000000000000000000000000000E1
:10EF300000000000000000000000000006000000CB :10EF300000000000000000000000000000000000D1
:10EF400000000000000000000000000000000000C1 :10EF400000000000010000000E000000150000009D
:10EF500000000000000000000000000000000000B1 :10EF500000000000000000000000000000000000B1
:10EF600000000000000000000000000000000000A1 :10EF600000000000000000000000000000000000A1
:10EF70000800000015000000000000000000000074 :10EF7000000000000000000001000000060000008A
:10EF80000000000000000000000000000000000081 :10EF80000000000000000000000000000000000081
:10EF90000000000000000000000000000000000071 :10EF90000000000000000000000000000000000071
:10EFA000010000000E00000015000000000000003D :10EFA0000000000000000000000000000000000061
:10EFB0000000000000000000000000000000000051 :10EFB000060000000000000000000000000000004B
:10EFC0000000000000000000000000000000000041 :10EFC0000000000000000000000000000000000041
:10EFD000000000000100000006000000000000002A :10EFD0000000000000000000000000000000000031
:10EFE0000000000000000000000000000000000021 :10EFE0000200000000000000983A00000E01FA0044
:10EFF0000000000000000000000000000000000011 :10EFF000CC019001020000000300000000000000AE
:10F0000000000000000000000000000006000000FA :10F00000983A0000720182001C02CC01020000004C
:10F0100000000000000000000000000000000000F0 :10F01000050000000000000030750000F600190136
:10F0200000000000000000000000000000000000E0 :10F02000BD02EE010200000006000000000000002A
:10F0300000000000000000000000000002000000CE :10F030003075000087018C009501940102000000EA
:10F0400000000000983A00000E01FA00CC01900187 :10F0400001000000D8F20010E0EF00100200000004
:10F05000020000000300000000000000983A0000D9 :10F05000D4F300100800320002000000E0F20010BB
:10F06000720182001C02CC010200000005000000B9 :10F0600010F0001002000000D0F300100C0032007D
:10F070000000000030750000F6001901BD02EE012D :10F0700010B5074979441831064C7C44163404E035
:10F0800002000000060000000000000030750000D3 :10F08000081D0A68891888470100A142F8D110BDFF
:10F0900087018C009501940102000000010000002E :10F090000800000024000000D5FDFFFFE409000087
:10F0A00034F300103CF000100200000030F40010B7 :10F0A00008130028000000007DF8FFFF5E03000049
:10F0B00008003200020000003CF300106CF0001069 :10F0B0003A010000000800280000030100000000E1
:10F0C000020000002CF400100C00320010B50749BB :10F0C000010000000000000000000000010000003E
:10F0D00079441831064C7C44163404E0081D0A6853 :10F0D000FF0F0100000000004D4F544F525F494E9A
:10F0E000891888470100A142F8D110BD080000002E :10F0E000325F4842375F4842385F484F52495A4F73
:10F0F00024000000D5FDFFFFE409000008130028EC :10F0F0004E54414C0000000004207146084202D0EA
:10F1000000000000A9F8FFFF5E0300003A010000C4 :10F10000EFF3098001E0EFF3088000B5014988477B
:10F1100000080028000003010000000001000000BA :10F1100000BD00006F80001000F00EF8002801D044
:10F12000000000000000000001000000FF0F0100CF :10F12000FFF7A6FF00BF00BF002000BF00BFFFF732
:10F13000000000004D4F544F525F494E325F48422D :10F130008DFB00F003F800000020704780B500F060
:10F14000375F4842385F484F52495A4F4E54414CFE :10F1400001F801BD0746384600F002F8FBE7000071
:10F150000000000004207146084202D0EFF309804D :10F1500080B500BF00BF024A11001820ABBEFBE71C
:10F1600001E0EFF3088000B50149884700BD0000C9 :10F16000260002004D4F544F525F494E335F4842D4
:10F170006F80001000F00EF8002801D0FFF7A6FF06 :10F17000355F4842365F534541545F50414E000071
:10F1800000BF00BF002000BF00BFFFF75FFB00F023 :10F180004D4F544F525F494E345F4842335F4842BF
:10F1900003F800000020704780B500F001F801BDC1 :10F19000345F4241434B5245535400004D4F544F4E
:10F1A0000746384600F002F8FBE7000080B500BFD4 :10F1A000525F494E315F4842315F4842325F4C45C1
:10F1B00000BF024A11001820ABBEFBE72600020088 :10F1B000475245535400000004207146084201D0D4
:10F1C0004D4F544F525F494E335F4842355F48427E :10F1C000EFF30980EFF3088000B50149884700BDDF
:10F1D000365F534541545F50414E00004D4F544FF0 :10F1D00095F200100548014603B4684680F309889B
:10F1E000525F494E345F4842335F4842345F424188 :10F1E00000BF00BF00BF00BFFFF796FFA5EDF5FE13
:10F1F000434B5245535400004D4F544F525F494EBC :10F1F0009E029E0AAA02AA0ACE03CE0B2A042A0C59
:10F20000315F4842315F4842325F4C454752455377 :10F200000003000BF002F00A00127A000000000078
:10F210005400000004207146084201D0EFF3098039 :10F210000024F40000800000008000000080000056
:10F22000EFF3088000B50149884700BDF1F20010F6 :10F2200001000200040008001000200040008000DF
:10F230000548014603B4684680F3098800BF00BF53 :10F2300000010002484F52495A4F4E54414C5F481A
:10F2400000BF00BFFFF796FFA5EDF5FE9E029E0AE8 :10F24000414C4C00534541545F50414E5F48414C46
:10F25000AA02AA0ACE03CE0B2A042A0C0003000B32 :10F250004C0000004241434B524553545F48414CDF
:10F26000F002F00A00127A00000000000024F4000E :10F260004C0000004C4547524553545F48414C4CBC
:10F27000008000000080000000800000010002000B :10F2700000000000E02E0000401F0000E02E000013
:10F28000040008001000200040008000000100027F :10F2800030750000000300010004010302020002C7
:10F29000484F52495A4F4E54414C5F48414C4C00E4 :10F29000000300058169891E0978C9B201607047C1
:10F2A000534541545F50414E5F48414C4C00000073 :10F2A0000000000000010101010101000001000057
:10F2B0004241434B524553545F48414C4C0000007F :10F2B0000203040506080C10000000000000000016
:10F2C0004C4547524553545F48414C4C00000000A8 :10F2C00002000000010000000403000113140015F7
:10F2D000E02E0000401F0000E02E0000307500000E :10F2D000C7010300010000004001BE00F401AE01BF
:10F2E0000003000100040103020200020003000504 :10F2E00087018C009501940100080028FFFF012888
:10F2F0008169891E0978C9B2016070470000000069 :10F2F000000000000000000000000000000000000E
:10F3000000010101010101000001000002030405E8 :10F3000000000000000000000000000000000000FD
:10F3100006080C10000000000000000002000000C1 :10F3100000000000000000000000000000000000ED
:10F32000010000000403000113140015C7010300CD :10F3200000000000000000000000000000000000DD
:10F33000010000004001BE00F401AE0187018C0015 :10F3300000000000000000000000000000000000CD
:10F340009501940100080028FFFF0128000000003B :10F3400000000000000000000000000000000000BD
:10F3500000000000000000000000000000000000AD :10F3500000000000000000000000000000000000AD
:10F36000000000000000000000000000000000009D :10F36000000000000000000000000000000000009D
:10F37000000000000000000000000000000000008D :10F37000000000000000000000000000000000008D
@@ -3899,27 +3899,27 @@
:10F39000000000000000000000000000000000006D :10F39000000000000000000000000000000000006D
:10F3A000000000000000000000000000000000005D :10F3A000000000000000000000000000000000005D
:10F3B000000000000000000000000000000000004D :10F3B000000000000000000000000000000000004D
:10F3C000000000000000000000000000000000003D :10F3C0000000000000000000000000000001020337
:10F3D000000000000000000000000000000000002D :10F3D00032325A325A5A5A32000102035A5A5A32B7
:10F3E000000000000000000000000000000000001D :10F3E000FEE70000FEE70000FEE70000FEE7000089
:10F3F000000000000000000000000000000000000D :10F3F000FEE70000FEE70000FEE70000FEE7000079
:10F4000000000000000000000000000000000000FC :10F40000FEE70000FEE70000FEE7400455000200B2
:10F4100000000000000000000000000000000000EC :10F41000010001500325A40010000132DD0CA004FE
:10F4200000000000000000000001020332325A32E6 :10F420000900040101000112060E83F0F1286004B6
:10F430005A5A5A32000102035A5A5A32FEE7000061 :10F43000FFFFFF1F020225920807F0010352400060
:10F44000FEE70000FEE70000FEE70000FEE7000028 :10F4400001F2060218F2060418F2060618F206087F
:10F45000FEE70000FEE70000FEE70000FEE7000018 :10F4500018F306005478F10618F2060418F20606AE
:10F46000FEE70000FEE740045500020001000150E5 :10F4600018F2050818F002FFC9E5001004F1FF04C6
:10F470000325A40010000132DD0CA00409000401E2 :10F47000F1FF04F1FF04F1FF04F1FF04F1FF04F1D7
:10F4800001000112060E834CF2286004FFFFFF1FEB :10F48000FF04F147046C0248090028060AF321062C
:10F49000020225920807F0010352400001F2060221 :10F490000001F12334F2520868000A01010102035D
:10F4A00018F2060418F2060618F2060818F3060009 :10F4A0000203010F0F010100000000000000000036
:10F4B0005478F10618F2060418F2060618F2050848 :10F4B000000000000000000000000000000000004C
:10F4C00018F002FFC9E5001004F1FF04F1FF04F198 :10F4C000000000000000000000000000000000003C
:10F4D000FF04F1FF04F1FF04F1FF04F1FF04F14721 :10F4D000000000000000000000000000000000002C
:10F4E000046C0248090028060AF321060001F123F2 :10F4E000000000000000000000000000000000001C
:10F4F00034F2520868000A01010102030203010FFD :10F4F000000000000000000000000000000000000C
:10F500000F010100000000000000000000000000EA :10F5000000000000000000000000000000000000FB
:10F5100000000000000000000000000000000000EB :10F5100000000000000000000000000000000000EB
:10F5200000000000000000000000000000000000DB :10F5200000000000000000000000000000000000DB
:10F5300000000000000000000000000000000000CB :10F5300000000000000000000000000000000000CB
@@ -3937,11 +3937,6 @@
:10F5F000000000000000000000000000000000000B :10F5F000000000000000000000000000000000000B
:10F6000000000000000000000000000000000000FA :10F6000000000000000000000000000000000000FA
:10F6100000000000000000000000000000000000EA :10F6100000000000000000000000000000000000EA
:10F6200000000000000000000000000000000000DA :01F6200000E9
:10F6300000000000000000000000000000000000CA :040000051000F1D521
:10F6400000000000000000000000000000000000BA
:10F6500000000000000000000000000000000000AA
:10F66000000000000000000000000000000000009A
:0DF67000000000000000000000000000008D
:040000051000F231C4
:00000001FF :00000001FF
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,86 @@
"""Conservative, local defaults for adding named devices to an existing mode.
Only a complete *bare device list* uses these defaults. Explicit parameters and
actions stay in the normal parser; unrecognised list entries never disappear.
Defaults are proposals for a fully validated preview, not executable commands.
"""
from __future__ import annotations
import re
from copy import deepcopy
from typing import Any
_ADD_PREFIX = re.compile(
r"^(?:(?:请|帮我|帮忙|再|还|也|同时|顺便|继续|并且|并)\s*)*"
r"(?:加入|添加|增加|补充|加上|追加|新增)\s*"
)
_DEVICE_GROUPS = {
"空调": "climate", "冷气": "climate",
"氛围灯": "ambient_light", "气氛灯": "ambient_light",
"阅读灯": "reading", "读书灯": "reading",
"车窗": "window", "窗户": "window",
"遮阳帘": "roof", "天幕遮阳帘": "roof", "天窗遮阳帘": "roof",
"室内灯": "indoor", "顶灯": "indoor",
}
_LIST_SEPARATOR = re.compile(r"[、,,;;\s]+|以及|还有|和|与|及")
_EXPLICIT_ACTION = re.compile(
r"\d|[零一二三四五六七八九十百]+(?:度|档|挡|秒|分钟|%)|"
r"打开|开启|关闭|关掉|开到|关到|调整|设置|调到|调成|变成|改成|"
r"升高|降低|调高|调低|调亮|调暗|全开|全关|半开|一半|"
r"红色|蓝色|绿色|白色|暖色|冷色|亮度|温度|风量|内循环|外循环"
)
def strip_mode_addition_prefix(change_text: str) -> str:
"""Remove only the editing verb, not any device, value or negation."""
return _ADD_PREFIX.sub("", change_text.strip(), count=1)
def mode_device_additions(
change_text: str,
base_operations: list[dict[str, Any]],
default_operations: list[dict[str, Any]],
) -> list[dict[str, Any]] | None:
"""Return missing device defaults, None for non-list/explicit edits.
Existing groups are already configured and are left completely untouched,
including their target zones, power state and custom parameter values.
Reading and indoor lights are separate groups within the same domain.
"""
if not _ADD_PREFIX.match(change_text.strip()):
return None
body = strip_mode_addition_prefix(change_text).strip("。.!!吧 ")
if _EXPLICIT_ACTION.search(body):
return None
body = re.sub(r"(?:等)?(?:相关)?(?:设备)?(?:的)?(?:控制)?(?:动作|能力|联动)?$", "", body)
body = re.sub(r"等$", "", body)
devices = [item for item in _LIST_SEPARATOR.split(body) if item]
groups: list[str] = []
for device in devices:
device = re.sub(r"(?:设备)?(?:的)?(?:控制)?(?:动作|能力)$", "", device)
group = _DEVICE_GROUPS.get(device)
if group is None:
raise ValueError(f"未识别或暂不支持增补设备“{device}”;未修改任何模式动作。")
if group not in groups:
groups.append(group)
if not groups:
raise ValueError("请说明要加入哪些设备,或提供明确的设备动作。")
def belongs(operation: dict[str, Any], group: str) -> bool:
capability = str(operation.get("capability") or "")
if group == "reading":
return capability == "interior_light.set_reading_power"
if group == "indoor":
return capability == "interior_light.set_indoor_power"
return operation.get("domain") == group
additions: list[dict[str, Any]] = []
for group in groups:
if any(belongs(operation, group) for operation in base_operations):
continue
candidates = [operation for operation in default_operations if belongs(operation, group)]
if not candidates:
raise ValueError(f"该设备尚无经过登记的默认编排,请补充明确目标:{group}。")
additions.extend(deepcopy(candidates))
return additions
@@ -170,11 +170,12 @@ _MODE_PROFILE_DELETE_BA = re.compile(
) )
_MODE_PROFILE_UPDATE_NEGATION = re.compile( _MODE_PROFILE_UPDATE_NEGATION = re.compile(
r"不要|别(?:再)?|不用|无需|不必|禁止|不许|不得|" r"不要|别(?:再)?|不用|无需|不必|禁止|不许|不得|"
r"取消(?:修改|调整|更改|更新|变更|操作)|不执行|不下发|不保存" r"取消(?:修改|调整|更改|更新|变更|加入|添加|增加|补充|加上|追加|新增|操作)|"
r"撤销|算了|不执行|不下发|不保存"
) )
_MODE_PROFILE_UPDATE_PREFIX_NEGATION = re.compile( _MODE_PROFILE_UPDATE_PREFIX_NEGATION = re.compile(
r"^(?:请|麻烦)?(?:先|暂时)?(?:不要|别|不用|无需|不必|禁止|不许|不得)" r"^(?:请|麻烦)?(?:先|暂时)?(?:不要|别|不用|无需|不必|禁止|不许|不得)"
r"(?:把|将|修改|调整|更改|更新|变更)" r"(?:把|将|在|给|为|往|向|修改|调整|更改|更新|变更)"
) )
_MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)" _MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)"
_MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])" _MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])"
@@ -197,7 +198,19 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
rf"^{_LEADING_POLITE}{_QUOTED_MODE_NAME}[,,::]" rf"^{_LEADING_POLITE}{_QUOTED_MODE_NAME}[,,::]"
r"(?P<change>.+?)[。.!!]?$" r"(?P<change>.+?)[。.!!]?$"
) )
_MODE_PROFILE_APPEND_VERB = r"(?:加入|添加|增加|补充|加上|追加|新增)"
_APPEND_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:在|给|为|往|向)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?[,,::]?"
rf"(?:再|还|也|同时|另外)?(?P<change>{_MODE_PROFILE_APPEND_VERB}.+?)[。.!!]?$"
)
_APPEND_TO_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:把|将)(?P<change>.+?)"
rf"(?P<append_verb>{_MODE_PROFILE_APPEND_VERB})(?:到|进)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?(?:吧|呀|啊|哦|谢谢(?:你)?)?[。.!!]?$"
)
MODE_PROFILE_CHANGE_PREDICATE = re.compile( MODE_PROFILE_CHANGE_PREDICATE = re.compile(
rf"{_MODE_PROFILE_APPEND_VERB}|"
r"修改|改成|改为|更改|更新|变更|变成|变为|换成|" r"修改|改成|改为|更改|更新|变更|变成|变为|换成|"
r"调整|调节|调高|调低|调到|调成|调为|调至|" r"调整|调节|调高|调低|调到|调成|调为|调至|"
r"设置|设为|设成|设到|设至|" r"设置|设为|设成|设到|设至|"
@@ -208,7 +221,7 @@ MODE_PROFILE_CHANGE_PREDICATE = re.compile(
r"(?:重复|循环)(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)次" r"(?:重复|循环)(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)次"
) )
MODE_PROFILE_QUERY_CUE = re.compile( MODE_PROFILE_QUERY_CUE = re.compile(
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|" r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|能否|能不能|可不可以|会不会|"
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|" r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|" r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$" r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$"
@@ -610,6 +623,8 @@ def _candidate_matches(
) -> list[tuple[re.Match[str], bool]]: ) -> list[tuple[re.Match[str], bool]]:
matches: list[tuple[re.Match[str], bool]] = [] matches: list[tuple[re.Match[str], bool]] = []
for pattern, predicate_required in ( for pattern, predicate_required in (
(_APPEND_MODE_UPDATE, True),
(_APPEND_TO_MODE_UPDATE, False),
(_PREFIX_UPDATE, False), (_PREFIX_UPDATE, False),
(_BA_UPDATE, True), (_BA_UPDATE, True),
(_NAME_FIRST_UPDATE, True), (_NAME_FIRST_UPDATE, True),
@@ -629,6 +644,14 @@ def _candidate_matches(
return matches return matches
def _mode_profile_update_change(match: re.Match[str], name: str) -> str:
change = _strip_repeated_mode_update_subject(str(match.group("change") or ""), name)
append_verb = str(match.groupdict().get("append_verb") or "")
# Keep reverse-order additions on the same service contract as name-first
# additions. Device names, explicit targets and parameters remain unchanged.
return f"{append_verb}{change}" if append_verb else change
def _strip_repeated_mode_update_subject(change: str, mode_name: str) -> str: def _strip_repeated_mode_update_subject(change: str, mode_name: str) -> str:
"""Remove a repeated ``把<same mode>的`` wrapper from an update body.""" """Remove a repeated ``把<same mode>的`` wrapper from an update body."""
@@ -708,10 +731,7 @@ def is_mode_profile_update_shape(
and _is_unquoted_device_property_mode(match, name) and _is_unquoted_device_property_mode(match, name)
): ):
continue continue
change = _strip_repeated_mode_update_subject( change = _mode_profile_update_change(match, name)
str(match.group("change") or ""),
name,
)
if change and ( if change and (
not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change) not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change)
): ):
@@ -746,10 +766,9 @@ def parse_mode_profile_update(
and _is_unquoted_device_property_mode(match, name) and _is_unquoted_device_property_mode(match, name)
): ):
continue continue
change = _strip_repeated_mode_update_subject( change = _mode_profile_update_change(match, name)
str(match.group("change") or ""), if re.fullmatch(rf"(?:再|还|也|同时|另外)?{_MODE_PROFILE_APPEND_VERB}(?:一下)?", change):
name, continue
)
if ( if (
(not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change)) (not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change))
and not _MODE_PROFILE_UPDATE_NEGATION.search(change) and not _MODE_PROFILE_UPDATE_NEGATION.search(change)
+308
View File
@@ -0,0 +1,308 @@
"""Read-only HTTP surface for vehicle firmware discovery and download."""
from __future__ import annotations
import argparse
import json
import mimetypes
import os
import re
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import unquote, urlparse
from cognitive_agent.config_loader import load_local_env
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
load_local_env(REPOSITORY_ROOT / ".env", override=True)
RELEASE_ID_PATTERN = re.compile(r"[a-z0-9][a-z0-9-]{7,80}")
DOWNLOAD_PATH_PATTERN = re.compile(
r"/api/firmware-workbench/releases/(?P<release_id>[a-z0-9-]+)/download"
)
LATEST_PATHS = frozenset(
{
"/latest",
"/api/firmware-workbench/releases/latest",
}
)
RELEASES_PATHS = frozenset(
{
"/releases",
"/api/firmware-workbench/releases",
}
)
class FirmwareOtaRepository:
"""Resolve atomically published OTA manifests without loading workbench state."""
def __init__(self, root: Path, public_base_url: str) -> None:
self.root = Path(root).resolve()
self.public_base_url = str(public_base_url).rstrip("/")
def latest(self) -> dict[str, object]:
manifest_path = self.root / "latest.json"
manifest = self._read_manifest(manifest_path)
return self._public_manifest(manifest)
def releases(self) -> dict[str, object]:
releases_root = self.root / "releases"
manifests: list[dict[str, object]] = []
if releases_root.is_dir():
for release_root in releases_root.iterdir():
if not release_root.is_dir():
continue
try:
manifest = self._read_manifest(release_root / "release.json")
release_id = self._release_id(manifest)
if release_id != release_root.name:
continue
package, _, _ = self.release_package(release_id)
expected_size = manifest.get("size_bytes")
if (
expected_size is not None
and int(expected_size) != package.stat().st_size
):
continue
if str(manifest.get("status") or "published") != "published":
continue
manifests.append(self._public_manifest(manifest))
except (FileNotFoundError, OSError, TypeError, ValueError):
continue
manifests.sort(
key=lambda item: str(item.get("published_at") or ""), reverse=True
)
latest_release_id = (
str(manifests[0]["release_id"]) if manifests else None
)
return {
"schema": "n7-ota-release-list/v1",
"count": len(manifests),
"latest_release_id": latest_release_id,
"releases": manifests,
}
def release_package(self, release_id: str) -> tuple[Path, str, str]:
checked = str(release_id).strip()
if not RELEASE_ID_PATTERN.fullmatch(checked):
raise FileNotFoundError("release not found")
release_root = (self.root / "releases" / checked).resolve()
try:
release_root.relative_to((self.root / "releases").resolve())
except ValueError as exc:
raise FileNotFoundError("release not found") from exc
manifest = self._read_manifest(release_root / "release.json")
if self._release_id(manifest) != checked:
raise FileNotFoundError("release not found")
filename = str(manifest.get("firmware_filename") or "")
if not filename or Path(filename).name != filename:
raise FileNotFoundError("firmware package not found")
package = (release_root / filename).resolve()
try:
package.relative_to(release_root)
except ValueError as exc:
raise FileNotFoundError("firmware package not found") from exc
if not package.is_file():
raise FileNotFoundError("firmware package not found")
expected_hash = str(manifest.get("sha256") or "").lower()
return package, filename, expected_hash
def _public_manifest(self, manifest: dict[str, object]) -> dict[str, object]:
payload = dict(manifest)
release_id = self._release_id(payload)
payload["schema"] = "n7-ota-release/v1"
payload["release_id"] = release_id
payload["download_url"] = f"{self.public_base_url}/{release_id}/download"
payload.pop("package_path", None)
return payload
@staticmethod
def _read_manifest(path: Path) -> dict[str, object]:
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError) as exc:
raise FileNotFoundError("manifest not found") from exc
if not isinstance(document, dict):
raise FileNotFoundError("manifest not found")
return document
@staticmethod
def _release_id(manifest: dict[str, object]) -> str:
release_id = str(manifest.get("release_id") or manifest.get("id") or "")
if not RELEASE_ID_PATTERN.fullmatch(release_id):
raise FileNotFoundError("release not found")
return release_id
class FirmwareOtaHandler(BaseHTTPRequestHandler):
"""Expose health, release manifests, and immutable firmware packages."""
repository: FirmwareOtaRepository
server_version = "N7FirmwareOta/1.0"
def do_GET(self) -> None: # noqa: N802 - stdlib handler API
self._handle_request(send_body=True)
def do_HEAD(self) -> None: # noqa: N802 - stdlib handler API
self._handle_request(send_body=False)
def _handle_request(self, *, send_body: bool) -> None:
path = unquote(urlparse(self.path).path)
if path == "/healthz":
self._json({"status": "ok", "service": "n7-firmware-ota"}, send_body)
return
if path in LATEST_PATHS:
try:
self._json(self.repository.latest(), send_body)
except FileNotFoundError:
self._json(
{"error": "not_found", "message": "暂无可供车机升级的固件。"},
send_body,
HTTPStatus.NOT_FOUND,
)
return
if path in RELEASES_PATHS:
self._json(self.repository.releases(), send_body)
return
match = DOWNLOAD_PATH_PATTERN.fullmatch(path)
if match:
try:
package, filename, expected_hash = self.repository.release_package(
match.group("release_id")
)
self._file(package, filename, expected_hash, send_body)
except FileNotFoundError:
self._json(
{"error": "not_found", "message": "OTA 固件包不存在。"},
send_body,
HTTPStatus.NOT_FOUND,
)
return
self._json(
{"error": "not_found", "message": "接口不存在。"},
send_body,
HTTPStatus.NOT_FOUND,
)
def _file(
self,
path: Path,
filename: str,
expected_hash: str,
send_body: bool,
) -> None:
size = path.stat().st_size
try:
start, end, partial = self._byte_range(size)
except ValueError:
self.send_response(HTTPStatus.REQUESTED_RANGE_NOT_SATISFIABLE)
self.send_header("Content-Range", f"bytes */{size}")
self.send_header("Content-Length", "0")
self._security_headers()
self.end_headers()
return
content_length = end - start + 1
self.send_response(HTTPStatus.PARTIAL_CONTENT if partial else HTTPStatus.OK)
self.send_header(
"Content-Type", mimetypes.guess_type(filename)[0] or "application/octet-stream"
)
self.send_header("Content-Length", str(content_length))
self.send_header("Content-Disposition", f'attachment; filename="{filename}"')
self.send_header("Accept-Ranges", "bytes")
self.send_header("Cache-Control", "public, max-age=31536000, immutable")
if expected_hash:
self.send_header("X-Firmware-SHA256", expected_hash)
if partial:
self.send_header("Content-Range", f"bytes {start}-{end}/{size}")
self._security_headers()
self.end_headers()
if not send_body:
return
with path.open("rb") as source:
source.seek(start)
remaining = content_length
while remaining:
chunk = source.read(min(64 * 1024, remaining))
if not chunk:
break
self.wfile.write(chunk)
remaining -= len(chunk)
def _byte_range(self, size: int) -> tuple[int, int, bool]:
value = str(self.headers.get("Range") or "").strip()
if not value:
return 0, size - 1, False
match = re.fullmatch(r"bytes=(\d*)-(\d*)", value)
if not match or not any(match.groups()):
raise ValueError("invalid byte range")
first, last = match.groups()
if first:
start = int(first)
end = int(last) if last else size - 1
else:
suffix = int(last)
if suffix <= 0:
raise ValueError("invalid byte range")
start = max(0, size - suffix)
end = size - 1
if start >= size or end < start:
raise ValueError("invalid byte range")
return start, min(end, size - 1), True
def _json(
self,
payload: object,
send_body: bool,
status: HTTPStatus = HTTPStatus.OK,
) -> None:
content = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-store")
self._security_headers()
self.end_headers()
if send_body:
self.wfile.write(content)
def _security_headers(self) -> None:
self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
def main() -> None:
parser = argparse.ArgumentParser(description="Run the read-only vehicle OTA service")
parser.add_argument(
"--host", default=os.getenv("FIRMWARE_OTA_BIND_HOST", "127.0.0.1")
)
parser.add_argument(
"--port", type=int, default=int(os.getenv("FIRMWARE_OTA_PORT", "8012"))
)
args = parser.parse_args()
configured_root = os.getenv("FIRMWARE_WORKBENCH_OTA_ROOT", "").strip()
ota_root = (
Path(configured_root)
if configured_root
else REPOSITORY_ROOT / "artifacts" / "server" / "firmware-ota"
)
public_base_url = os.getenv(
"FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL",
f"http://{args.host}:{args.port}/api/firmware-workbench/releases",
)
FirmwareOtaHandler.repository = FirmwareOtaRepository(ota_root, public_base_url)
server = ThreadingHTTPServer((args.host, args.port), FirmwareOtaHandler)
print(f"N7 firmware OTA service: http://{args.host}:{args.port}", flush=True)
try:
server.serve_forever()
except KeyboardInterrupt:
pass
finally:
server.server_close()
if __name__ == "__main__":
main()
+167 -3
View File
@@ -43,6 +43,10 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402
CodexAssistantService, CodexAssistantService,
CodexVehicleToolError, CodexVehicleToolError,
) )
from cognitive_agent.application.firmware_workbench import ( # noqa: E402
FirmwareWorkbenchError,
FirmwareWorkbenchService,
)
from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402 from cognitive_agent.application.gesture_bridge import GestureCapabilityBridge # noqa: E402
from cognitive_agent.application.mode_profile_intent import ( # noqa: E402 from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
normalize_mode_profile_name, normalize_mode_profile_name,
@@ -103,7 +107,11 @@ PUBLIC_AUTH_PATHS = {
"/network.js", "/network.js",
"/api/auth/status", "/api/auth/status",
"/api/auth/login", "/api/auth/login",
"/api/firmware-workbench/releases/latest",
} }
PUBLIC_OTA_DOWNLOAD_PATH = re.compile(
r"/api/firmware-workbench/releases/[a-z0-9-]+/download"
)
NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"}) NON_DISPATCH_DISPLAY_SOURCES = frozenset({"mode_editor_compile"})
@@ -207,6 +215,7 @@ def _lan_ipv4_address() -> str | None:
class SeatDemoHandler(BaseHTTPRequestHandler): class SeatDemoHandler(BaseHTTPRequestHandler):
service = SeatDemoService() service = SeatDemoService()
firmware_workbench = FirmwareWorkbenchService(REPOSITORY_ROOT)
# Codex defaults to its own empty temporary workspace. A deployment may # Codex defaults to its own empty temporary workspace. A deployment may
# opt into another isolated directory through CODEX_ASSISTANT_WORKSPACE, # opt into another isolated directory through CODEX_ASSISTANT_WORKSPACE,
# but it must never inherit access to this application's source tree. # but it must never inherit access to this application's source tree.
@@ -317,6 +326,63 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
if path == "/api/assistant/capabilities": if path == "/api/assistant/capabilities":
self._json(self._assistant_capability_catalog().snapshot()) self._json(self._assistant_capability_catalog().snapshot())
return return
if path == "/api/firmware-workbench/state":
self._json(self.firmware_workbench.snapshot())
return
if path == "/api/firmware-workbench/releases/latest":
try:
self._json(self.firmware_workbench.latest_release())
except FirmwareWorkbenchError as exc:
self._json(
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
)
return
firmware_task_match = re.fullmatch(
r"/api/firmware-workbench/tasks/(?P<task_id>[a-z0-9-]+)", path
)
if firmware_task_match:
try:
self._json(
self.firmware_workbench.task(firmware_task_match.group("task_id"))
)
except FirmwareWorkbenchError as exc:
self._json(
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
)
return
firmware_artifact_download_match = re.fullmatch(
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)/download",
path,
)
if firmware_artifact_download_match:
self._serve_firmware_download(
"artifact", firmware_artifact_download_match.group("artifact_id")
)
return
firmware_release_download_match = re.fullmatch(
r"/api/firmware-workbench/releases/(?P<release_id>[a-z0-9-]+)/download",
path,
)
if firmware_release_download_match:
self._serve_firmware_download(
"release", firmware_release_download_match.group("release_id")
)
return
firmware_artifact_match = re.fullmatch(
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)", path
)
if firmware_artifact_match:
try:
self._json(
self.firmware_workbench.artifact(
firmware_artifact_match.group("artifact_id")
)
)
except FirmwareWorkbenchError as exc:
self._json(
{"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status)
)
return
if path == "/api/runtime": if path == "/api/runtime":
self._json(self.service.runtime_info()) self._json(self.service.runtime_info())
return return
@@ -510,6 +576,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
return return
if not self._require_json_content_type(): if not self._require_json_content_type():
return return
if path == "/api/firmware-workbench/tasks" or path.startswith(
"/api/firmware-workbench/"
):
self._handle_firmware_workbench_post(path)
return
if path in { if path in {
"/api/hardware/access/request", "/api/hardware/access/request",
"/api/hardware/access/release", "/api/hardware/access/release",
@@ -2529,6 +2600,24 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
HTTPStatus.BAD_REQUEST, HTTPStatus.BAD_REQUEST,
) )
return return
firmware_artifact_match = re.fullmatch(
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)",
path,
)
if firmware_artifact_match is not None:
try:
self._json(
self.firmware_workbench.delete_artifact(
firmware_artifact_match.group("artifact_id")
),
HTTPStatus.OK,
)
except FirmwareWorkbenchError as exc:
self._json(
{"error": exc.code, "message": str(exc)},
HTTPStatus(exc.status),
)
return
prefix = "/api/mode-profiles/" prefix = "/api/mode-profiles/"
if not path.startswith(prefix): if not path.startswith(prefix):
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
@@ -3259,6 +3348,70 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
raise ValueError("request body must be a JSON object") raise ValueError("request body must be a JSON object")
return payload return payload
def _handle_firmware_workbench_post(self, path: str) -> None:
try:
payload = self._read_json_body(max_bytes=16 * 1024)
if path == "/api/firmware-workbench/tasks":
result = self.firmware_workbench.start_task(
str(payload.get("instruction", "")),
str(payload.get("baseline_id", "time_only")),
)
self._json(result, HTTPStatus.ACCEPTED)
return
task_action_match = re.fullmatch(
r"/api/firmware-workbench/tasks/(?P<task_id>[a-z0-9-]+)/(?P<action>cancel|retry)",
path,
)
if task_action_match:
task_id = task_action_match.group("task_id")
if task_action_match.group("action") == "cancel":
self._json(self.firmware_workbench.cancel_task(task_id))
else:
self._json(
self.firmware_workbench.retry_task(task_id), HTTPStatus.ACCEPTED
)
return
artifact_publish_match = re.fullmatch(
r"/api/firmware-workbench/artifacts/(?P<artifact_id>[a-z0-9-]+)/publish",
path,
)
if artifact_publish_match:
self._json(
self.firmware_workbench.publish_artifact(
artifact_publish_match.group("artifact_id")
)
)
return
self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND)
except (ValueError, json.JSONDecodeError) as exc:
self._json(
{"error": "invalid_request", "message": str(exc)},
HTTPStatus.BAD_REQUEST,
)
except FirmwareWorkbenchError as exc:
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
def _serve_firmware_download(self, kind: str, item_id: str) -> None:
try:
if kind == "release":
path, filename = self.firmware_workbench.resolve_release_download(item_id)
else:
path, filename = self.firmware_workbench.resolve_artifact_download(item_id)
content = path.read_bytes()
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/zip")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "private, no-store")
self.send_header(
"Content-Disposition",
f"attachment; filename*=UTF-8''{quote(filename, safe='.-_')}",
)
self._send_security_headers()
self.end_headers()
self.wfile.write(content)
except FirmwareWorkbenchError as exc:
self._json({"error": exc.code, "message": str(exc)}, HTTPStatus(exc.status))
def _require_json_content_type(self) -> bool: def _require_json_content_type(self) -> bool:
if is_json_content_type(self.headers.get("Content-Type", "")): if is_json_content_type(self.headers.get("Content-Type", "")):
return True return True
@@ -3505,7 +3658,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
pass pass
def _authorize(self, path: str) -> bool: def _authorize(self, path: str) -> bool:
if not self.auth.config.enabled or path in PUBLIC_AUTH_PATHS: if (
not self.auth.config.enabled
or path in PUBLIC_AUTH_PATHS
or PUBLIC_OTA_DOWNLOAD_PATH.fullmatch(path)
):
return True return True
if self._session_username() is not None: if self._session_username() is not None:
return True return True
@@ -3780,8 +3937,13 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
def _send_security_headers(self) -> None: def _send_security_headers(self) -> None:
allow_same_origin_frame = (
unquote(urlparse(self.path).path) == "/firmware-workbench.html"
)
self.send_header("X-Content-Type-Options", "nosniff") self.send_header("X-Content-Type-Options", "nosniff")
self.send_header("X-Frame-Options", "DENY") self.send_header(
"X-Frame-Options", "SAMEORIGIN" if allow_same_origin_frame else "DENY"
)
self.send_header("Referrer-Policy", "no-referrer") self.send_header("Referrer-Policy", "no-referrer")
self.send_header( self.send_header(
"Permissions-Policy", "Permissions-Policy",
@@ -3796,7 +3958,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
"media-src 'self' blob:; " "media-src 'self' blob:; "
"frame-src 'self' http://*:3001; " "frame-src 'self' http://*:3001; "
"connect-src 'self' blob: http://172.20.10.6:8889; " "connect-src 'self' blob: http://172.20.10.6:8889; "
"object-src 'none'; base-uri 'none'; frame-ancestors 'none'", "object-src 'none'; base-uri 'none'; frame-ancestors "
+ ("'self'" if allow_same_origin_frame else "'none'"),
) )
if self._is_https(): if self._is_https():
self.send_header( self.send_header(
@@ -3893,6 +4056,7 @@ def main() -> None:
SeatDemoHandler.nfc.close() SeatDemoHandler.nfc.close()
SeatDemoHandler.calibration.close() SeatDemoHandler.calibration.close()
SeatDemoHandler.repair.close() SeatDemoHandler.repair.close()
SeatDemoHandler.firmware_workbench.close()
if __name__ == "__main__": if __name__ == "__main__":
+59
View File
@@ -75,6 +75,65 @@ async function main() {
await page.locator('[data-execution-mode="dry_run"]').click(); await page.locator('[data-execution-mode="dry_run"]').click();
assert.equal(await page.evaluate(() => window.CognitiveAgentConsole.getExecutionMode()), "dry_run"); assert.equal(await page.evaluate(() => window.CognitiveAgentConsole.getExecutionMode()), "dry_run");
async function submitDraft(text) {
const responsePromise = responseFor("/api/assistant/stream");
await page.locator("#utterance").fill(text);
await page.locator("#utterance").press("Enter");
const response = await responsePromise;
const body = responses.filter((item) => item.pathname === "/api/assistant/stream").at(-1).text;
assert.equal(response.status(), 200, `${text}: ${body}`);
const events = body.trim().split("\n").map((line) => JSON.parse(line));
await waitIdle();
const payload = events.find((event) => event.payload?.mode_profile_draft)?.payload;
assert.ok(payload, `Missing draft for ${text}: ${body}`);
assert.equal(payload.status, "succeeded");
assert.equal(payload.execution_mode, "dry_run");
const draft = payload.mode_profile_draft;
const card = page.locator(`[data-assistant-mode-draft="${draft.source_request_id}"]`);
await card.waitFor({ state: "visible" });
assert.equal(await card.locator(".assistant-mode-draft-row").count(), draft.operation_count);
return { payload, draft, card, request: posted.filter((item) => item.pathname === "/api/assistant/stream").at(-1).payload };
}
// The original failing two-turn request must revise the server-owned
// draft, not attempt immediate hardware execution or replace its seats.
const recline = await submitDraft("构建一个躺平模式");
const originalActions = recline.payload.actions;
await recline.card.scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(evidenceDir, "mode-device-addition-before.png") });
const expanded = await submitDraft("在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作");
assert.deepEqual(expanded.request.mode_profile_draft_context, {
name: recline.draft.name,
source_request_id: recline.draft.source_request_id,
vehicle_id: recline.draft.vehicle_id,
});
assert.notEqual(expanded.draft.source_request_id, recline.draft.source_request_id);
assert.equal(expanded.draft.draft_revision, 2);
const actionKey = (action) => `${action.capability}:${action.target?.instance || action.target?.zone || ""}`;
const expandedActions = new Map(expanded.payload.actions.map((action) => [actionKey(action), action]));
for (const original of originalActions) {
assert.deepEqual(expandedActions.get(actionKey(original))?.parameters, original.parameters,
"adding devices must preserve all existing action values");
}
for (const capability of ["climate.set_temperature", "interior_light.set_reading_power", "window.set_position"]) {
assert.ok(expanded.payload.actions.some((action) => action.capability === capability), capability);
}
assert.ok(expanded.payload.actions.some((action) => action.domain === "ambient_light"));
assert.ok(expanded.payload.actions.every((action) => action.domain !== "seat"
|| (action.target?.instance || action.target?.zone) === "passenger"));
assert.ok(expanded.draft.operation_count > recline.draft.operation_count);
assert.equal(await recline.card.getAttribute("data-superseded"), "true");
assert.equal(await recline.card.locator(".assistant-mode-draft-save").isDisabled(), true);
assert.equal(posted.filter((item) => item.pathname === "/api/mode-profiles").length, 0);
await expanded.card.scrollIntoViewIfNeeded();
await page.screenshot({ path: path.join(evidenceDir, "mode-device-addition-after.png") });
const temperatureEdit = await submitDraft("把躺平模式的空调温度调整到26度");
assert.equal(temperatureEdit.request.mode_profile_draft_context.source_request_id, expanded.draft.source_request_id);
assert.equal(temperatureEdit.draft.draft_revision, 3);
assert.equal(temperatureEdit.draft.operation_count, expanded.draft.operation_count);
assert.ok(temperatureEdit.payload.actions.some((action) => action.capability === "climate.set_temperature"
&& action.parameters.temperature_c === 26));
// Exercise the same real assistant endpoint and rendered cards for both // Exercise the same real assistant endpoint and rendered cards for both
// implicit passenger selection and an explicit driver request. These // implicit passenger selection and an explicit driver request. These
// previews stay unsaved, so the timeline save/trial tests remain isolated. // previews stay unsaved, so the timeline save/trial tests remain isolated.
+112
View File
@@ -0,0 +1,112 @@
from __future__ import annotations
import hashlib
import json
import threading
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
from cognitive_agent.firmware_ota_server import FirmwareOtaHandler, FirmwareOtaRepository
class TestFirmwareOtaServer:
def test_latest_download_head_and_resume_are_read_only(self, tmp_path: Path) -> None:
release_id = "fw-rel-123456789abc"
filename = "PassengerSeat_test.zip"
package = (b"vehicle-firmware-package" * 128) + b"end"
release_root = tmp_path / "releases" / release_id
release_root.mkdir(parents=True)
package_path = release_root / filename
package_path.write_bytes(package)
manifest = {
"schema": "n7-ota-release/v1",
"release_id": release_id,
"firmware_filename": filename,
"size_bytes": len(package),
"sha256": hashlib.sha256(package).hexdigest(),
"capabilities": ["时间控制", "座椅加热"],
"published_at": "2026-09-16T13:17:17+08:00",
"status": "published",
"download_url": "http://stale-address.invalid/download",
}
(release_root / "release.json").write_text(
json.dumps(manifest, ensure_ascii=False), encoding="utf-8"
)
(tmp_path / "latest.json").write_text(
json.dumps(manifest, ensure_ascii=False), encoding="utf-8"
)
older_release_id = "fw-rel-000000000001"
older_root = tmp_path / "releases" / older_release_id
older_root.mkdir(parents=True)
older_filename = "PassengerSeat_older.zip"
older_package = b"older-firmware-package"
(older_root / older_filename).write_bytes(older_package)
older_manifest = {
**manifest,
"release_id": older_release_id,
"firmware_filename": older_filename,
"size_bytes": len(older_package),
"sha256": hashlib.sha256(older_package).hexdigest(),
"published_at": "2026-09-15T13:17:17+08:00",
}
(older_root / "release.json").write_text(
json.dumps(older_manifest, ensure_ascii=False), encoding="utf-8"
)
class Handler(FirmwareOtaHandler):
repository = FirmwareOtaRepository(
tmp_path, "https://n7-ota.example.ts.net/api/firmware-workbench/releases"
)
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
base_url = f"http://127.0.0.1:{server.server_port}"
try:
with urllib.request.urlopen(
f"{base_url}/api/firmware-workbench/releases/latest", timeout=3
) as response:
latest = json.loads(response.read().decode("utf-8"))
assert latest["download_url"] == (
f"https://n7-ota.example.ts.net/api/firmware-workbench/releases/"
f"{release_id}/download"
)
with urllib.request.urlopen(
f"{base_url}/api/firmware-workbench/releases", timeout=3
) as response:
releases = json.loads(response.read().decode("utf-8"))
assert releases["schema"] == "n7-ota-release-list/v1"
assert releases["count"] == 2
assert releases["latest_release_id"] == release_id
assert [item["release_id"] for item in releases["releases"]] == [
release_id,
older_release_id,
]
assert releases["releases"][1]["download_url"].endswith(
f"/{older_release_id}/download"
)
request = urllib.request.Request(
f"{base_url}/api/firmware-workbench/releases/{release_id}/download",
headers={"Range": "bytes=8-31"},
)
with urllib.request.urlopen(request, timeout=3) as response:
assert response.status == 206
assert response.headers["Content-Range"] == f"bytes 8-31/{len(package)}"
assert response.headers["X-Firmware-SHA256"] == manifest["sha256"]
assert response.read() == package[8:32]
head = urllib.request.Request(
f"{base_url}/api/firmware-workbench/releases/{release_id}/download",
method="HEAD",
)
with urllib.request.urlopen(head, timeout=3) as response:
assert response.status == 200
assert int(response.headers["Content-Length"]) == len(package)
assert response.read() == b""
finally:
server.shutdown()
server.server_close()
thread.join(timeout=3)
+167
View File
@@ -0,0 +1,167 @@
from __future__ import annotations
import json
import tempfile
import threading
import time
import urllib.request
from http.server import ThreadingHTTPServer
from pathlib import Path
from cognitive_agent.application.firmware_workbench import FirmwareWorkbenchService
from cognitive_agent.auth import AuthConfig, AuthRuntime
from cognitive_agent.server import SeatDemoHandler
ROOT = Path(__file__).resolve().parents[2]
class TestFirmwareWorkbenchApi:
@classmethod
def setup_class(cls) -> None:
cls.temporary_directory = tempfile.TemporaryDirectory()
root = Path(cls.temporary_directory.name)
image = root / "Seat_Controller_demo.hex"
image.write_bytes(b":020000040000FA\n" + b":100000000102030405060708090A0B0C0D0E0F1068\n" * 80)
def build_runner(task_id: str, log: object, cancel: threading.Event) -> Path:
log("info", f"IAR task {task_id}: 0 errors, 0 warnings")
return image
cls.workbench = FirmwareWorkbenchService(
ROOT,
artifact_root=root / "workbench",
ota_root=root / "ota",
build_runner=build_runner,
)
cls.original_workbench = SeatDemoHandler.firmware_workbench
cls.original_auth = SeatDemoHandler.auth
SeatDemoHandler.firmware_workbench = cls.workbench
SeatDemoHandler.auth = AuthRuntime(
AuthConfig(
enabled=False,
username="",
password_hash="",
session_secret=b"t" * 64,
session_ttl_seconds=3600,
login_attempts=5,
login_window_seconds=300,
requests_per_minute=120,
trust_proxy_headers=False,
)
)
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), SeatDemoHandler)
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
cls.thread.start()
cls.base_url = f"http://127.0.0.1:{cls.server.server_port}"
@classmethod
def teardown_class(cls) -> None:
cls.server.shutdown()
cls.server.server_close()
cls.thread.join(timeout=2)
cls.workbench.close()
SeatDemoHandler.firmware_workbench = cls.original_workbench
SeatDemoHandler.auth = cls.original_auth
cls.temporary_directory.cleanup()
@classmethod
def post(cls, path: str, payload: dict[str, object]) -> tuple[int, dict[str, object]]:
request = urllib.request.Request(
f"{cls.base_url}{path}",
method="POST",
headers={"Content-Type": "application/json"},
data=json.dumps(payload, ensure_ascii=False).encode(),
)
with urllib.request.urlopen(request, timeout=5) as response:
return response.status, json.loads(response.read().decode())
def test_real_http_task_publish_detail_and_download_flow(self) -> None:
instruction = "增加位置控制、座椅通风和座椅加热功能"
status, created = self.post(
"/api/firmware-workbench/tasks", {"instruction": instruction}
)
assert status == 202
task_id = str(created["task"]["id"])
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/tasks/{task_id}", timeout=5
) as response:
task = json.loads(response.read().decode())
if task["status"] not in {"waiting", "running", "pending_confirmation"}:
break
time.sleep(0.03)
assert task["status"] == "success"
assert task["message"] == "已发布,待车机端下载升级"
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/releases/latest", timeout=5
) as response:
latest = json.loads(response.read().decode())
assert latest["release_id"] == task["release_id"]
assert latest["download_url"].endswith(f"/{task['release_id']}/download")
repeated_status, repeated = self.post(
"/api/firmware-workbench/tasks", {"instruction": instruction}
)
assert repeated_status == 202
assert repeated["task"]["id"] != task_id
repeated_task_id = str(repeated["task"]["id"])
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/tasks/{repeated_task_id}", timeout=5
) as response:
repeated_task = json.loads(response.read().decode())
if repeated_task["status"] not in {"waiting", "running", "pending_confirmation"}:
break
time.sleep(0.03)
assert repeated_task["status"] == "success"
artifact_id = task["artifact_id"]
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}", timeout=5
) as response:
artifact = json.loads(response.read().decode())
assert artifact["status"] == "published"
assert artifact["sha256"]
release_id = task["release_id"]
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/releases/{release_id}/download",
timeout=5,
) as response:
package = response.read()
assert response.headers["Content-Type"] == "application/zip"
assert "attachment" in response.headers["Content-Disposition"]
assert len(package) == artifact["size_bytes"]
delete_request = urllib.request.Request(
f"{self.base_url}/api/firmware-workbench/artifacts/{artifact_id}",
method="DELETE",
)
with urllib.request.urlopen(delete_request, timeout=5) as response:
deleted = json.loads(response.read().decode())
assert deleted["deleted"] is True
repeated_artifact_id = str(repeated_task["artifact_id"])
repeated_delete_request = urllib.request.Request(
f"{self.base_url}/api/firmware-workbench/artifacts/{repeated_artifact_id}",
method="DELETE",
)
with urllib.request.urlopen(repeated_delete_request, timeout=5) as response:
assert json.loads(response.read().decode())["deleted"] is True
with urllib.request.urlopen(
f"{self.base_url}/api/firmware-workbench/state", timeout=5
) as response:
state = json.loads(response.read().decode())
assert [item["id"] for item in state["artifacts"]] == [
"fw-art-time-only-baseline"
]
with urllib.request.urlopen(
f"{self.base_url}/firmware-workbench.html", timeout=5
) as response:
page = response.read().decode()
assert response.headers["X-Frame-Options"] == "SAMEORIGIN"
assert "frame-ancestors 'self'" in response.headers["Content-Security-Policy"]
assert "座椅固件开发工作台" in page
+30
View File
@@ -89,6 +89,13 @@ class TestMutationRequestSecurity:
except urllib.error.HTTPError as exc: except urllib.error.HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8")) return exc.code, json.loads(exc.read().decode("utf-8"))
def get(self, path: str) -> tuple[int, dict[str, object]]:
try:
with urllib.request.urlopen(f"{self.base_url}{path}", timeout=3) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
return exc.code, json.loads(exc.read().decode("utf-8"))
def test_same_origin_json_assistant_request_is_accepted(self) -> None: def test_same_origin_json_assistant_request_is_accepted(self) -> None:
status, body = self.post( status, body = self.post(
"/api/assistant/stream", "/api/assistant/stream",
@@ -147,6 +154,29 @@ class TestMutationRequestSecurity:
assert status == 200 assert status == 200
assert MutationSecurityHandler.last_hardware_bypass is False assert MutationSecurityHandler.last_hardware_bypass is False
def test_vehicle_ota_download_is_public_but_workbench_state_remains_private(self) -> None:
MutationSecurityHandler.auth = _auth(enabled=True)
try:
download_status, download_body = self.get(
"/api/firmware-workbench/releases/fw-rel-missing1234/download"
)
latest_status, latest_body = self.get(
"/api/firmware-workbench/releases/latest"
)
state_status, state_body = self.get("/api/firmware-workbench/state")
finally:
MutationSecurityHandler.auth = _auth(enabled=False)
assert download_status == 404
assert download_body["error"] == "not_found"
assert latest_status in {200, 404}
if latest_status == 404:
assert latest_body["error"] == "not_found"
else:
assert latest_body["download_url"]
assert state_status == 401
assert state_body["error"] == "authentication_required"
def test_hardware_bypass_requires_auth_enabled_and_direct_loopback(self) -> None: def test_hardware_bypass_requires_auth_enabled_and_direct_loopback(self) -> None:
handler = object.__new__(SeatDemoHandler) handler = object.__new__(SeatDemoHandler)
handler.client_address = ("127.0.0.1", 12345) handler.client_address = ("127.0.0.1", 12345)
+6 -6
View File
@@ -734,7 +734,7 @@ operations:
) as response: ) as response:
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
with self.opener.open( with self.opener.open(
f"{self.base_url}/assistant-chat.js?v=codex-assistant-29", timeout=3 f"{self.base_url}/assistant-chat.js?v=codex-assistant-30", timeout=3
) as response: ) as response:
assistant_script = response.read().decode("utf-8") assistant_script = response.read().decode("utf-8")
with self.opener.open( with self.opener.open(
@@ -796,8 +796,8 @@ operations:
self.assertNotIn("滚动查看更多", html) self.assertNotIn("滚动查看更多", html)
self.assertIn('aria-label="发送消息"', html) self.assertIn('aria-label="发送消息"', html)
self.assertIn('id="assistantTranscript"', html) self.assertIn('id="assistantTranscript"', html)
self.assertIn('href="/assistant-chat.css?v=codex-assistant-29"', html) self.assertIn('href="/assistant-chat.css?v=codex-assistant-30"', html)
self.assertIn('src="/assistant-chat.js?v=codex-assistant-29"', html) self.assertIn('src="/assistant-chat.js?v=codex-assistant-30"', html)
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html) self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html) self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
self.assertNotIn('"admin"', console_script) self.assertNotIn('"admin"', console_script)
@@ -1075,11 +1075,11 @@ operations:
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script) self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
self.assertIn('"cognitive-agent:module-change"', app_script) self.assertIn('"cognitive-agent:module-change"', app_script)
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v283"', service_worker) self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v284"', service_worker)
self.assertIn('/console-v2.css?v=delivery-state-68', service_worker) self.assertIn('/console-v2.css?v=delivery-state-68', service_worker)
self.assertIn('/console-v2.js?v=local-first-66', service_worker) self.assertIn('/console-v2.js?v=local-first-66', service_worker)
self.assertIn('/assistant-chat.css?v=codex-assistant-29', service_worker) self.assertIn('/assistant-chat.css?v=codex-assistant-30', service_worker)
self.assertIn('/assistant-chat.js?v=codex-assistant-29', service_worker) self.assertIn('/assistant-chat.js?v=codex-assistant-30', service_worker)
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker) self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker) self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
self.assertIn('"/app.js?v=dual-seat-133"', service_worker) self.assertIn('"/app.js?v=dual-seat-133"', service_worker)
@@ -0,0 +1,321 @@
"""A mode device list is an additive configuration edit, never live vehicle control."""
from __future__ import annotations
import hashlib
import json
from copy import deepcopy
from unittest.mock import MagicMock, patch
import pytest
from cognitive_agent.application.assistant_capabilities import AssistantCapabilityCatalog
from cognitive_agent.application.assistant_plan_guard import MAX_PLAN_REQUESTS
from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
_authorize_vehicle_request,
)
from cognitive_agent.application.mode_profile import ModeProfileRegistry
from cognitive_agent.application.seat_service import REPOSITORY_ROOT, SeatDemoService
@pytest.fixture
def addition_service(tmp_path, monkeypatch):
for variable in (
"COGNITIVE_AGENT_HARDWARE_EXECUTION_ENABLED", "COGNITIVE_AGENT_VEHICLE_CAN_ENABLED",
"COGNITIVE_AGENT_WIRED_PASSENGER_CAN_ENABLED", "COGNITIVE_AGENT_VEHICLE_MQTT_ENABLED",
):
monkeypatch.setenv(variable, "false")
service = SeatDemoService(REPOSITORY_ROOT)
service.artifact_root = tmp_path / "runs"
user_root = tmp_path / "user-modes"
service.mode_profiles = ModeProfileRegistry(REPOSITORY_ROOT / "config" / "modes", user_root=user_root)
try:
with (
patch.object(service.hardware_execution, "reserve", side_effect=AssertionError("mode edit reached real CAN")) as reserve,
patch.object(service._vehicle_mqtt_transport, "bus", side_effect=AssertionError("mode edit reached MQTT")) as mqtt,
patch.object(service.openai, "interpret", side_effect=AssertionError("deterministic mode edit contacted a model")),
):
yield service, AssistantCapabilityCatalog(service), user_root
reserve.assert_not_called()
mqtt.assert_not_called()
finally:
service.close()
def _preview(service, text, *, draft=None, plan=None, conversation="conv-device-additions"):
return service.preview_native_can(
text, provider="local", conversation_id=conversation, vehicle_id="denza_n7",
execution_mode="hardware", mode_profile_draft_context=deepcopy(draft), assistant_plan=plan,
)
def _actions(result):
return (result.get("intent_ir") or {}).get("spec", {}).get("actions", [])
def _action_map(result):
return {
(action["capability"], action["target"].get("instance") or action["target"].get("zone")):
action["parameters"] for action in _actions(result)
}
def _profile_map(profile):
return {(op.capability, op.target_scope): op.parameters for op in profile.operations}
def _seat_actions(mapping):
return {key: value for key, value in mapping.items() if key[0].startswith("seat.")}
def _reference(result):
return {key: result["mode_profile_draft"][key] for key in ("name", "source_request_id", "vehicle_id")}
def _hashes(root):
return {str(path): hashlib.sha256(path.read_bytes()).hexdigest() for path in root.rglob("*.yaml")}
def _seed_old_seat_only_draft(service, *, scope="passenger", name="躺平模式", extra=""):
# A deliberately explicit, seat-only request represents an older legitimate
# preview without relying on ignored developer artifacts or a model stub.
settings = (
"副驾座椅水平向前调整到20%,副驾靠背调整到45%,副驾座椅坐盆调整到30%,副驾腿托调整到70%"
if scope == "passenger" else "主驾座椅通风调整到低档,主驾座椅加热调整到低档"
)
result = _preview(service, f"构建一个{name},{settings}{extra}")
_assert_draft(result)
assert {target for _, target in _seat_actions(_action_map(result))} == {scope}
return result
def _assert_draft(result):
assert result["status"] == "succeeded", (result.get("mode_creation_failure"), result.get("validation"))
assert result["orchestration_route"] == "mode_profile_create_preview"
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert result.get("mode_profile_draft")
assert len(_actions(result)) == result["mode_profile_draft"]["operation_count"]
assert len(_actions(result)) <= MAX_PLAN_REQUESTS
assert len(_action_map(result)) == len(_actions(result)), "append duplicated an existing action key"
def _assistant(service, catalog, message, *, draft=None):
client = MagicMock()
client.available = False
assistant = CodexAssistantService(client)
snapshot = catalog.snapshot(vehicle_id="denza_n7")
fast = catalog.fast_answer(message, snapshot)
raw = []
def execute(instruction, progress, assistant_plan=None):
result = _preview(service, instruction, draft=draft, plan=assistant_plan)
raw.append(result)
return result
answer = assistant.stream(
message, conversation_id="conv-device-additions", principal="device-additions-test", history=[],
emit=lambda _event: True, execute_control=execute,
authoritative_answer=fast.answer if fast else None,
capability_snapshot=snapshot, capability_context=catalog.compact_context(snapshot),
scene_capability_context=catalog.scene_composition_context(snapshot),
control_terms=catalog.control_terms(snapshot), mode_profile_draft_context=draft,
)
assert len(raw) == 1, answer
client.start_structured_turn.assert_not_called()
client.start_turn.assert_not_called()
return answer, raw[0]
@pytest.mark.parametrize("message", ["构建一个躺平模式", "帮我构建一个躺平模式吧", "构建一个主驾躺平模式"])
def test_flat_mode_fast_template_includes_requested_cabin_domains(addition_service, message):
service, catalog, user_root = addition_service
answer, result = _assistant(service, catalog, message)
assert answer["success"] is True, answer
assert answer["route"] == "scene_template"
_assert_draft(result)
assert {"seat", "climate", "ambient_light", "interior_light", "window"} <= {
action["domain"] for action in _actions(result)
}
assert {target for _, target in _seat_actions(_action_map(result))} == (
{"driver"} if "主驾" in message else {"passenger"}
)
assert not _hashes(user_root)
@pytest.mark.parametrize("message", [
"在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作",
"帮我给躺平模式加上空调、氛围灯、阅读灯和车窗",
"给躺平模式增加空调、氛围灯、阅读灯、车窗的联动",
"躺平模式中补充空调、氛围灯、阅读灯和车窗动作",
"调整躺平模式,加入空调、氛围灯、阅读灯、车窗",
])
@pytest.mark.parametrize("scope", ["passenger", "driver"])
def test_device_list_addition_revises_old_draft_without_execution_or_losing_seat_parameters(addition_service, message, scope):
service, catalog, user_root = addition_service
original = _seed_old_seat_only_draft(service, scope=scope)
before = deepcopy(_action_map(original))
reference = _reference(original)
authorization = _authorize_vehicle_request(message, mode_profile_draft_context=reference)
assert authorization.reason == "explicit_mode_profile_draft_update"
answer, revised = _assistant(service, catalog, message, draft=reference)
assert answer["success"] is True, answer
_assert_draft(revised)
assert _seat_actions(_action_map(revised)) == _seat_actions(before)
assert {"climate", "ambient_light", "interior_light", "window"} <= {
action["domain"] for action in _actions(revised)
}
assert revised["mode_profile_draft"]["revises_source_request_id"] == original["request_id"]
assert revised["mode_profile_draft"]["draft_revision"] == 2
assert _action_map(original) == before
assert not _hashes(user_root)
def test_repeated_device_list_is_idempotent_and_keeps_existing_custom_values(addition_service):
service, _, user_root = addition_service
original = _seed_old_seat_only_draft(service, extra=",空调调整到26度,关闭氛围灯")
message = "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作"
first = _preview(service, message, draft=_reference(original))
_assert_draft(first)
for key, params in _action_map(original).items():
assert _action_map(first)[key] == params
second = _preview(service, message, draft=_reference(first))
_assert_draft(second)
assert _action_map(second) == _action_map(first)
assert second["mode_profile_draft"]["draft_revision"] == 3
assert not _hashes(user_root)
@pytest.mark.parametrize("scope", ["passenger", "driver"])
def test_saved_mode_addition_changes_only_selected_profile_and_can_be_replayed(addition_service, scope):
service, _, user_root = addition_service
original = _seed_old_seat_only_draft(service, scope=scope)
draft = original["mode_profile_draft"]
saved = service.create_mode_profile_card(draft["name"], draft["instruction"], draft["source_request_id"], draft["vehicle_id"])
mode_id = saved["mode_profile"]["id"]
unrelated = service.mode_profiles.save_mode("不会改变的模式", [{
"domain": "climate", "capability": "climate.set_temperature", "target_scope": "vehicle",
"parameters": {"temperature_c": 23},
}], vehicle_id="denza_n7")
unrelated_revision = service.mode_profiles.profile_revision(unrelated)
before = _profile_map(service.mode_profiles.get_profile(mode_id, "denza_n7"))
updated = _preview(service, "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作")
assert updated["status"] == "succeeded", updated.get("validation")
assert updated["orchestration_route"] == "mode_profile_update"
assert updated["execution_mode"] == "dry_run"
assert updated["hardware_execution_allowed"] is False
after = service.mode_profiles.get_profile(mode_id, "denza_n7")
assert _seat_actions(_profile_map(after)) == _seat_actions(before)
assert {"climate", "ambient_light", "interior_light", "window"} <= {op.domain for op in after.operations}
assert service.mode_profiles.profile_revision(service.mode_profiles.get_profile(unrelated.id, "denza_n7")) == unrelated_revision
assert len(_hashes(user_root)) == 2
replayed = service.execute_mode_profile_card(mode_id, execution_mode="dry_run", vehicle_id="denza_n7")
assert replayed["status"] == "succeeded"
assert _seat_actions(_action_map(replayed)) == _seat_actions(before)
@pytest.mark.parametrize("mutation", ["request_id", "vehicle", "name", "conversation"])
def test_invalid_or_stale_context_never_becomes_live_control_or_changes_saved_mode(addition_service, mutation):
service, _, user_root = addition_service
original = _seed_old_seat_only_draft(service)
reference = _reference(original)
conversation = "conv-device-additions"
if mutation == "request_id":
reference["source_request_id"] = "req-missing-old-source"
elif mutation == "vehicle":
reference["vehicle_id"] = "other_vehicle"
elif mutation == "name":
reference["name"] = "其他模式"
else:
conversation = "conv-another-conversation"
try:
result = _preview(service, "在躺平模式中加入空调和车窗", draft=reference, conversation=conversation)
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert result["execution_mode"] == "dry_run"
assert result["hardware_execution_allowed"] is False
assert not _actions(result)
assert not result.get("command_plans")
assert not _hashes(user_root)
@pytest.mark.parametrize("message", [
"在躺平模式中加入空调和后备箱动作",
"在躺平模式中加入阅读灯和天窗动作",
"在躺平模式中加入空调并把温度调整到99度",
])
def test_unsupported_or_out_of_range_addition_rejects_all_changes_atomically(addition_service, message):
service, _, user_root = addition_service
original = _seed_old_seat_only_draft(service)
try:
result = _preview(service, message, draft=_reference(original))
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert not result.get("command_plans")
assert result["hardware_execution_allowed"] is False
assert not _hashes(user_root)
def test_adding_devices_to_full_twelve_action_draft_never_truncates_original_actions(addition_service):
service, _, user_root = addition_service
operations = [
("climate.set_power", "vehicle", {"state": "on"}),
("climate.set_temperature", "vehicle", {"temperature_c": 24}),
("climate.set_fan_level", "vehicle", {"level": 2}),
("climate.set_cycle_mode", "vehicle", {"mode": "internal"}),
("climate.set_control_mode", "vehicle", {"mode": "auto"}),
("climate.set_front_defrost", "vehicle", {"state": "off"}),
("roof.set_shade_position", "vehicle", {"position_pct": 0}),
("window.set_position", "front_left", {"position_pct": 0}),
("window.set_position", "front_right", {"position_pct": 0}),
("ambient_light.set_power", "vehicle", {"state": "off"}),
("interior_light.set_indoor_power", "vehicle", {"state": "off"}),
("interior_light.set_reading_power", "all", {"state": "off"}),
]
plan = {
"detected_domains": sorted({cap.split(".", 1)[0] for cap, _, _ in operations}),
"requests": [{
"domain": cap.split(".", 1)[0], "capability": cap, "target_scope": target,
"operation": "set", "parameters": [{"name": name, "value": value} for name, value in params.items()],
"duration_ms": None, "reason": "固定目标边界回归。",
} for cap, target, params in operations],
"ambiguities": [], "confidence": 0.95, "decision_summary": ["验证12项原动作保留。"],
}
original = _preview(service, "构建一个满载参数模式", plan=plan)
_assert_draft(original)
assert len(_actions(original)) == 12
source_path = service.artifact_root / original["request_id"] / "result.json"
source_hash = hashlib.sha256(source_path.read_bytes()).hexdigest()
try:
result = _preview(service, "在满载参数模式中加入副驾座椅通风动作", draft=_reference(original))
except ValueError:
pass
else:
assert result["status"] != "succeeded"
assert not result.get("mode_profile_draft")
assert not _actions(result)
assert hashlib.sha256(source_path.read_bytes()).hexdigest() == source_hash
assert not _hashes(user_root)
def test_previous_artifact_without_new_metadata_remains_an_editable_server_owned_draft(addition_service):
service, _, user_root = addition_service
original = _seed_old_seat_only_draft(service)
legacy = deepcopy(original)
legacy["mode_profile_draft"].pop("draft_revision", None)
legacy["mode_profile_draft"].pop("generation_source", None)
service._persist(legacy)
result = _preview(service, "在躺平模式中加入空调、氛围灯、阅读灯、车窗", draft=_reference(legacy))
_assert_draft(result)
assert result["mode_profile_draft"]["draft_revision"] == 2
assert _seat_actions(_action_map(result)) == _seat_actions(_action_map(original))
assert not _hashes(user_root)
assert json.loads((service.artifact_root / original["request_id"] / "result.json").read_text(encoding="utf-8"))["request_id"] == original["request_id"]
@@ -552,6 +552,58 @@ test("the change-into predicate carries only a same-name opaque draft reference"
assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false); assert.equal(Object.hasOwn(anotherMode, "mode_profile_draft_context"), false);
}); });
for (const phrase of [
"在午休模式中加入空调、氛围灯、阅读灯、车窗等设备动作",
"请帮我在午休模式里添加空调和车窗动作",
"给午休模式增加氛围灯",
"为午休模式补充阅读灯",
"往午休模式内加入空调,温度26度",
"向午休模式中加上关闭车窗",
"午休模式中再加入空调温度26度",
"午休模式再补充车窗和阅读灯",
"把关闭车窗加入到午休模式中",
"请将空调26度和柔和氛围灯添加到午休模式",
]) {
test(`device additions carry only the same-name opaque draft: ${phrase}`, async () => {
const events = modeDraftEvents("req-draft-add-devices", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, { type: "answer.completed", answer: "已收到草稿补充请求" });
harness.input.value = phrase;
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls.at(-1).options.body);
assert.equal(request.message, phrase);
assert.deepEqual(request.mode_profile_draft_context, {
name: "午休模式", source_request_id: "req-draft-add-devices", vehicle_id: "demo-vehicle",
});
assert.equal(harness.requestCalls.every((call) => call.url === "/api/assistant/stream"), true);
});
}
for (const phrase of [
"在露营模式中加入空调和车窗",
"把空调添加到露营模式中",
"在午休模式中加入空调可以吗?",
"午休模式中加入哪些设备合适?",
"在午休模式中不要加入空调",
"在午休模式中先别添加车窗",
"打开空调",
"设置空调出风模式为吹脚",
]) {
test(`non-update or other-mode additions do not borrow a draft: ${phrase}`, async () => {
const events = modeDraftEvents("req-draft-add-isolation", 24);
const harness = createHarness(events);
harness.input.value = "构建一个午休模式";
await harness.sandbox.CognitiveAgentAssistant.submit();
events.splice(0, events.length, { type: "answer.completed", answer: "已收到" });
harness.input.value = phrase;
await harness.sandbox.CognitiveAgentAssistant.submit();
const request = JSON.parse(harness.requestCalls.at(-1).options.body);
assert.equal(Object.hasOwn(request, "mode_profile_draft_context"), false);
});
}
test("shade position rows explain the target opening at semantic boundaries", async () => { test("shade position rows explain the target opening at semantic boundaries", async () => {
const positions = [0, 30, 50, 100]; const positions = [0, 30, 50, 100];
const events = [ const events = [
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
assert 'aria-label="AI助手对话"' in html assert 'aria-label="AI助手对话"' in html
assert 'placeholder="向 AI助手提问或交代工作"' in html assert 'placeholder="向 AI助手提问或交代工作"' in html
assert 'aria-label="发送消息"' in html assert 'aria-label="发送消息"' in html
assert 'href="/assistant-chat.css?v=codex-assistant-29"' in html assert 'href="/assistant-chat.css?v=codex-assistant-30"' in html
assert 'src="/assistant-chat.js?v=codex-assistant-29"' in html assert 'src="/assistant-chat.js?v=codex-assistant-30"' in html
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker(): def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8") service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v284"' in service_worker
assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker assert '"/assistant-chat.css?v=codex-assistant-30"' in service_worker
assert '"/assistant-chat.js?v=codex-assistant-29"' in service_worker assert '"/assistant-chat.js?v=codex-assistant-30"' in service_worker
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events(): def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
@@ -214,7 +214,7 @@ def test_assistant_carries_only_the_active_mode_draft_reference_to_follow_up_tur
assert "rememberModeDraftContext(draft)" in assistant assert "rememberModeDraftContext(draft)" in assistant
assert "activeModeDraftContext = loadActiveModeDraftContext()" in assistant assert "activeModeDraftContext = loadActiveModeDraftContext()" in assistant
assert "function isModeDraftUpdateRequest(message, context)" in assistant assert "function isModeDraftUpdateRequest(message, context)" in assistant
assert "normalizeModeName(matched[1]) === normalizeModeName(context.name)" in assistant assert "normalizeModeName(matched[nameIndex]) === normalizeModeName(context.name)" in assistant
def test_latest_same_name_mode_draft_retires_stale_card_and_lifecycle_clears_context(): def test_latest_same_name_mode_draft_retires_stale_card_and_lifecycle_clears_context():
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
): ):
assert asset in html assert asset in html
assert f'"{asset}"' in service_worker assert f'"{asset}"' in service_worker
assert html.index("/assistant-chat.js?v=codex-assistant-29") < html.index( assert html.index("/assistant-chat.js?v=codex-assistant-30") < html.index(
"/energy-monitor.js?v=energy-monitor-4" "/energy-monitor.js?v=energy-monitor-4"
) )
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v288"' in service_worker
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None: def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
+253
View File
@@ -0,0 +1,253 @@
from __future__ import annotations
import hashlib
import json
import threading
import time
import zipfile
from pathlib import Path
from cognitive_agent.application.firmware_workbench import (
FINAL_RELEASE_MESSAGE,
FirmwareWorkbenchService,
)
ROOT = Path(__file__).resolve().parents[2]
def wait_for_terminal(service: FirmwareWorkbenchService, task_id: str) -> dict[str, object]:
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
task = service.task(task_id)
if task["status"] not in {"waiting", "running", "pending_confirmation"}:
return task
time.sleep(0.02)
raise AssertionError("firmware pipeline did not reach a terminal state")
def make_image(tmp_path: Path) -> Path:
image = tmp_path / "Seat_Controller_demo.hex"
image.write_bytes(b":020000040000FA\n" + b":100000000102030405060708090A0B0C0D0E0F1068\n" * 80)
return image
def test_pipeline_publishes_a_downloadable_ota_package(
tmp_path: Path, monkeypatch
) -> None:
monkeypatch.setenv(
"FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL",
"http://192.168.43.130:8000/api/firmware-workbench/releases",
)
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
instruction = "增加支持座椅位置参数控制的能力,增加座椅通风功能,增加座椅加热功能"
first = service.start_task(instruction)
task = wait_for_terminal(service, str(first["task"]["id"]))
assert task["status"] == "success"
assert task["message"] == FINAL_RELEASE_MESSAGE
assert [stage["status"] for stage in task["stages"]] == ["success"] * 8
assert len(task["test_results"]) == 7
snapshot = service.snapshot()
assert snapshot["baselines"] == [
{
"id": "time_only",
"name": "时间控制基线",
"capabilities": ["时间控制"],
}
]
assert len(snapshot["artifacts"]) == 2
assert len(snapshot["releases"]) == 1
artifact = next(item for item in snapshot["artifacts"] if item["task_id"] == task["id"])
baseline = next(item for item in snapshot["artifacts"] if item.get("is_baseline"))
release = snapshot["releases"][0]
assert baseline["capabilities"] == ["时间控制"]
assert baseline["protected"] is True
assert artifact["capabilities"] == ["时间控制", "位置控制", "座椅通风", "座椅加热"]
assert artifact["status"] == "published"
assert release["download_url"] == (
f"http://192.168.43.130:8000/api/firmware-workbench/releases/"
f"{release['id']}/download"
)
assert service.latest_release()["download_url"] == release["download_url"]
latest_manifest = json.loads((tmp_path / "ota" / "latest.json").read_text("utf-8"))
assert latest_manifest["download_url"] == release["download_url"]
package, filename = service.resolve_release_download(str(release["id"]))
assert filename == artifact["firmware_filename"]
assert hashlib.sha256(package.read_bytes()).hexdigest() == artifact["sha256"]
with zipfile.ZipFile(package) as archive:
assert {
"Seat_Controller_demo.hex",
"manifest.json",
"requirements.json",
"validation.json",
"feature_profile.json",
} <= set(archive.namelist())
assert (tmp_path / "ota" / "latest.json").is_file()
assert service.publish_artifact(str(artifact["id"]))["id"] == release["id"]
service.close()
monkeypatch.setenv(
"FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL",
"http://192.168.43.131:8000/api/firmware-workbench/releases",
)
migrated = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
migrated_release = migrated.latest_release()
assert migrated_release["download_url"].startswith("http://192.168.43.131:8000/")
migrated_latest = json.loads((tmp_path / "ota" / "latest.json").read_text("utf-8"))
assert migrated_latest["download_url"] == migrated_release["download_url"]
migrated.close()
def test_repeated_instruction_always_creates_and_executes_a_new_task(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
first = service.start_task("增加座椅加热功能。")
first_task = wait_for_terminal(service, str(first["task"]["id"]))
second = service.start_task("增加座椅加热功能。")
assert first["task"]["id"] != second["task"]["id"]
assert "duplicate" not in first
assert "duplicate" not in second
second_task = wait_for_terminal(service, str(second["task"]["id"]))
assert first_task["status"] == "success"
assert second_task["status"] == "success"
assert first_task["target_capabilities"] == ["时间控制", "座椅加热"]
assert second_task["target_capabilities"] == ["时间控制", "座椅加热"]
service.close()
def test_non_baseline_artifact_can_be_deleted_but_baseline_is_protected(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: 0 errors, 0 warnings")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加座椅通风功能")
task = wait_for_terminal(service, str(started["task"]["id"]))
package_path, _ = service.resolve_artifact_download(str(task["artifact_id"]))
release_path, _ = service.resolve_release_download(str(task["release_id"]))
result = service.delete_artifact(str(task["artifact_id"]))
assert result["deleted"] is True
assert not package_path.exists()
assert not release_path.exists()
snapshot = service.snapshot()
assert [artifact["id"] for artifact in snapshot["artifacts"]] == [
"fw-art-time-only-baseline"
]
assert snapshot["releases"] == []
try:
service.delete_artifact("fw-art-time-only-baseline")
except Exception as exc: # noqa: BLE001 - public error type is asserted below
assert getattr(exc, "code", None) == "baseline_protected"
else:
raise AssertionError("time-only baseline must not be deletable")
service.close()
def test_position_instruction_merges_with_time_only_baseline(tmp_path: Path) -> None:
image = make_image(tmp_path)
def build_runner(
_task_id: str, log: object, _cancel: threading.Event
) -> Path:
log("info", "IAR build: profile position=1 ventilation=0 heating=0")
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加支持座椅位置参数控制的能力。", "time_only")
task = wait_for_terminal(service, str(started["task"]["id"]))
assert task["status"] == "success"
assert task["baseline_capabilities"] == ["时间控制"]
assert task["requested_capabilities"] == ["位置控制"]
assert task["target_capabilities"] == ["时间控制", "位置控制"]
artifact = service.snapshot()["artifacts"][0]
assert artifact["capabilities"] == ["时间控制", "位置控制"]
assert artifact["capability_diff"]["added"] == ["位置控制"]
assert "vent" not in artifact["firmware_filename"]
assert "heat" not in artifact["firmware_filename"]
service.close()
def test_running_pipeline_can_be_canceled_and_retried(tmp_path: Path) -> None:
image = make_image(tmp_path)
allow_build = threading.Event()
def build_runner(
_task_id: str, log: object, cancel: threading.Event
) -> Path:
log("info", "IAR build started")
while not allow_build.is_set():
if cancel.wait(0.01):
raise InterruptedError
return image
service = FirmwareWorkbenchService(
ROOT,
artifact_root=tmp_path / "workbench",
ota_root=tmp_path / "ota",
build_runner=build_runner,
)
started = service.start_task("增加座椅通风功能")
task_id = str(started["task"]["id"])
deadline = time.monotonic() + 3
while service.task(task_id)["current_stage"] != "build" and time.monotonic() < deadline:
time.sleep(0.01)
service.cancel_task(task_id)
canceled = wait_for_terminal(service, task_id)
assert canceled["status"] == "canceled"
assert any(stage["status"] == "canceled" for stage in canceled["stages"])
allow_build.set()
retried = service.retry_task(task_id)
assert retried["id"] == task_id
completed = wait_for_terminal(service, task_id)
assert completed["status"] == "success"
service.close()
@@ -0,0 +1,62 @@
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WEB = ROOT / "apps" / "web-console"
def source(name: str) -> str:
return (WEB / name).read_text(encoding="utf-8")
def test_workbench_is_a_standalone_development_surface() -> None:
html = source("firmware-workbench.html")
script = source("firmware-workbench.js")
assert "座椅固件开发工作台" in html
assert '<textarea id="instructionInput"' in html
assert 'id="startTaskButton"' in html
assert 'id="cancelTaskButton"' in html
assert 'id="pipeline"' in html
assert 'id="logStream"' in html
assert 'id="artifactTableBody"' in html
assert 'id="baselineSelect"' in html
assert 'id="targetCapabilities"' in html
assert "执行摘要" not in html
assert "单屏工作台" not in html
assert "相同指令只会复用同一任务" not in html
assert "固件交付流水线</h2>" not in html
assert html.count("<option") == 1
assert '<option value="time_only">时间控制</option>' in html
for english_label in ("DEVELOPMENT BRIEF", "DELIVERY PIPELINE", "LIVE EXECUTION", "SOFTWARE ARTIFACTS"):
assert english_label not in html
assert "普通聊天" not in html
for stage in ("指令输入", "需求解析", "基线检查", "代码生成", "编译构建", "测试验证", "固件打包", "发布服务器"):
assert stage in script or stage in html
for status in ("等待", "运行中", "待确认", "成功", "失败", "取消", "阻塞"):
assert status in script or status in html
def test_workbench_exposes_artifact_actions_and_always_starts_a_new_task() -> None:
html = source("firmware-workbench.html")
script = source("firmware-workbench.js")
entry = source("firmware-workbench-entry.js")
index = source("index.html")
worker = source("service-worker.js")
for label in ("查看详情", "能力差异", "代码差异", "验证记录", "下载固件", "发布到服务器", "删除固件产物"):
assert label in script
assert "检测到相同指令" not in script
assert "result.duplicate" not in script
assert "已发布,待车机端下载升级" not in script
assert 'id = "firmwareWorkbenchToggle"' in entry
assert 'className = "firmware-workbench-layer"' in entry
assert "/firmware-workbench-entry.js?v=2" in index
assert "/firmware-workbench-entry.css?v=1" in index
for asset in (
"/firmware-workbench-entry.css?v=1",
"/firmware-workbench-entry.js?v=2",
"/firmware-workbench.html",
"/firmware-workbench.css?v=5",
"/firmware-workbench.js?v=3",
):
assert f'"{asset}"' in worker
+1 -1
View File
@@ -47,7 +47,7 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
): ):
assert asset in html assert asset in html
assert f'"{asset}"' in service_worker assert f'"{asset}"' in service_worker
assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker assert 'const CACHE_VERSION = "cognitive-agent-pad-v288"' in service_worker
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None: def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
@@ -0,0 +1,123 @@
from __future__ import annotations
from copy import deepcopy
import pytest
from cognitive_agent.application.codex_assistant import (
CodexAssistantService,
_authorize_vehicle_request,
_validated_mode_profile_draft_context,
)
from cognitive_agent.application.mode_profile_intent import (
is_mode_profile_update_shape,
parse_mode_profile_update,
)
DRAFT_REFERENCE = {
"name": "躺平模式", "source_request_id": "req-augmentation-source-123456", "vehicle_id": "denza_n7",
}
CONTROL_TERMS = ("空调", "氛围灯", "阅读灯", "车窗", "座椅")
@pytest.mark.parametrize("prefix", ["在", "给", "为", "往", "向", ""])
@pytest.mark.parametrize("verb", ["加入", "添加", "增加", "补充", "加上", "追加", "新增"])
def test_named_mode_device_augmentation_is_recognized(prefix, verb):
message = f"{prefix}躺平模式中{verb}空调、氛围灯、阅读灯、车窗等设备动作"
assert parse_mode_profile_update(message) == ("躺平模式", f"{verb}空调、氛围灯、阅读灯、车窗等设备动作")
assert _validated_mode_profile_draft_context(DRAFT_REFERENCE, message=message) == DRAFT_REFERENCE
authorization = _authorize_vehicle_request(message, CONTROL_TERMS, DRAFT_REFERENCE)
assert authorization.instruction == message
assert authorization.reason == "explicit_mode_profile_draft_update"
@pytest.mark.parametrize(("message", "change"), [
("请在躺平模式里补充空调", "补充空调"),
("帮我给躺平模式添加空调", "添加空调"),
("为躺平模式新增阅读灯", "新增阅读灯"),
("躺平模式再补充车窗", "补充车窗"),
("躺平模式中再加入阅读灯", "再加入阅读灯"),
("在躺平模式中加入空调24度、阅读灯打开", "加入空调24度、阅读灯打开"),
("把空调24度、阅读灯打开加入到躺平模式中", "加入空调24度、阅读灯打开"),
("将车窗、氛围灯追加进躺平模式", "追加车窗、氛围灯"),
("请把空调加入躺平模式吧", "加入空调"),
("把副驾空调温度25.5度补充到躺平模式", "补充副驾空调温度25.5度"),
("在“躺平模式”中添加空调", "添加空调"),
])
def test_append_word_order_preserves_device_parameters_and_scoped_name(message, change):
parsed = parse_mode_profile_update(message)
assert parsed is not None and parsed[0] == "躺平模式"
# A leading 再 modifier is harmless; neither parameters nor target words
# may be lost when the reverse-order request is normalized.
assert parsed[1].removeprefix("再") == change.removeprefix("再")
@pytest.mark.parametrize("message", [
"不要在躺平模式中加入空调", "在躺平模式中不要加入空调",
"躺平模式中先不要补充阅读灯", "给躺平模式添加空调,但不要执行",
"给躺平模式添加空调,算了", "给躺平模式添加空调,撤销",
"躺平模式中取消加入空调", "如何在躺平模式中加入空调?",
"在躺平模式中加入空调好吗?", "在躺平模式中能否加入空调",
"把空调加入到躺平模式会怎样?", "解释在躺平模式中加入空调这句话",
"在躺平模式中加入哪些设备?", "引用在躺平模式中加入空调",
"在空调出风模式中加入阅读灯", "在副驾按摩模式中添加车窗",
"在躺平模式中加入", "给躺平模式补充",
])
def test_negated_read_only_or_device_property_requests_do_not_authorize_augmentation(message):
assert parse_mode_profile_update(message) is None
assert _validated_mode_profile_draft_context(DRAFT_REFERENCE, message=message) is None
authorization = _authorize_vehicle_request(message, CONTROL_TERMS, DRAFT_REFERENCE)
assert authorization.reason not in {"explicit_mode_profile_draft_update", "explicit_mode_profile_update"}
def test_other_named_mode_never_borrows_the_selected_draft_reference():
message = "在阅读模式中加入空调"
assert parse_mode_profile_update(message) == ("阅读模式", "加入空调")
assert _validated_mode_profile_draft_context(DRAFT_REFERENCE, message=message) is None
assert _authorize_vehicle_request(message, CONTROL_TERMS, DRAFT_REFERENCE).reason == "explicit_mode_profile_update"
def test_augmentation_reference_cannot_include_browser_supplied_device_actions():
reference = {**DRAFT_REFERENCE, "operations": [{"capability": "seat.anything"}]}
with pytest.raises(ValueError, match="fields are invalid"):
_validated_mode_profile_draft_context(reference, message="在躺平模式中加入空调")
def test_speech_update_shape_preserves_named_augmentation_even_when_negated():
assert is_mode_profile_update_shape("在躺平模式中加入空调,但不要执行")
assert parse_mode_profile_update("在躺平模式中加入空调,但不要执行") is None
def test_device_augmentation_bypasses_general_agent_and_partial_control_paths():
client = type("UnavailableClient", (), {"available": False})()
assistant = CodexAssistantService(client)
message = "在躺平模式中加入空调、氛围灯、阅读灯、车窗等设备动作"
calls = []
events = []
def execute_control(instruction, _progress):
calls.append(instruction)
return {
"request_id": "req-revised-123456", "status": "succeeded", "execution_mode": "dry_run",
"orchestration_route": "mode_profile_create_preview",
"intent_ir": {"spec": {"actions": [{"domain": "climate"}]}},
"mode_profile_draft": {
"name": "躺平模式", "source_request_id": "req-revised-123456", "vehicle_id": "denza_n7",
"instruction": "进入躺平模式", "operation_count": 1,
"generation_source": "draft_revision", "draft_revision": 2,
},
}
reference = deepcopy(DRAFT_REFERENCE)
result = assistant.stream(
message, conversation_id="conv-augmentation-intent-123456", principal="test",
history=[], emit=events.append, execute_control=execute_control,
preview_control=lambda *_args: pytest.fail("augmentation must not run partial hardware controls"),
supplement_control=lambda *_args: pytest.fail("augmentation must not become a vehicle supplement"),
mode_profile_draft_context=reference, control_terms=CONTROL_TERMS,
)
assert calls == [message]
assert result["success"] is True
assert result["result"]["mode_profile_draft"]["name"] == "躺平模式"
assert reference == DRAFT_REFERENCE
assert events[0]["route"] == "vehicle_control"
+76 -9
View File
@@ -4,7 +4,14 @@ param(
[string]$Profile = 'combined', [string]$Profile = 'combined',
[string]$Configuration = 'rev_d', [string]$Configuration = 'rev_d',
[string]$RepositoryRoot = '', [string]$RepositoryRoot = '',
[string]$IarBuild = '' [string]$IarBuild = '',
[string]$OutputDirectory = '',
[ValidateSet(0, 1)]
[int]$PositionControl = 1,
[ValidateSet(0, 1)]
[int]$Ventilation = 1,
[ValidateSet(0, 1)]
[int]$Heating = 1
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -69,9 +76,46 @@ function Write-ImageSummary {
if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) { if (-not (Test-Path -LiteralPath $ImagePath -PathType Leaf)) {
throw "Build completed but HEX image was not found: $ImagePath" throw "Build completed but HEX image was not found: $ImagePath"
} }
$hash = Get-FileHash -LiteralPath $ImagePath -Algorithm SHA256 # Use the .NET primitive directly. Some locked-down PowerShell hosts do not
# expose Get-FileHash even though the Utility module is otherwise present.
$stream = [System.IO.File]::OpenRead($ImagePath)
try {
$algorithm = [System.Security.Cryptography.SHA256]::Create()
try {
$hashBytes = $algorithm.ComputeHash($stream)
}
finally {
$algorithm.Dispose()
}
}
finally {
$stream.Dispose()
}
$hashValue = [System.BitConverter]::ToString($hashBytes).Replace('-', '').ToLowerInvariant()
Write-Host "Seat_Controller_demo HEX: $ImagePath" Write-Host "Seat_Controller_demo HEX: $ImagePath"
Write-Host "Seat_Controller_demo SHA256: $($hash.Hash.ToLowerInvariant())" Write-Host "Seat_Controller_demo SHA256: $hashValue"
}
function Copy-BuildArtifact {
param(
[string]$Source,
[string]$Destination
)
$lastError = $null
for ($attempt = 1; $attempt -le 6; $attempt++) {
try {
Copy-Item -LiteralPath $Source -Destination $Destination -Force
return
}
catch [System.IO.IOException] {
$lastError = $_
if ($attempt -lt 6) {
Start-Sleep -Milliseconds (150 * $attempt)
}
}
}
throw $lastError
} }
# Preserve the IAR project's original ../../common and ../../tviibh8m layout # Preserve the IAR project's original ../../common and ../../tviibh8m layout
@@ -97,23 +141,46 @@ try {
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'common') -Target $vendorCommon | Out-Null New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'common') -Target $vendorCommon | Out-Null
New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target $vendorDevice | Out-Null New-Item -ItemType Junction -Path (Join-Path $stagingRootFull 'tviibh8m') -Target $vendorDevice | Out-Null
$featureProfilePath = Join-Path $stagingProjectRoot 'Config\seat_feature_profile.h'
$featureProfile = @"
#ifndef SEAT_FEATURE_PROFILE_H
#define SEAT_FEATURE_PROFILE_H
/* Generated by tools/build_seat_firmware.ps1 for this build only. */
#define SEAT_FEATURE_POSITION_CONTROL ($PositionControl)
#define SEAT_FEATURE_VENTILATION ($Ventilation)
#define SEAT_FEATURE_HEATING ($Heating)
#endif /* SEAT_FEATURE_PROFILE_H */
"@
[System.IO.File]::WriteAllText(
$featureProfilePath,
$featureProfile,
[System.Text.UTF8Encoding]::new($false)
)
Write-Host "Feature profile: position=$PositionControl ventilation=$Ventilation heating=$Heating"
$stagedProject = Join-Path $stagingProjectRoot 'IAR\Seat_Controller_demo.ewp' $stagedProject = Join-Path $stagingProjectRoot 'IAR\Seat_Controller_demo.ewp'
Invoke-IarBuild -ProjectPath $stagedProject Invoke-IarBuild -ProjectPath $stagedProject
$stagedExe = Join-Path $stagingProjectRoot "IAR\$Configuration\Exe" $stagedExe = Join-Path $stagingProjectRoot "IAR\$Configuration\Exe"
$repositoryExe = Join-Path $repositoryFirmware "IAR\$Configuration\Exe" $artifactOutput = if ($OutputDirectory) {
New-Item -ItemType Directory -Path $repositoryExe -Force | Out-Null [System.IO.Path]::GetFullPath($OutputDirectory)
} else {
Join-Path $repositoryFirmware "IAR\$Configuration\Exe"
}
New-Item -ItemType Directory -Path $artifactOutput -Force | Out-Null
foreach ($artifact in @('Seat_Controller_demo.hex', 'Seat_Controller_demo.out')) { foreach ($artifact in @('Seat_Controller_demo.hex', 'Seat_Controller_demo.out')) {
$sourceArtifact = Join-Path $stagedExe $artifact $sourceArtifact = Join-Path $stagedExe $artifact
if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) { if (-not (Test-Path -LiteralPath $sourceArtifact -PathType Leaf)) {
throw "IAR build did not produce $sourceArtifact" throw "IAR build did not produce $sourceArtifact"
} }
Copy-Item -LiteralPath $sourceArtifact -Destination ( Copy-BuildArtifact -Source $sourceArtifact -Destination (
Join-Path $repositoryExe $artifact Join-Path $artifactOutput $artifact
) -Force )
} }
Write-ImageSummary -ImagePath ( Write-ImageSummary -ImagePath (
Join-Path $repositoryExe 'Seat_Controller_demo.hex' Join-Path $artifactOutput 'Seat_Controller_demo.hex'
) )
} }
finally { finally {
+65 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Start N7Web and its read-only LAN mode-card service together.""" """Start N7Web, the read-only OTA endpoint, and the LAN mode-card service."""
from __future__ import annotations from __future__ import annotations
@@ -13,6 +13,8 @@ import time
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from urllib import error as urlerror
from urllib import request as urlrequest
REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
@@ -49,9 +51,20 @@ def _parser() -> argparse.ArgumentParser:
parser.add_argument( parser.add_argument(
"--host", "--host",
default="127.0.0.1", default="127.0.0.1",
help="main N7Web bind address (use 0.0.0.0 for trusted-LAN access)", help="main N7Web bind address (defaults to local management only)",
) )
parser.add_argument("--port", type=_port, default=8000, help="main N7Web port") parser.add_argument("--port", type=_port, default=8000, help="main N7Web port")
parser.add_argument(
"--ota-host",
default="127.0.0.1",
help="read-only firmware OTA bind address (designed for an HTTPS tunnel)",
)
parser.add_argument(
"--ota-port",
type=_port,
default=8012,
help="read-only firmware OTA port",
)
parser.add_argument( parser.add_argument(
"--scene-bind", "--scene-bind",
default="0.0.0.0", default="0.0.0.0",
@@ -140,7 +153,7 @@ def _commands(
args: argparse.Namespace, args: argparse.Namespace,
python: Path, python: Path,
catalog: Path, catalog: Path,
) -> tuple[list[str], list[str]]: ) -> tuple[list[str], list[str], list[str]]:
main_command = [ main_command = [
str(python), str(python),
"-m", "-m",
@@ -167,7 +180,16 @@ def _commands(
"--server-id", "--server-id",
args.server_id, args.server_id,
] ]
return main_command, scene_command ota_command = [
str(python),
"-m",
"cognitive_agent.firmware_ota_server",
"--host",
args.ota_host,
"--port",
str(args.ota_port),
]
return main_command, scene_command, ota_command
def _display_command(command: Sequence[str]) -> str: def _display_command(command: Sequence[str]) -> str:
@@ -189,6 +211,28 @@ def _catalog_is_ready(path: Path) -> bool:
) )
def _ota_service_is_ready(host: str, port: int) -> bool:
probe_host = "127.0.0.1" if host in {"0.0.0.0", "::", "[::]"} else host
if ":" in probe_host and not probe_host.startswith("["):
probe_host = f"[{probe_host}]"
request = urlrequest.Request(
f"http://{probe_host}:{port}/healthz",
headers={"Accept": "application/json"},
)
opener = urlrequest.build_opener(urlrequest.ProxyHandler({}))
try:
with opener.open(request, timeout=1.0) as response:
document = json.loads(response.read().decode("utf-8"))
except (OSError, ValueError, urlerror.URLError):
return False
return bool(
response.status == 200
and isinstance(document, dict)
and document.get("status") == "ok"
and document.get("service") == "n7-firmware-ota"
)
def _wait_for_catalog( def _wait_for_catalog(
path: Path, path: Path,
main_process: subprocess.Popen[bytes], main_process: subprocess.Popen[bytes],
@@ -264,10 +308,11 @@ def run(argv: Sequence[str] | None = None) -> int:
return 2 return 2
environment = _child_environment() environment = _child_environment()
main_command, scene_command = _commands(args, python, catalog) main_command, scene_command, ota_command = _commands(args, python, catalog)
print(f"Repository: {REPOSITORY_ROOT}", flush=True) print(f"Repository: {REPOSITORY_ROOT}", flush=True)
print(f"Catalog: {catalog}", flush=True) print(f"Catalog: {catalog}", flush=True)
print(f"Main: {_display_command(main_command)}", flush=True) print(f"Main: {_display_command(main_command)}", flush=True)
print(f"OTA: {_display_command(ota_command)}", flush=True)
print(f"Cards: {_display_command(scene_command)}", flush=True) print(f"Cards: {_display_command(scene_command)}", flush=True)
if args.dry_run: if args.dry_run:
return 0 return 0
@@ -283,6 +328,20 @@ def run(argv: Sequence[str] | None = None) -> int:
children.append(ChildProcess("main N7Web service", main_process)) children.append(ChildProcess("main N7Web service", main_process))
_wait_for_catalog(catalog, main_process, args.catalog_ready_timeout) _wait_for_catalog(catalog, main_process, args.catalog_ready_timeout)
if _ota_service_is_ready(args.ota_host, args.ota_port):
print(
f"OTA service already available at "
f"http://{args.ota_host}:{args.ota_port}; reusing it.",
flush=True,
)
else:
ota_process = subprocess.Popen(
ota_command,
cwd=REPOSITORY_ROOT,
env=environment,
)
children.append(ChildProcess("firmware OTA service", ota_process))
scene_process = subprocess.Popen( scene_process = subprocess.Popen(
scene_command, scene_command,
cwd=REPOSITORY_ROOT, cwd=REPOSITORY_ROOT,
@@ -290,7 +349,7 @@ def run(argv: Sequence[str] | None = None) -> int:
) )
children.append(ChildProcess("LAN mode-card service", scene_process)) children.append(ChildProcess("LAN mode-card service", scene_process))
print( print(
"Both services are running; press Ctrl+C once to stop both.", "All services are running; press Ctrl+C once to stop all of them.",
flush=True, flush=True,
) )
name, exit_code = _wait_for_exit(children) name, exit_code = _wait_for_exit(children)
+30
View File
@@ -0,0 +1,30 @@
$ErrorActionPreference = "Stop"
$repositoryRoot = Split-Path -Parent $PSScriptRoot
$venvPython = Join-Path $repositoryRoot ".venv\Scripts\python.exe"
$logRoot = Join-Path $repositoryRoot "artifacts\logs"
$logPath = Join-Path $logRoot "n7web-autostart.log"
if (-not (Test-Path -LiteralPath $venvPython -PathType Leaf)) {
throw "N7Web Python executable not found: $venvPython"
}
New-Item -ItemType Directory -Path $logRoot -Force | Out-Null
$python = (& $venvPython -c "import sys; print(sys._base_executable)").Trim()
if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $python -PathType Leaf)) {
throw "N7Web base Python executable could not be resolved."
}
$env:PYTHONPATH = @(
(Join-Path $repositoryRoot "src")
(Join-Path $repositoryRoot "domains\seat\src")
) -join [System.IO.Path]::PathSeparator
while ($true) {
& $python -m cognitive_agent.firmware_ota_server --host 127.0.0.1 --port 8012 *>> $logPath
$exitCode = $LASTEXITCODE
$timestamp = Get-Date -Format "yyyy-MM-ddTHH:mm:ssK"
Add-Content -LiteralPath $logPath -Value (
"[$timestamp] OTA service exited with code $exitCode; restarting in 2 seconds."
)
Start-Sleep -Seconds 2
}