feat: add seat firmware workbench and OTA delivery
This commit is contained in:
@@ -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/
|
||||||
|
|||||||
@@ -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 或磁盘轮询。
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -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; }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
})();
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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 */
|
||||||
+155
@@ -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.
|
||||||
+88
@@ -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 */
|
||||||
+1619
File diff suppressed because it is too large
Load Diff
+2359
File diff suppressed because it is too large
Load Diff
+185
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
+213
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
+2690
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
@@ -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
@@ -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
|
||||||
|
|||||||
Binary file not shown.
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)
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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__":
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user