diff --git a/.gitignore b/.gitignore index 1ca1739..002616f 100644 --- a/.gitignore +++ b/.gitignore @@ -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/List/ 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 +artifacts/logs/ diff --git a/README.md b/README.md index 58115c4..8e60764 100755 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ config/gesture/default.yaml .\.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 或磁盘轮询。 diff --git a/apps/web-console/assistant-chat.js b/apps/web-console/assistant-chat.js index 76154b3..67e1297 100644 --- a/apps/web-console/assistant-chat.js +++ b/apps/web-console/assistant-chat.js @@ -170,11 +170,13 @@ if (!compact || !context?.name) return false; const politePrefix = "(?:(?:请|麻烦)(?:帮我|帮忙|给我)?|帮我|帮忙|给我)?"; const prefixUpdateVerb = "(?:修改一下|修改|调整|更改|更新|改一下|变更)"; - const changePredicate = "(?:修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)"; + const addPredicate = "(?:加入|添加|增加|补充|加上|追加|新增)"; + const changePredicate = `(?:${addPredicate}|修改|改成|改为|更改|更新|变更|变为|变成|换成|调整|调节|调高|调低|调到|调成|调为|调至|设置|设为|设成|设到|设至|打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|复位|半开|开到|关到|拉开|拉上|上锁|解锁)`; const modeName = "([^,。!?!?、;;::]{1,64}?模式)"; const modePossessiveBoundary = "(?:中的|里的|内的|之中的|的|中|里|内)"; const modeMemberBoundary = `(?:${modePossessiveBoundary}|[,,::])`; const question = /为什么|什么|怎么|如何|介绍|解释|作用|区别|原理|哪些|多少|几(?:度|档|级|个|项)?|是否|能否|可不可以|合适吗|合理吗|会怎样|列出|告诉我|[??]|(?:吗|么|呢)[。.!!]?$/; + const declinedAddition = new RegExp(`^(?:先|暂时)?(?:不要|别|不用|不必|无需|取消|撤销)(?:再|继续)?${addPredicate}`); const candidates = [ { pattern: new RegExp(`^${politePrefix}${prefixUpdateVerb}${modeName}${modeMemberBoundary}[,,::]?(.*)$`), @@ -188,14 +190,29 @@ pattern: new RegExp(`^${politePrefix}${modeName}${modeMemberBoundary}[,,::]?(.+)$`), 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 change = String(matched?.[2] || ""); + const change = String(matched?.[changeIndex] || ""); return Boolean( matched - && normalizeModeName(matched[1]) === normalizeModeName(context.name) + && normalizeModeName(matched[nameIndex]) === normalizeModeName(context.name) && !question.test(change) + && !declinedAddition.test(change) && (!predicateRequired || new RegExp(changePredicate).test(change)) ); }); diff --git a/apps/web-console/firmware-workbench-entry.css b/apps/web-console/firmware-workbench-entry.css new file mode 100644 index 0000000..7c766a2 --- /dev/null +++ b/apps/web-console/firmware-workbench-entry.css @@ -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; } +} diff --git a/apps/web-console/firmware-workbench-entry.js b/apps/web-console/firmware-workbench-entry.js new file mode 100644 index 0000000..cd652b2 --- /dev/null +++ b/apps/web-console/firmware-workbench-entry.js @@ -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 = '座椅固件开发工作台'; + + 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 = ` +
+
+
+ +

座椅固件开发工作台

+
+ +
+ +
`; + + 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); + } +})(); diff --git a/apps/web-console/firmware-workbench.css b/apps/web-console/firmware-workbench.css new file mode 100644 index 0000000..3f0a587 --- /dev/null +++ b/apps/web-console/firmware-workbench.css @@ -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; } +} diff --git a/apps/web-console/firmware-workbench.html b/apps/web-console/firmware-workbench.html new file mode 100644 index 0000000..60ce069 --- /dev/null +++ b/apps/web-console/firmware-workbench.html @@ -0,0 +1,143 @@ + + + + + + + + 座椅固件开发工作台 · 511 工程 + + + + + + +
+
+
+ + + + +

座椅固件开发工作台

+
+
+ 正在连接发布服务 +
+
+ +
+
+
+
+
+

开发指令

+
+ +
+ + +
+ + + +
+
+
当前基线时间控制
+ +
目标固件能力时间控制+位置控制+座椅通风+座椅加热
+
+ +
+
+ +
+
    +
    + +
    当前交付状态尚未开始
    +
    +
    + 等待运行中 + 待确认成功 + 失败取消 + 阻塞 +
    +
    + +
    +
    +
    +
    +

    任务执行日志

    +
    +
    + 等待 + + +
    +
    +
    +
    开始时间
    —
    结束时间
    —
    +
    实际耗时
    —
    文件变更
    —
    +
    +
    +
    +
    暂无执行记录创建开发任务后,这里将实时显示各阶段输出。
    +
    + +
    +
    +
    + +
    +
    +
    +

    固件版本与软件产物

    +
    +
    发布目标车机升级服务器
    +
    +
    + + + +
    软件 / 版本能力构建时间固件文件 / 校验状态操作
    尚未生成固件产物
    +
    +
    +
    +
    + + +
    + + diff --git a/apps/web-console/firmware-workbench.js b/apps/web-console/firmware-workbench.js new file mode 100644 index 0000000..0e9638a --- /dev/null +++ b/apps/web-console/firmware-workbench.js @@ -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) => ` +
  1. + + ${escapeHtml(stage.name)} + ${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""} +
  2. `).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 = `
    当前交付状态${escapeHtml(label)}
    `; + } + + function renderLogs(task) { + const logs = task?.logs || []; + if (!logs.length) { + ui.logs.innerHTML = `
    暂无执行记录创建开发任务后,这里将实时显示各阶段输出。
    `; + model.lastLogCount = 0; + return; + } + const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80; + ui.logs.innerHTML = logs.map((entry) => ` +
    + + +
    ${escapeHtml(entry.title)}

    ${escapeHtml(entry.message)}

    +
    `).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 = ` +
    开始时间
    ${escapeHtml(formatDate(task?.started_at))}
    +
    结束时间
    ${escapeHtml(formatDate(task?.finished_at))}
    +
    实际耗时
    ${escapeHtml(formatDuration(task?.duration_ms))}
    +
    文件变更
    ${fileCount ? `${fileCount} 个文件` : "—"}
    `; + document.querySelectorAll("[data-inspector-tab]").forEach((button) => { + button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab); + }); + if (!task) { + ui.inspector.innerHTML = `
    暂无任务数据
    `; + return; + } + if (model.inspectorTab === "build") { + ui.inspector.innerHTML = task.build_output?.length + ? `
    ${escapeHtml(task.build_output.join("\n"))}
    ` + : `
    尚无编译输出
    `; + return; + } + if (model.inspectorTab === "tests") { + ui.inspector.innerHTML = task.test_results?.length + ? task.test_results.map((test) => `
    ${escapeHtml(test.name)}${escapeHtml(test.summary)}
    `).join("") + : `
    测试尚未执行
    `; + return; + } + ui.inspector.innerHTML = task.file_changes?.length + ? task.file_changes.map((change) => `
    ${change.type === "added" ? "新增" : "修改"}
    ${escapeHtml(change.path)}${escapeHtml(change.summary)}
    `).join("") + : `
    尚无文件变更
    `; + } + + function renderArtifacts() { + const artifacts = model.snapshot.artifacts || []; + if (!artifacts.length) { + ui.artifacts.innerHTML = `
    尚未生成固件产物
    `; + return; + } + ui.artifacts.innerHTML = artifacts.map((artifact) => ` + +
    ${escapeHtml(artifact.software_name)}${escapeHtml(artifact.version)}
    +
    ${(artifact.capabilities || []).map((capability) => `${escapeHtml(capability)}`).join("")}
    + ${escapeHtml(formatDate(artifact.build_time))} +
    ${escapeHtml(artifact.firmware_filename)}${escapeHtml(formatSize(artifact.size_bytes))} · ${escapeHtml(artifact.sha256)}
    + ${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)} +
    + + + + + + + ${artifact.is_baseline ? `受保护` : ``} +
    + `).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 ? `正在提交` : `开始生成`; + 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 = `发布服务已连接`; + ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器"; + render(); + } catch (error) { + ui.server.classList.remove("online"); + ui.server.innerHTML = `发布服务连接失败`; + 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("固件产物详情", `
    +
    软件名称${escapeHtml(artifact.software_name)}
    +
    版本${escapeHtml(artifact.version)}
    +
    固件包${escapeHtml(artifact.firmware_filename)}
    +
    构建时间${escapeHtml(formatDate(artifact.build_time))}
    +
    固件包大小${escapeHtml(formatSize(artifact.size_bytes))}
    +
    发布状态${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}
    +
    SHA-256(固件包)${escapeHtml(artifact.sha256)}
    +
    SHA-256(HEX 镜像)${escapeHtml(artifact.image_sha256)}
    +
    `); + } else if (action === "capabilities") { + const diff = artifact.capability_diff || {}; + showModal("能力差异", `
    +

    固件基线

    ${(artifact.baseline_capabilities || []).map((item) => `

    • ${escapeHtml(item)}

    `).join("")}
    +

    目标固件变化

    ${(diff.added || []).map((item) => `

    + ${escapeHtml(item)}

    `).join("")}${(diff.retained || []).map((item) => `

    ✓ 保留${escapeHtml(item)}

    `).join("") || "

    无新增能力

    "}
    +
    `); + } else if (action === "code") { + showModal("代码差异", `
    ${escapeHtml(artifact.code_diff || "暂无代码差异记录")}
    `); + } else if (action === "validation") { + showModal("验证记录", (artifact.validation_records || []).map((test) => `
    ${escapeHtml(test.name)}${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}
    `).join("") || `
    暂无验证记录
    `); + } + } + + 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(); +})(); diff --git a/apps/web-console/index.html b/apps/web-console/index.html index 315ce40..52d6c43 100755 --- a/apps/web-console/index.html +++ b/apps/web-console/index.html @@ -13,8 +13,9 @@ - + + @@ -26,8 +27,9 @@ - + +
    diff --git a/apps/web-console/service-worker.js b/apps/web-console/service-worker.js index 2dd254a..29c5f44 100644 --- a/apps/web-console/service-worker.js +++ b/apps/web-console/service-worker.js @@ -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 RUNTIME_CACHE = `${CACHE_VERSION}-runtime`; const APP_SHELL = [ @@ -16,10 +16,15 @@ const APP_SHELL = [ "/speech-input.css?v=sensevoice-7", "/speech-input.js?v=sensevoice-13", "/speech-audio-worklet.js?v=sensevoice-5", - "/assistant-chat.css?v=codex-assistant-29", - "/assistant-chat.js?v=codex-assistant-29", + "/assistant-chat.css?v=codex-assistant-30", + "/assistant-chat.js?v=codex-assistant-30", "/energy-monitor.css?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-entrance.png?v=3", "/manifest.webmanifest", diff --git a/config/scene_templates/lay_flat.yaml b/config/scene_templates/lay_flat.yaml new file mode 100644 index 0000000..bdad3ec --- /dev/null +++ b/config/scene_templates/lay_flat.yaml @@ -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: 关闭右后车窗,降低外部噪声。} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/main_cm0plus.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/main_cm0plus.c new file mode 100644 index 0000000..f79c233 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/main_cm0plus.c @@ -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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.c new file mode 100644 index 0000000..52a14d4 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.c @@ -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; +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.h new file mode 100644 index 0000000..e4e67a3 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/safety_manager.h @@ -0,0 +1,30 @@ +#ifndef SAFETY_MANAGER_H +#define SAFETY_MANAGER_H + +#include +#include + +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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.c new file mode 100644 index 0000000..fa5f195 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.c @@ -0,0 +1,1269 @@ +#include "seat_motor_manager.h" + +#include "seat_move_config.h" + +#include +#include + +#define SEAT_MOTOR_RETRY_DELAY_MS (2000ul) +#define SEAT_MOTOR_SPEED_CAL_WARMUP_MS (500ul) +#define SEAT_MOTOR_SPEED_CAL_MEASURE_MS (1000ul) +#define SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT (95u) +#define SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS (70000ul) +#define SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS (500ul) +#define SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS (900ul) +#define SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS (1000ul) +#define SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS (1200ul) +#define SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS (700ul) +#define SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS (1900ul) +#define SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS (1600ul) + +typedef char seat_motor_manager_requires_four_motors[ + (MOTOR_MODE_COUNT == 4) ? 1 : -1]; + +typedef struct +{ + seat_motor_task_state_t state; + seat_motor_task_state_t stateAfterStart; + seat_motor_result_t result; + uint32_t commandSequence; + uint32_t targetValueX100; + uint32_t durationMs; + uint32_t speedPlanDurationMs; + uint32_t deadlineGraceMs; + uint32_t deadlineMs; + uint32_t retryAtMs; + uint32_t speedValuePerSecondX100; + uint32_t timedRawStartEdges; + uint32_t calibrationRawStart; + uint32_t calibrationStartMs; + uint32_t measuredMaxSpeedPpsX100; + motor_direction_t direction; + uint8_t dutyPercent; + uint8_t retryCount; + bool dynamicSpeed; + bool active; +} seat_motor_task_t; + +static seat_motor_task_t tasks[MOTOR_MODE_COUNT]; + +static bool SeatMotor_ModeValid(motor_mode_t mode) +{ + return ((uint32_t)mode < (uint32_t)MOTOR_MODE_COUNT); +} + +static uint32_t SeatMotor_MaxValueX100(motor_mode_t mode) +{ + if(mode == MOTOR_MODE_0_LINEAR) + { + return MOTOR_MODE0_MAX_POSITION_MM_X100; + } + if(mode == MOTOR_MODE_1_ANGLE) + { + return MOTOR_MODE1_MAX_VERTICAL_MM_X100; + } + if(mode == MOTOR_MODE_2_ANGLE) + { + return MOTOR_MODE2_MAX_ANGLE_DEG_X100; + } + if(mode == MOTOR_MODE_3_LINEAR_TRACK) + { + return MOTOR_MODE3_MAX_ANGLE_DEG_X100; + } + return 0ul; +} + +static uint8_t SeatMotor_DefaultDutyPercent(motor_mode_t mode) +{ + if(mode == MOTOR_MODE_0_LINEAR) + { + return SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT; + } + if(mode == MOTOR_MODE_1_ANGLE) + { + return SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT; + } + if(mode == MOTOR_MODE_2_ANGLE) + { + return SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT; + } + if(mode == MOTOR_MODE_3_LINEAR_TRACK) + { + return SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT; + } + return 0u; +} + +static bool SeatMotor_TimeReached(uint32_t now, uint32_t deadline) +{ + return ((int32_t)(now - deadline) >= 0); +} + +static uint32_t SeatMotor_AbsDelta(uint32_t lhs, uint32_t rhs) +{ + return (lhs >= rhs) ? (lhs - rhs) : (rhs - lhs); +} + +static uint32_t SeatMotor_TimedPlanExtensionMs( + motor_mode_t mode, + motor_direction_t direction) +{ + /* The four loaded mechanisms do not have identical low-speed breakaway + * and coast behavior. Bench traces show repeatable early arrival against + * M2 when every axis follows the same ideal time-position line. Extend + * only the speed plan (never the phase deadline) by the measured lead so + * all four axes converge at the requested phase boundary. */ + if(mode == MOTOR_MODE_0_LINEAR) + { + return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ? + SEAT_MOTOR_TIMED_M0_NEAR_EXTENSION_MS : + SEAT_MOTOR_TIMED_M0_FAR_EXTENSION_MS; + } + if(mode == MOTOR_MODE_1_ANGLE) + { + return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ? + SEAT_MOTOR_TIMED_M1_NEAR_EXTENSION_MS : + SEAT_MOTOR_TIMED_M1_FAR_EXTENSION_MS; + } + if(mode == MOTOR_MODE_3_LINEAR_TRACK) + { + return (direction == MOTOR_DIRECTION_TOWARD_NEAR) ? + SEAT_MOTOR_TIMED_M3_NEAR_EXTENSION_MS : + SEAT_MOTOR_TIMED_M3_FAR_EXTENSION_MS; + } + return 0ul; +} + +static bool SeatMotor_TargetWithinHallTolerance( + const seat_motor_task_t *task, + const motor_position_info_t *info) +{ + uint32_t targetHallCount; + + if((NULL == task) || (NULL == info) || + (false == info->positionValid) || (0ul == info->maxValueX100) || + (0ul == info->maxHallCount) || + (task->targetValueX100 > info->maxValueX100)) + { + return false; + } + targetHallCount = (uint32_t) + (((uint64_t)task->targetValueX100 * info->maxHallCount + + (info->maxValueX100 / 2ul)) / info->maxValueX100); + return SeatMotor_AbsDelta(targetHallCount, info->hallCount) <= + SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE; +} + +static bool SeatMotor_DutyValid(uint8_t dutyPercent) +{ + return (dutyPercent >= MOTOR_SPEED_MIN_DUTY_PERCENT) && + (dutyPercent <= MOTOR_SPEED_MAX_DUTY_PERCENT); +} + +static bool SeatMotor_TaskIsPosition(const seat_motor_task_t *task) +{ + return (task->state == SEAT_MOTOR_TASK_POSITION) || + ((task->state == SEAT_MOTOR_TASK_WAIT_START) && + (task->stateAfterStart == SEAT_MOTOR_TASK_POSITION)); +} + +static bool SeatMotor_TaskIsTimed(const seat_motor_task_t *task) +{ + return (task->state == SEAT_MOTOR_TASK_TIMED) || + ((task->state == SEAT_MOTOR_TASK_WAIT_START) && + (task->stateAfterStart == SEAT_MOTOR_TASK_TIMED)); +} + +static void SeatMotor_SetFailed(motor_mode_t mode, + seat_motor_result_t result) +{ + seat_motor_task_t *task = &tasks[(uint32_t)mode]; + + (void)Motor_StopMode(mode); + task->state = SEAT_MOTOR_TASK_FAILED; + task->result = result; + task->active = false; +} + +static void SeatMotor_SetComplete(motor_mode_t mode) +{ + seat_motor_task_t *task = &tasks[(uint32_t)mode]; + + task->state = SEAT_MOTOR_TASK_COMPLETE; + task->result = SEAT_MOTOR_RESULT_OK; + task->active = false; +} + +static void SeatMotor_SetStopped(motor_mode_t mode) +{ + seat_motor_task_t *task = &tasks[(uint32_t)mode]; + + task->state = SEAT_MOTOR_TASK_IDLE; + task->result = SEAT_MOTOR_RESULT_STOPPED; + task->active = false; +} + +static bool SeatMotor_PrepareNewTask(motor_mode_t mode, + uint32_t commandSequence) +{ + seat_motor_task_t *task; + + if((false == SeatMotor_ModeValid(mode)) || + Motor_IsDriverFaultLatched() || + (commandSequence > UINT16_MAX)) + { + return false; + } + if((MOTOR_API_SUCCESS != Motor_StopMode(mode)) || + (MOTOR_API_SUCCESS != Motor_AcknowledgeCommand( + mode, (uint16_t)commandSequence))) + { + return false; + } + task = &tasks[(uint32_t)mode]; + (void)memset(task, 0, sizeof(*task)); + task->commandSequence = commandSequence; + task->result = SEAT_MOTOR_RESULT_RUNNING; + task->active = true; + return true; +} + +static bool SeatMotor_StartPositionTask(motor_mode_t mode, + uint32_t targetValueX100, + uint8_t dutyPercent, + uint32_t speedValuePerSecondX100, + uint32_t durationLimitMs, + uint32_t speedPlanDurationMs, + uint32_t commandSequence, + bool dynamicSpeed, + bool preserveRetry, + seat_motor_task_state_t runningState) +{ + seat_motor_task_t savedTask; + seat_motor_task_t *task; + motor_position_info_t info; + uint32_t travelValueX100; + uint32_t planDurationMs = 0ul; + uint8_t retryCount; + + if((false == SeatMotor_ModeValid(mode)) || + ((runningState != SEAT_MOTOR_TASK_POSITION) && + (runningState != SEAT_MOTOR_TASK_TIMED)) || + (targetValueX100 > SeatMotor_MaxValueX100(mode)) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid)) + { + return false; + } + task = &tasks[(uint32_t)mode]; + retryCount = task->retryCount; + if((false == dynamicSpeed) && (false == SeatMotor_DutyValid(dutyPercent))) + { + return false; + } + travelValueX100 = SeatMotor_AbsDelta(targetValueX100, info.valueX100); + if((false != dynamicSpeed) && + ((0ul == speedValuePerSecondX100) || (0ul == travelValueX100))) + { + if(0ul == travelValueX100) + { + if(false == preserveRetry) + { + if(false == SeatMotor_PrepareNewTask(mode, commandSequence)) + { + return false; + } + } + SeatMotor_SetComplete(mode); + return true; + } + return false; + } + + savedTask = *task; + if((false == preserveRetry) && + (false == SeatMotor_PrepareNewTask(mode, commandSequence))) + { + return false; + } + task = &tasks[(uint32_t)mode]; + if(false != preserveRetry) + { + *task = savedTask; + task->result = SEAT_MOTOR_RESULT_RUNNING; + task->active = true; + retryCount = 1u; + } + + if(false != dynamicSpeed) + { + planDurationMs = (speedPlanDurationMs != 0ul) ? + speedPlanDurationMs : (durationLimitMs != 0ul) ? durationLimitMs : + (uint32_t)((((uint64_t)travelValueX100 * 1000ull) + + speedValuePerSecondX100 - 1ull) / + speedValuePerSecondX100); + if((0ul == planDurationMs) || + (MOTOR_API_SUCCESS != Motor_SetSpeedPlan(mode, + planDurationMs, + travelValueX100))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + } + else if((MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(mode, + dutyPercent))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + + task->stateAfterStart = runningState; + task->state = (durationLimitMs != 0ul) ? + SEAT_MOTOR_TASK_WAIT_START : runningState; + task->targetValueX100 = targetValueX100; + task->durationMs = durationLimitMs; + task->speedPlanDurationMs = planDurationMs; + task->deadlineMs = 0ul; + task->speedValuePerSecondX100 = speedValuePerSecondX100; + task->dutyPercent = dutyPercent; + task->dynamicSpeed = dynamicSpeed; + task->retryCount = retryCount; + task->direction = (targetValueX100 >= info.valueX100) ? + MOTOR_DIRECTION_TOWARD_FAR : + MOTOR_DIRECTION_TOWARD_NEAR; + + if(MOTOR_API_SUCCESS != Motor_StartMoveToValue(mode, targetValueX100)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + return true; +} + +void SeatMotorManager_Init(void) +{ + (void)memset(tasks, 0, sizeof(tasks)); +} + +void SeatMotorManager_Stop(motor_mode_t mode, uint32_t commandSequence) +{ + seat_motor_task_t *task; + + if(false == SeatMotor_ModeValid(mode)) + { + return; + } + (void)Motor_StopMode(mode); + task = &tasks[(uint32_t)mode]; + (void)memset(task, 0, sizeof(*task)); + task->commandSequence = commandSequence; + task->state = SEAT_MOTOR_TASK_IDLE; + task->result = SEAT_MOTOR_RESULT_STOPPED; +} + +void SeatMotorManager_StopAll(uint32_t commandSequence) +{ + uint32_t index; + + for(index = 0ul; index < (uint32_t)MOTOR_MODE_COUNT; index++) + { + SeatMotorManager_Stop((motor_mode_t)index, commandSequence); + } +} + +bool SeatMotorManager_IsAnyActive(void) +{ + uint32_t index; + + for(index = 0ul; index < (uint32_t)MOTOR_MODE_COUNT; index++) + { + if(false != tasks[index].active) + { + return true; + } + } + return false; +} + +static void SeatMotor_ServiceSpeedCalibration(motor_mode_t mode, + const motor_position_info_t *info, + uint32_t now) +{ + seat_motor_task_t *task = &tasks[(uint32_t)mode]; + motor_persistent_state_t rawState; + uint32_t elapsedMs; + uint32_t measuredPpsX100; + + if(task->state == SEAT_MOTOR_TASK_SPEED_CAL_SETTLE) + { + if(false == Motor_IsCommandCurrent( + mode, (uint16_t)task->commandSequence)) + { + SeatMotor_SetStopped(mode); + return; + } + if(Motor_IsMoving(mode)) + { + return; + } + if((0ul == task->measuredMaxSpeedPpsX100) || + (MOTOR_API_SUCCESS != Motor_SetMaximumSpeedPpsX100( + mode, + task->measuredMaxSpeedPpsX100))) + { + if(false == Motor_IsCommandCurrent( + mode, (uint16_t)task->commandSequence)) + { + SeatMotor_SetStopped(mode); + } + else + { + SeatMotor_SetFailed( + mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + } + return; + } + SeatMotor_SetComplete(mode); + return; + } + + if((info->lastEvent == MOTOR_EVENT_STALL) || + (info->lastEvent == MOTOR_EVENT_NEAR_ENDPOINT) || + (info->lastEvent == MOTOR_EVENT_FAR_ENDPOINT) || + (info->lastEvent == MOTOR_EVENT_TIMEOUT) || + (info->lastEvent == MOTOR_EVENT_TARGET_REACHED)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return; + } + if(task->state == SEAT_MOTOR_TASK_WAIT_START) + { + return; + } + if(task->state == SEAT_MOTOR_TASK_SPEED_CAL_WARMUP) + { + if(SeatMotor_TimeReached(now, + task->calibrationStartMs + + SEAT_MOTOR_SPEED_CAL_WARMUP_MS)) + { + if(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &rawState)) + { + SeatMotor_SetFailed(mode, + SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return; + } + task->calibrationRawStart = rawState.rawRisingEdges; + task->calibrationStartMs = now; + task->state = SEAT_MOTOR_TASK_SPEED_CAL_MEASURE; + } + return; + } + + if(SeatMotor_TimeReached(now, + task->calibrationStartMs + + SEAT_MOTOR_SPEED_CAL_MEASURE_MS)) + { + if(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &rawState)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return; + } + elapsedMs = now - task->calibrationStartMs; + measuredPpsX100 = (uint32_t) + (((uint64_t)(rawState.rawRisingEdges - + task->calibrationRawStart) * 100000ull) / + elapsedMs); + if((0ul == measuredPpsX100) || + (MOTOR_API_SUCCESS != Motor_StopMode(mode))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return; + } + task->measuredMaxSpeedPpsX100 = measuredPpsX100; + task->state = SEAT_MOTOR_TASK_SPEED_CAL_SETTLE; + } +} + +void SeatMotorManager_Service(void) +{ + uint32_t index; + uint32_t now; + + Motor_ServiceAll(); + now = Motor_GetTimeMs(); + for(index = 0ul; index < (uint32_t)MOTOR_MODE_COUNT; index++) + { + motor_mode_t mode = (motor_mode_t)index; + seat_motor_task_t *task = &tasks[index]; + motor_position_info_t info; + bool endpointEvent; + + if((false == task->active) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info))) + { + continue; + } + if(Motor_IsDriverFaultLatched() || + (info.lastEvent == MOTOR_EVENT_DRIVER_FAULT)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_DRIVER_FAULT); + continue; + } + if(false == Motor_IsCommandCurrent( + mode, (uint16_t)task->commandSequence)) + { + SeatMotor_SetStopped(mode); + continue; + } + if((info.lastEvent == MOTOR_EVENT_ABORTED) && + (task->state != SEAT_MOTOR_TASK_SPEED_CAL_SETTLE)) + { + SeatMotor_SetStopped(mode); + continue; + } + if((task->state == SEAT_MOTOR_TASK_WAIT_START) && + (info.direction != MOTOR_DIRECTION_STOPPED)) + { + task->state = task->stateAfterStart; + if(task->state == SEAT_MOTOR_TASK_SPEED_CAL_WARMUP) + { + task->calibrationStartMs = now; + } + else if(0ul != task->durationMs) + { + task->deadlineMs = now + task->durationMs + + task->deadlineGraceMs; + } + else + { + task->deadlineMs = 0ul; + } + } + if((task->state == SEAT_MOTOR_TASK_SPEED_CAL_WARMUP) || + (task->state == SEAT_MOTOR_TASK_SPEED_CAL_MEASURE) || + (task->state == SEAT_MOTOR_TASK_SPEED_CAL_SETTLE) || + ((task->state == SEAT_MOTOR_TASK_WAIT_START) && + (task->stateAfterStart == + SEAT_MOTOR_TASK_SPEED_CAL_WARMUP))) + { + SeatMotor_ServiceSpeedCalibration(mode, &info, now); + continue; + } + if(task->state == SEAT_MOTOR_TASK_RETRY_DELAY) + { + if(SeatMotor_TimeReached(now, task->retryAtMs)) + { + if(false == SeatMotor_StartPositionTask( + mode, + task->targetValueX100, + task->dutyPercent, + task->speedValuePerSecondX100, + task->durationMs, + task->speedPlanDurationMs, + task->commandSequence, + task->dynamicSpeed, + true, + task->stateAfterStart)) + { + if(false == Motor_IsCommandCurrent( + mode, + (uint16_t)task->commandSequence)) + { + SeatMotor_SetStopped(mode); + } + else + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_STALL); + } + } + } + continue; + } + endpointEvent = (info.lastEvent == MOTOR_EVENT_NEAR_ENDPOINT) || + (info.lastEvent == MOTOR_EVENT_FAR_ENDPOINT); + if((info.lastEvent == MOTOR_EVENT_STALL) || endpointEvent) + { + /* The low-level safety event has already stopped the hardware. + * At a discrete Hall endpoint, the mechanism may stop up to two + * pulses short of the mathematically rounded target. This is a + * successful position arrival, not a fault/retry condition. */ + if(SeatMotor_TaskIsPosition(task) && + SeatMotor_TargetWithinHallTolerance(task, &info)) + { + SeatMotor_SetComplete(mode); + continue; + } + if(SeatMotor_TaskIsPosition(task) && + (0u == task->retryCount)) + { + if(MOTOR_API_SUCCESS != Motor_StopMode(mode)) + { + SeatMotor_SetFailed( + mode, + Motor_IsDriverFaultLatched() ? + SEAT_MOTOR_RESULT_DRIVER_FAULT : + SEAT_MOTOR_RESULT_INVALID); + continue; + } + task->state = SEAT_MOTOR_TASK_RETRY_DELAY; + task->deadlineMs = 0ul; + task->retryAtMs = now + SEAT_MOTOR_RETRY_DELAY_MS; + task->retryCount = 1u; + } + else if(endpointEvent && SeatMotor_TaskIsTimed(task)) + { + SeatMotor_SetComplete(mode); + } + else + { + SeatMotor_SetFailed( + mode, + endpointEvent ? SEAT_MOTOR_RESULT_ENDPOINT : + SEAT_MOTOR_RESULT_STALL); + } + continue; + } + if(info.lastEvent == MOTOR_EVENT_TARGET_REACHED) + { + SeatMotor_SetComplete(mode); + continue; + } + if(info.lastEvent == MOTOR_EVENT_TIMEOUT) + { + /* Some seat actuators open their internal limit contact instead + * of producing a high-current endpoint event. The low-level + * no-motion timer then reports TIMEOUT even when discrete Hall + * feedback stopped only a few pulses from the requested pose. + * Accept only the same tightly bounded Hall-domain arrival proof + * used for endpoint/stall events; every other timeout remains a + * failure. */ + if(SeatMotor_TaskIsPosition(task) && + SeatMotor_TargetWithinHallTolerance(task, &info)) + { + SeatMotor_SetComplete(mode); + continue; + } + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_TIMEOUT); + continue; + } + if((0ul != task->deadlineMs) && + SeatMotor_TimeReached(now, task->deadlineMs)) + { + if(MOTOR_API_SUCCESS != Motor_StopMode(mode)) + { + SeatMotor_SetFailed( + mode, + Motor_IsDriverFaultLatched() ? + SEAT_MOTOR_RESULT_DRIVER_FAULT : + SEAT_MOTOR_RESULT_INVALID); + continue; + } + if(task->state == SEAT_MOTOR_TASK_TIMED) + { + motor_persistent_state_t rawState; + + /* A duration expiring is not proof that the requested axis + * actually moved. Require at least one Hall edge so an open + * circuit, unpowered bridge or disconnected motor is reported + * as STALL instead of a misleading OK completion. */ + if(MOTOR_API_SUCCESS != + Motor_GetPersistentState(mode, &rawState)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + } + else if(rawState.rawRisingEdges == + task->timedRawStartEdges) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_STALL); + } + else + { + SeatMotor_SetComplete(mode); + } + } + else + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_TIMEOUT); + } + } + } +} + +bool SeatMotor_MoveAbsoluteDuty(motor_mode_t mode, + uint32_t targetValueX100, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + return SeatMotor_StartPositionTask(mode, + targetValueX100, + dutyPercent, + 0ul, + 0ul, + 0ul, + commandSequence, + false, + false, + SEAT_MOTOR_TASK_POSITION); +} + +bool SeatMotor_MoveRelativeDuty(motor_mode_t mode, + int32_t deltaValueX100, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + motor_position_info_t info; + int64_t target; + + if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid)) + { + return false; + } + target = (int64_t)info.valueX100 + deltaValueX100; + if((target < 0ll) || (target > (int64_t)SeatMotor_MaxValueX100(mode))) + { + return false; + } + return SeatMotor_MoveAbsoluteDuty(mode, + (uint32_t)target, + dutyPercent, + commandSequence); +} + +bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode, + uint32_t targetPercentX100, + uint32_t timeoutMs, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + motor_position_info_t info; + uint32_t targetValueX100; + + if((targetPercentX100 > 10000ul) || (0ul == timeoutMs) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid) || (0ul == info.maxValueX100)) + { + return false; + } + targetValueX100 = (uint32_t) + ((((uint64_t)targetPercentX100 * info.maxValueX100) + 5000ull) / + 10000ull); + return SeatMotor_StartPositionTask(mode, + targetValueX100, + dutyPercent, + 0ul, + timeoutMs, + 0ul, + commandSequence, + false, + false, + SEAT_MOTOR_TASK_POSITION); +} + +bool SeatMotor_MoveRelativeNormalized(motor_mode_t mode, + int32_t deltaPercentX100, + uint32_t timeoutMs, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + motor_position_info_t info; + int32_t targetPercentX100; + + if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid)) + { + return false; + } + targetPercentX100 = (int32_t)info.percentX100 + deltaPercentX100; + if(targetPercentX100 < 0) + { + targetPercentX100 = 0; + } + else if(targetPercentX100 > 10000) + { + targetPercentX100 = 10000; + } + else + { + /* Target already lies in the normalized range. */ + } + return SeatMotor_MoveAbsoluteNormalized(mode, + (uint32_t)targetPercentX100, + timeoutMs, + dutyPercent, + commandSequence); +} + +bool SeatMotor_MoveTimedDuty(motor_mode_t mode, + motor_direction_t direction, + uint32_t durationMs, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + seat_motor_task_t *task; + + if((0ul == durationMs) || (false == SeatMotor_DutyValid(dutyPercent)) || + ((direction != MOTOR_DIRECTION_TOWARD_FAR) && + (direction != MOTOR_DIRECTION_TOWARD_NEAR)) || + (false == SeatMotor_PrepareNewTask(mode, commandSequence))) + { + return false; + } + if(MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent(mode, dutyPercent)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task = &tasks[(uint32_t)mode]; + { + motor_persistent_state_t rawState; + + if(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &rawState)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task->timedRawStartEdges = rawState.rawRisingEdges; + } + /* A timed manual movement is valid even before an absolute endpoint has + * been calibrated. It therefore uses the open-loop direction primitive + * and is bounded by duration, Hall stall/endpoint detection and the + * low-level absolute operation timeout. */ + if(MOTOR_API_SUCCESS != Motor_StartOpenLoop(mode, direction)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task->state = SEAT_MOTOR_TASK_WAIT_START; + task->stateAfterStart = SEAT_MOTOR_TASK_TIMED; + task->direction = direction; + task->durationMs = durationMs; + task->deadlineMs = 0ul; + task->dutyPercent = dutyPercent; + return true; +} + +bool SeatMotor_MoveAbsoluteSpeed(motor_mode_t mode, + uint32_t targetValueX100, + uint32_t speedValuePerSecondX100, + uint32_t commandSequence) +{ + return SeatMotor_StartPositionTask(mode, + targetValueX100, + 0u, + speedValuePerSecondX100, + 0ul, + 0ul, + commandSequence, + true, + false, + SEAT_MOTOR_TASK_POSITION); +} + +bool SeatMotor_MoveRelativeSpeed(motor_mode_t mode, + int32_t deltaValueX100, + uint32_t speedValuePerSecondX100, + uint32_t commandSequence) +{ + motor_position_info_t info; + int64_t target; + + if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid)) + { + return false; + } + target = (int64_t)info.valueX100 + deltaValueX100; + if((target < 0ll) || (target > (int64_t)SeatMotor_MaxValueX100(mode))) + { + return false; + } + return SeatMotor_MoveAbsoluteSpeed(mode, + (uint32_t)target, + speedValuePerSecondX100, + commandSequence); +} + +bool SeatMotor_MoveTimedSpeed(motor_mode_t mode, + motor_direction_t direction, + uint32_t durationMs, + uint32_t speedValuePerSecondX100, + uint32_t commandSequence) +{ + seat_motor_task_t *task; + uint32_t travel; + + if((0ul == durationMs) || (0ul == speedValuePerSecondX100) || + ((direction != MOTOR_DIRECTION_TOWARD_FAR) && + (direction != MOTOR_DIRECTION_TOWARD_NEAR)) || + (false == SeatMotor_PrepareNewTask(mode, commandSequence))) + { + return false; + } + travel = (uint32_t)((((uint64_t)speedValuePerSecondX100) * + durationMs) / 1000ull); + if((0ul == travel) || (travel > SeatMotor_MaxValueX100(mode)) || + (MOTOR_API_SUCCESS != Motor_SetSpeedPlan(mode, durationMs, travel))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task = &tasks[(uint32_t)mode]; + { + motor_persistent_state_t rawState; + + if(MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &rawState)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task->timedRawStartEdges = rawState.rawRisingEdges; + } + if(MOTOR_API_SUCCESS != Motor_StartOpenLoop(mode, direction)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_INVALID); + return false; + } + task->state = SEAT_MOTOR_TASK_WAIT_START; + task->stateAfterStart = SEAT_MOTOR_TASK_TIMED; + task->direction = direction; + task->durationMs = durationMs; + task->deadlineMs = 0ul; + task->speedValuePerSecondX100 = speedValuePerSecondX100; + task->dynamicSpeed = true; + return true; +} + +bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode, + uint32_t targetValueX100, + uint32_t durationMs, + uint32_t commandSequence) +{ + return SeatMotor_MoveAbsoluteInTimeWithGrace(mode, + targetValueX100, + durationMs, + 0ul, + commandSequence); +} + +bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode, + uint32_t targetValueX100, + uint32_t durationMs, + uint32_t deadlineGraceMs, + uint32_t commandSequence) +{ + motor_position_info_t info; + uint32_t travel; + uint32_t speed; + uint32_t speedPlanDurationMs; + uint32_t speedPlanExtensionMs; + motor_direction_t direction; + bool started; + + if((0ul == durationMs) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid)) + { + return false; + } + travel = SeatMotor_AbsDelta(targetValueX100, info.valueX100); + if(0ul == travel) + { + return SeatMotor_MoveAbsoluteDuty(mode, + targetValueX100, + MOTOR_SPEED_MIN_DUTY_PERCENT, + commandSequence); + } + direction = (targetValueX100 >= info.valueX100) ? + MOTOR_DIRECTION_TOWARD_FAR : + MOTOR_DIRECTION_TOWARD_NEAR; + speedPlanDurationMs = (durationMs > SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) ? + (durationMs - SEAT_MOTOR_TIMED_ARRIVAL_SETTLE_MS) : durationMs; + speedPlanExtensionMs = SeatMotor_TimedPlanExtensionMs(mode, direction); + if(speedPlanExtensionMs > (UINT32_MAX - speedPlanDurationMs)) + { + return false; + } + speedPlanDurationMs += speedPlanExtensionMs; + speed = (uint32_t) + ((((uint64_t)travel) * 1000ull) / speedPlanDurationMs); + started = SeatMotor_StartPositionTask(mode, + targetValueX100, + 0u, + speed, + durationMs, + speedPlanDurationMs, + commandSequence, + true, + false, + SEAT_MOTOR_TASK_POSITION); + if(started) + { + tasks[(uint32_t)mode].deadlineGraceMs = deadlineGraceMs; + } + return started; +} + +bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode, + uint32_t targetHallCount, + uint8_t dutyPercent, + uint32_t commandSequence) +{ + motor_position_info_t info; + uint32_t targetValueX100; + + if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid) || (0ul == info.maxHallCount) || + (targetHallCount > info.maxHallCount)) + { + return false; + } + targetValueX100 = (uint32_t) + (((uint64_t)targetHallCount * info.maxValueX100 + + (info.maxHallCount / 2ul)) / info.maxHallCount); + return SeatMotor_MoveAbsoluteDuty(mode, + targetValueX100, + dutyPercent, + commandSequence); +} + +bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode, + uint32_t targetHallCount, + uint32_t durationMs, + uint32_t commandSequence) +{ + return SeatMotor_MoveAbsoluteHallInTimeWithGrace(mode, + targetHallCount, + durationMs, + 0ul, + commandSequence); +} + +bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode, + uint32_t targetHallCount, + uint32_t durationMs, + uint32_t deadlineGraceMs, + uint32_t commandSequence) +{ + motor_position_info_t info; + uint32_t targetValueX100; + + if((MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid) || (0ul == info.maxHallCount) || + (targetHallCount > info.maxHallCount)) + { + return false; + } + targetValueX100 = (uint32_t) + (((uint64_t)targetHallCount * info.maxValueX100 + + (info.maxHallCount / 2ul)) / info.maxHallCount); + return SeatMotor_MoveAbsoluteInTimeWithGrace(mode, + targetValueX100, + durationMs, + deadlineGraceMs, + commandSequence); +} + +bool SeatMotor_CalibrateRange(motor_mode_t mode, + uint32_t legTimeoutMs, + uint32_t commandSequence) +{ + motor_calibration_result_t result; + uint8_t dutyPercent; + + if((false == SeatMotor_ModeValid(mode)) || (0ul == legTimeoutMs)) + { + return false; + } + /* The connected four-axis bench needs the same verified loaded duty used + * by normal absolute moves and known-range homing. The protocol minimum + * (50%) cannot reliably start M0/M1 at a loaded endpoint; using it makes + * the first quiet window look like an endpoint and invalidates an + * otherwise healthy calibration. Hall endpoint, timeout and current + * protections remain active throughout both legs. */ + dutyPercent = SeatMotor_DefaultDutyPercent(mode); + if((false == SeatMotor_PrepareNewTask(mode, commandSequence)) || + (MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent( + mode, dutyPercent))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return false; + } + tasks[(uint32_t)mode].state = SEAT_MOTOR_TASK_RANGE_CALIBRATION; + if((MOTOR_API_SUCCESS != Motor_Calibrate(mode, + MOTOR_CALIBRATE_FIND_BOTH, + legTimeoutMs, + &result)) || + (false == result.nearEndpointFound) || + (false == result.farEndpointFound)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return false; + } + SeatMotor_SetComplete(mode); + return true; +} + +bool SeatMotor_HomeKnownRange(motor_mode_t mode, + uint32_t legTimeoutMs, + uint32_t commandSequence) +{ + motor_calibration_result_t result; + uint8_t dutyPercent; + + if((false == SeatMotor_ModeValid(mode)) || (0ul == legTimeoutMs)) + { + return false; + } + dutyPercent = SeatMotor_DefaultDutyPercent(mode); + if((false == SeatMotor_PrepareNewTask(mode, commandSequence)) || + (MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent( + mode, dutyPercent))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return false; + } + tasks[(uint32_t)mode].state = SEAT_MOTOR_TASK_RANGE_CALIBRATION; + if((MOTOR_API_SUCCESS != Motor_Calibrate(mode, + MOTOR_CALIBRATE_FIND_NEAR, + legTimeoutMs, + &result)) || + (false == result.nearEndpointFound)) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return false; + } + SeatMotor_SetComplete(mode); + return true; +} + +bool SeatMotor_StartMaximumSpeedCalibration(motor_mode_t mode, + uint32_t commandSequence) +{ + motor_position_info_t info; + seat_motor_task_t *task; + motor_direction_t direction; + uint32_t target; + + if((false == SeatMotor_ModeValid(mode)) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &info)) || + (false == info.positionValid) || + (false == SeatMotor_PrepareNewTask(mode, commandSequence))) + { + return false; + } + direction = (info.percentX100 <= 5000ul) ? + MOTOR_DIRECTION_TOWARD_FAR : MOTOR_DIRECTION_TOWARD_NEAR; + target = (direction == MOTOR_DIRECTION_TOWARD_FAR) ? + SeatMotor_MaxValueX100(mode) : 0ul; + if((MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent( + mode, SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT)) || + (MOTOR_API_SUCCESS != Motor_StartMoveToValue(mode, target))) + { + SeatMotor_SetFailed(mode, SEAT_MOTOR_RESULT_CALIBRATION_FAILED); + return false; + } + task = &tasks[(uint32_t)mode]; + task->state = SEAT_MOTOR_TASK_WAIT_START; + task->stateAfterStart = SEAT_MOTOR_TASK_SPEED_CAL_WARMUP; + task->direction = direction; + task->dutyPercent = SEAT_MOTOR_SPEED_CAL_DUTY_PERCENT; + task->calibrationStartMs = 0ul; + return true; +} + +bool SeatMotorManager_ClearFault(motor_mode_t mode) +{ + seat_motor_task_t *task; + uint32_t sequence; + + if((false == SeatMotor_ModeValid(mode)) || Motor_IsMoving(mode) || + Motor_IsDriverFaultLatched()) + { + return false; + } + task = &tasks[(uint32_t)mode]; + if(false != task->active) + { + return false; + } + sequence = task->commandSequence; + (void)memset(task, 0, sizeof(*task)); + task->commandSequence = sequence; + return true; +} + +bool SeatMotor_RecoverPosition(motor_mode_t mode, uint32_t savedHallCount) +{ + motor_calibration_result_t calibration; + motor_persistent_state_t state; + uint32_t targetValueX100; + + if((MOTOR_API_SUCCESS != Motor_Calibrate(mode, + MOTOR_CALIBRATE_FIND_NEAR, + SEAT_MOTOR_DEFAULT_CAL_LEG_TIMEOUT_MS, + &calibration)) || + (false == calibration.nearEndpointFound) || + (MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &state))) + { + return false; + } + if(savedHallCount > state.maxHallCount) + { + savedHallCount = state.maxHallCount; + } + targetValueX100 = (uint32_t) + (((uint64_t)savedHallCount * SeatMotor_MaxValueX100(mode) + + (state.maxHallCount / 2ul)) / state.maxHallCount); + if(0ul == targetValueX100) + { + return true; + } + if((MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent( + mode, MOTOR_SPEED_MIN_DUTY_PERCENT)) || + (MOTOR_API_SUCCESS != Motor_MoveToValue(mode, targetValueX100))) + { + return false; + } + return true; +} + +bool SeatMotorManager_GetStatus(motor_mode_t mode, + seat_motor_status_t *status) +{ + motor_position_info_t position; + motor_speed_info_t speed; + motor_persistent_state_t persistent; + const seat_motor_task_t *task; + + if((NULL == status) || (false == SeatMotor_ModeValid(mode)) || + (MOTOR_API_SUCCESS != Motor_GetPositionInfo(mode, &position)) || + (MOTOR_API_SUCCESS != Motor_GetSpeedInfo(mode, &speed)) || + (MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &persistent))) + { + return false; + } + task = &tasks[(uint32_t)mode]; + status->taskState = task->state; + status->result = task->result; + status->lastMotorEvent = position.lastEvent; + status->direction = position.direction; + status->commandSequence = task->commandSequence; + status->valueX100 = position.valueX100; + status->hallCount = position.hallCount; + status->maxHallCount = position.maxHallCount; + status->maxSpeedPpsX100 = persistent.maxSpeedPulsesPerSecondX100; + status->hallSpeedPpsX100 = speed.actualPulsesPerSecondX100; + status->positionPercentX100 = position.percentX100; + status->positionMmX100 = position.positionMmX100; + status->horizontalMmX100 = position.horizontalMmX100; + status->verticalMmX100 = position.verticalMmX100; + status->angleDegX100 = position.angleDegX100; + status->dutyPercent = speed.currentDutyPercent; + status->retryCount = task->retryCount; + status->active = task->active; + status->retried = (task->retryCount != 0u); + status->positionValid = position.positionValid; + return true; +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.h new file mode 100644 index 0000000..7b17eb4 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_motor_manager.h @@ -0,0 +1,154 @@ +#ifndef SEAT_MOTOR_MANAGER_H +#define SEAT_MOTOR_MANAGER_H + +#include +#include + +#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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.c new file mode 100644 index 0000000..0c93c4f --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.c @@ -0,0 +1,1147 @@ +#include "seat_timed_application.h" + +#include +#include + +#include "bsp_seat_move_can.h" +#include "motor_control.h" +#include "safety_manager.h" +#include "seat_motor_manager.h" +#include "seat_move_config.h" + +#define SEAT_TIMED_CLEAR_FAULT_KEY (0xC33Cu) + +typedef enum +{ + SEAT_TIMED_ACK_NONE = 0u, + SEAT_TIMED_ACK_ACCEPTED = 1u, + SEAT_TIMED_ACK_REJECTED = 2u, + SEAT_TIMED_ACK_DUPLICATE = 3u, + SEAT_TIMED_ACK_STOP_ALL_ACCEPTED = 4u +} seat_timed_ack_t; + +typedef enum +{ + SEAT_TIMED_SYSTEM_INIT = 0u, + SEAT_TIMED_SYSTEM_READY = 1u, + SEAT_TIMED_SYSTEM_ACTIVE = 2u, + SEAT_TIMED_SYSTEM_INHIBITED = 3u, + SEAT_TIMED_SYSTEM_FAULT = 4u +} seat_timed_system_state_t; + +typedef enum +{ + SEAT_TIMED_REJECT_NONE = 0u, + SEAT_TIMED_REJECT_BAD_DLC = 1u, + SEAT_TIMED_REJECT_BAD_VERSION = 2u, + SEAT_TIMED_REJECT_BAD_OPCODE = 3u, + SEAT_TIMED_REJECT_BAD_MOTOR = 4u, + SEAT_TIMED_REJECT_BAD_DIRECTION = 5u, + SEAT_TIMED_REJECT_PARAM_RANGE = 6u, + SEAT_TIMED_REJECT_SEQUENCE_OLD = 8u, + SEAT_TIMED_REJECT_BUSY_SAME_MOTOR = 9u, + SEAT_TIMED_REJECT_INTERLOCK = 10u, + SEAT_TIMED_REJECT_FAULT_ACTIVE = 11u, + SEAT_TIMED_REJECT_INTERNAL_ERROR = 14u +} seat_timed_reject_t; + +/* These are the only accepted command opcodes in this image. */ +typedef enum +{ + SEAT_TIMED_OPCODE_INVALID = 0x00u, + SEAT_TIMED_OPCODE_MOVE_TIMED = 0x03u, + SEAT_TIMED_OPCODE_STOP_MOTOR = 0x08u, + SEAT_TIMED_OPCODE_STOP_ALL = 0x09u, + SEAT_TIMED_OPCODE_QUERY_STATUS = 0x0Cu, + SEAT_TIMED_OPCODE_CLEAR_FAULT = 0x0Du +} seat_timed_opcode_t; + +typedef enum +{ + SEAT_TIMED_CAN_TASK_IDLE = 0u, + SEAT_TIMED_CAN_TASK_ACCEPTED = 1u, + SEAT_TIMED_CAN_TASK_TIMED_RUN = 3u, + SEAT_TIMED_CAN_TASK_COMPLETE = 6u, + SEAT_TIMED_CAN_TASK_FAULT = 7u +} seat_timed_can_task_state_t; + +typedef enum +{ + SEAT_TIMED_CAN_RESULT_NONE = 0u, + SEAT_TIMED_CAN_RESULT_RUNNING = 1u, + SEAT_TIMED_CAN_RESULT_OK = 2u, + SEAT_TIMED_CAN_RESULT_STOPPED_BY_CMD = 3u, + SEAT_TIMED_CAN_RESULT_ENDPOINT_REACHED = 4u, + SEAT_TIMED_CAN_RESULT_STALL = 5u, + SEAT_TIMED_CAN_RESULT_TIMEOUT = 6u, + SEAT_TIMED_CAN_RESULT_DRIVER_FAULT = 7u, + SEAT_TIMED_CAN_RESULT_INVALID_CMD = 9u, + SEAT_TIMED_CAN_RESULT_CALIBRATION_FAILED = 11u +} seat_timed_can_result_t; + +typedef struct +{ + uint8_t opcode; + uint8_t motor; + motor_direction_t direction; + uint16_t sequence; + uint16_t paramA; + uint16_t paramB; +} seat_timed_command_t; + +typedef struct +{ + seat_timed_command_t command; + bool valid; +} seat_timed_pending_t; + +volatile uint32_t g_seatTimedRxCommandCount = 0u; +volatile uint32_t g_seatTimedInvalidCommandCount = 0u; +volatile uint32_t g_seatTimedConflictCount = 0u; +volatile uint32_t g_seatTimedAcceptedCount = 0u; +volatile uint32_t g_seatTimedFeedbackTxCount = 0u; +volatile uint32_t g_seatTimedFeedbackTxFailureCount = 0u; +volatile uint8_t g_seatTimedAck = SEAT_TIMED_ACK_NONE; +volatile uint8_t g_seatTimedRejectReason = SEAT_TIMED_REJECT_NONE; +volatile uint8_t g_seatTimedLastOpcode = SEAT_TIMED_OPCODE_INVALID; +volatile uint16_t g_seatTimedLastSequence = 0u; +volatile uint8_t g_seatTimedPendingMask = 0u; + +static const motor_mode_t s_logicalToPhysical[SEAT_MOVE_MOTOR_COUNT] = +{ + (motor_mode_t)SEAT_MOVE_LOGICAL_M0_PHYSICAL_MODE, + (motor_mode_t)SEAT_MOVE_LOGICAL_M1_PHYSICAL_MODE, + (motor_mode_t)SEAT_MOVE_LOGICAL_M2_PHYSICAL_MODE, + (motor_mode_t)SEAT_MOVE_LOGICAL_M3_PHYSICAL_MODE +}; + +static const uint32_t s_maxDurationMs[SEAT_MOVE_MOTOR_COUNT] = +{ + SEAT_TIMED_M0_MAX_DURATION_MS, + SEAT_TIMED_M1_MAX_DURATION_MS, + SEAT_TIMED_M2_MAX_DURATION_MS, + SEAT_TIMED_M3_MAX_DURATION_MS +}; + +static seat_timed_pending_t s_pending[SEAT_MOVE_MOTOR_COUNT]; +static seat_motor_status_t s_lastStatus[SEAT_MOVE_MOTOR_COUNT]; +static bool s_lastStatusValid[SEAT_MOVE_MOTOR_COUNT]; +static bool s_lastSequenceValid = false; +static bool s_lastCommandValid = false; +static bool s_ackNeedsClear = false; +static bool s_inhibitHandled = false; +static seat_timed_command_t s_lastCommand; +static uint16_t s_sequenceReference = 0u; +static bool s_systemStatusPending = true; +static uint8_t s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; +static uint8_t s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; +static bool s_firmwareIdPending = true; +static uint8_t s_dispatchCursor = 0u; +static uint8_t s_motorPollCursor = 0u; +static uint8_t s_diagnosticPollCursor = 0u; +static uint8_t s_motorPeriodicId = 0u; +static uint8_t s_diagnosticPeriodicId = 0u; +static bool s_motorPeriodicPending = false; +static bool s_diagnosticPeriodicPending = false; +static uint8_t s_systemAlive = 0u; +static uint8_t s_diagnosticAlive[SEAT_MOVE_MOTOR_COUNT]; +static uint32_t s_nextStartMs = 0u; +static uint32_t s_nextSystemStatusMs = 0u; +static uint32_t s_nextMotorPollMs = 0u; +static uint32_t s_nextDiagnosticPollMs = 0u; +static uint32_t s_nextFirmwareIdMs = 0u; + +static bool SeatTimed_TimeReached(uint32_t now, uint32_t deadline) +{ + return ((int32_t)(now - deadline) >= 0); +} + +static bool SeatTimed_SequenceNewer(uint16_t candidate, uint16_t reference) +{ + return ((int16_t)(candidate - reference) > 0); +} + +static bool SeatTimed_CommandEqual(const seat_timed_command_t *lhs, + const seat_timed_command_t *rhs) +{ + return (lhs != NULL) && (rhs != NULL) && + (lhs->opcode == rhs->opcode) && + (lhs->motor == rhs->motor) && + (lhs->direction == rhs->direction) && + (lhs->sequence == rhs->sequence) && + (lhs->paramA == rhs->paramA) && + (lhs->paramB == rhs->paramB); +} + +static motor_mode_t SeatTimed_PhysicalMode(uint8_t logicalMotor) +{ + return (logicalMotor < SEAT_MOVE_MOTOR_COUNT) ? + s_logicalToPhysical[logicalMotor] : MOTOR_MODE_COUNT; +} + +static bool SeatTimed_GetStatus(uint8_t logicalMotor, + seat_motor_status_t *status) +{ + if((logicalMotor >= SEAT_MOVE_MOTOR_COUNT) || (status == NULL)) + { + return false; + } + return SeatMotorManager_GetStatus(SeatTimed_PhysicalMode(logicalMotor), + status); +} + +static uint8_t SeatTimed_CommandDutyPercent( + const seat_timed_command_t *command) +{ + uint32_t duty; + + if((command == NULL) || (command->motor >= SEAT_MOVE_MOTOR_COUNT)) + { + return 0u; + } + duty = ((uint32_t)command->paramB + 5u) / 10u; + if(duty < MOTOR_SPEED_MIN_DUTY_PERCENT) + { + duty = MOTOR_SPEED_MIN_DUTY_PERCENT; + } + if(duty > MOTOR_SPEED_MAX_DUTY_PERCENT) + { + duty = MOTOR_SPEED_MAX_DUTY_PERCENT; + } + if((command->motor == 3u) && + (duty > SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT)) + { + duty = SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT; + } + return (uint8_t)duty; +} + +static void SeatTimed_SetResponse(seat_timed_ack_t ack, + seat_timed_reject_t reject, + const seat_timed_command_t *command) +{ + g_seatTimedAck = (uint8_t)ack; + g_seatTimedRejectReason = (uint8_t)reject; + if(command != NULL) + { + g_seatTimedLastOpcode = command->opcode; + g_seatTimedLastSequence = command->sequence; + } + s_ackNeedsClear = (ack != SEAT_TIMED_ACK_NONE); + s_systemStatusPending = true; +} + +static bool SeatTimed_ParseCommand(const bsp_seat_move_can_frame_t *frame, + seat_timed_command_t *command, + seat_timed_reject_t *reject) +{ + uint8_t control; + + if((frame == NULL) || (command == NULL) || (reject == NULL)) + { + return false; + } + (void)memset(command, 0, sizeof(*command)); + command->opcode = (uint8_t)(frame->data0 & 0xFFu); + control = (uint8_t)((frame->data0 >> 8u) & 0xFFu); + command->motor = control & 0x03u; + command->direction = (motor_direction_t)((control >> 2u) & 0x03u); + command->sequence = (uint16_t)((frame->data0 >> 16u) & 0xFFFFu); + command->paramA = (uint16_t)(frame->data1 & 0xFFFFu); + command->paramB = (uint16_t)((frame->data1 >> 16u) & 0xFFFFu); + + if((frame->identifier != SEAT_MOVE_CAN_COMMAND_ID) || + (frame->dlc != SEAT_MOVE_CAN_DLC)) + { + *reject = SEAT_TIMED_REJECT_BAD_DLC; + return false; + } + if((((control >> 4u) & 0x03u) != SEAT_MOVE_CAN_PROTOCOL_VERSION) || + ((control & 0xC0u) != 0u)) + { + *reject = SEAT_TIMED_REJECT_BAD_VERSION; + return false; + } + switch((seat_timed_opcode_t)command->opcode) + { + case SEAT_TIMED_OPCODE_MOVE_TIMED: + case SEAT_TIMED_OPCODE_STOP_MOTOR: + case SEAT_TIMED_OPCODE_STOP_ALL: + case SEAT_TIMED_OPCODE_QUERY_STATUS: + case SEAT_TIMED_OPCODE_CLEAR_FAULT: + return true; + default: + *reject = SEAT_TIMED_REJECT_BAD_OPCODE; + return false; + } +} + +static bool SeatTimed_ParametersValid(const seat_timed_command_t *command, + seat_timed_reject_t *reject) +{ + if((command == NULL) || (reject == NULL)) + { + return false; + } + if(command->motor >= SEAT_MOVE_MOTOR_COUNT) + { + *reject = SEAT_TIMED_REJECT_BAD_MOTOR; + return false; + } + if((command->opcode == SEAT_TIMED_OPCODE_STOP_ALL) && + (command->motor != 0u)) + { + *reject = SEAT_TIMED_REJECT_BAD_MOTOR; + return false; + } + if(command->opcode == SEAT_TIMED_OPCODE_MOVE_TIMED) + { + if((command->direction != MOTOR_DIRECTION_TOWARD_FAR) && + (command->direction != MOTOR_DIRECTION_TOWARD_NEAR)) + { + *reject = SEAT_TIMED_REJECT_BAD_DIRECTION; + return false; + } + if((command->paramA < SEAT_TIMED_MIN_DURATION_MS) || + ((uint32_t)command->paramA > s_maxDurationMs[command->motor]) || + (command->paramB < SEAT_TIMED_LEVEL_MIN_X10) || + (command->paramB > SEAT_TIMED_LEVEL_MAX_X10)) + { + *reject = SEAT_TIMED_REJECT_PARAM_RANGE; + return false; + } + } + else + { + if(command->direction != MOTOR_DIRECTION_STOPPED) + { + *reject = SEAT_TIMED_REJECT_BAD_DIRECTION; + return false; + } + if(command->opcode == SEAT_TIMED_OPCODE_CLEAR_FAULT) + { + if((command->paramA != SEAT_TIMED_CLEAR_FAULT_KEY) || + (command->paramB != 0u)) + { + *reject = SEAT_TIMED_REJECT_PARAM_RANGE; + return false; + } + } + else if((command->paramA != 0u) || (command->paramB != 0u)) + { + *reject = SEAT_TIMED_REJECT_PARAM_RANGE; + return false; + } + } + *reject = SEAT_TIMED_REJECT_NONE; + return true; +} + +static bool SeatTimed_MotorBusy(uint8_t motor) +{ + seat_motor_status_t status; + + return (motor < SEAT_MOVE_MOTOR_COUNT) && + (s_pending[motor].valid || + (SeatTimed_GetStatus(motor, &status) && status.active)); +} + +static uint8_t SeatTimed_ActiveCount(void) +{ + uint8_t motor; + uint8_t count = 0u; + + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + if(SeatTimed_MotorBusy(motor)) + { + count++; + } + } + return count; +} + +static uint8_t SeatTimed_ActiveMask(void) +{ + uint8_t motor; + uint8_t mask = 0u; + + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + if(SeatTimed_MotorBusy(motor)) + { + mask |= (uint8_t)(1u << motor); + } + } + return mask; +} + +static uint8_t SeatTimed_FaultedMask(void) +{ + uint8_t motor; + uint8_t mask = 0u; + + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + seat_motor_status_t status; + + if(SeatTimed_GetStatus(motor, &status) && + ((status.result == SEAT_MOTOR_RESULT_STALL) || + (status.result == SEAT_MOTOR_RESULT_TIMEOUT) || + (status.result == SEAT_MOTOR_RESULT_DRIVER_FAULT) || + (status.result == SEAT_MOTOR_RESULT_INVALID) || + (status.result == SEAT_MOTOR_RESULT_CALIBRATION_FAILED))) + { + mask |= (uint8_t)(1u << motor); + } + } + return mask; +} + +static uint8_t SeatTimed_PositionValidMask(void) +{ + uint8_t motor; + uint8_t mask = 0u; + + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + seat_motor_status_t status; + + if(SeatTimed_GetStatus(motor, &status) && status.positionValid) + { + mask |= (uint8_t)(1u << motor); + } + } + return mask; +} + +static void SeatTimed_StopMotor(uint8_t motor, uint16_t sequence) +{ + motor_mode_t mode; + + if(motor >= SEAT_MOVE_MOTOR_COUNT) + { + return; + } + mode = SeatTimed_PhysicalMode(motor); + Motor_RequestAbortMaskFromIsr((uint8_t)(1u << (uint8_t)mode), sequence); + Motor_ServiceAll(); + (void)Motor_AcknowledgeCommand(mode, sequence); + SeatMotorManager_Stop(mode, sequence); + s_pending[motor].valid = false; + g_seatTimedPendingMask &= (uint8_t)(~(uint8_t)(1u << motor)); + s_motorStatusPendingMask |= (uint8_t)(1u << motor); + s_diagnosticPendingMask |= (uint8_t)(1u << motor); + s_systemStatusPending = true; +} + +void SeatTimedApplication_StopAll(uint16_t sequence) +{ + uint8_t motor; + + Motor_RequestAbortAllFromIsr(sequence); + Motor_ServiceAll(); + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + motor_mode_t mode = SeatTimed_PhysicalMode(motor); + + (void)Motor_AcknowledgeCommand(mode, sequence); + SeatMotorManager_Stop(mode, sequence); + s_pending[motor].valid = false; + } + g_seatTimedPendingMask = 0u; + s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; + s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; + s_systemStatusPending = true; +} + +static void SeatTimed_QueueCommand(const seat_timed_command_t *command) +{ + uint8_t motor = command->motor; + + s_pending[motor].command = *command; + s_pending[motor].valid = true; + g_seatTimedPendingMask |= (uint8_t)(1u << motor); + s_motorStatusPendingMask |= (uint8_t)(1u << motor); + s_systemStatusPending = true; +} + +static void SeatTimed_HandleCommand(const bsp_seat_move_can_frame_t *frame) +{ + seat_timed_command_t command; + seat_timed_reject_t reject = SEAT_TIMED_REJECT_BAD_DLC; + + g_seatTimedRxCommandCount++; + if(!SeatTimed_ParseCommand(frame, &command, &reject) || + !SeatTimed_ParametersValid(&command, &reject)) + { + g_seatTimedInvalidCommandCount++; + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, reject, &command); + return; + } + + if(command.opcode != SEAT_TIMED_OPCODE_STOP_ALL) + { + if(s_lastSequenceValid && + (command.sequence == s_sequenceReference)) + { + if(s_lastCommandValid && + SeatTimed_CommandEqual(&command, &s_lastCommand)) + { + SeatTimed_SetResponse(SEAT_TIMED_ACK_DUPLICATE, + SEAT_TIMED_REJECT_NONE, + &command); + } + else + { + g_seatTimedInvalidCommandCount++; + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_SEQUENCE_OLD, + &command); + } + return; + } + if(s_lastSequenceValid && + !SeatTimed_SequenceNewer(command.sequence, s_sequenceReference)) + { + g_seatTimedInvalidCommandCount++; + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_SEQUENCE_OLD, + &command); + return; + } + } + + s_lastSequenceValid = true; + s_lastCommandValid = true; + s_sequenceReference = command.sequence; + s_lastCommand = command; + + if(command.opcode == SEAT_TIMED_OPCODE_STOP_ALL) + { + SeatTimedApplication_StopAll(command.sequence); + SeatTimed_SetResponse(SEAT_TIMED_ACK_STOP_ALL_ACCEPTED, + SEAT_TIMED_REJECT_NONE, + &command); + g_seatTimedAcceptedCount++; + return; + } + if(command.opcode == SEAT_TIMED_OPCODE_STOP_MOTOR) + { + SeatTimed_StopMotor(command.motor, command.sequence); + SeatTimed_SetResponse(SEAT_TIMED_ACK_ACCEPTED, + SEAT_TIMED_REJECT_NONE, + &command); + g_seatTimedAcceptedCount++; + return; + } + if(command.opcode == SEAT_TIMED_OPCODE_QUERY_STATUS) + { + uint8_t bit = (uint8_t)(1u << command.motor); + + s_motorStatusPendingMask |= bit; + s_diagnosticPendingMask |= bit; + SeatTimed_SetResponse(SEAT_TIMED_ACK_ACCEPTED, + SEAT_TIMED_REJECT_NONE, + &command); + g_seatTimedAcceptedCount++; + return; + } + if(command.opcode == SEAT_TIMED_OPCODE_CLEAR_FAULT) + { + if(SeatTimed_MotorBusy(command.motor)) + { + g_seatTimedConflictCount++; + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_BUSY_SAME_MOTOR, + &command); + return; + } + if(SafetyManager_IsInhibited() || Motor_IsDriverFaultLatched() || + !SeatMotorManager_ClearFault( + SeatTimed_PhysicalMode(command.motor))) + { + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_FAULT_ACTIVE, + &command); + return; + } + s_motorStatusPendingMask |= (uint8_t)(1u << command.motor); + s_diagnosticPendingMask |= (uint8_t)(1u << command.motor); + SeatTimed_SetResponse(SEAT_TIMED_ACK_ACCEPTED, + SEAT_TIMED_REJECT_NONE, + &command); + g_seatTimedAcceptedCount++; + return; + } + + if(SafetyManager_IsInhibited()) + { + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_FAULT_ACTIVE, + &command); + return; + } + if(SeatTimed_MotorBusy(command.motor)) + { + /* A newer timed command replaces only the selected axis. */ + SeatTimed_StopMotor(command.motor, command.sequence); + } + else if(SeatTimed_ActiveCount() >= SEAT_MOVE_MAX_CONCURRENT_MOTORS) + { + g_seatTimedConflictCount++; + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_INTERLOCK, + &command); + return; + } + + SeatTimed_QueueCommand(&command); + SeatTimed_SetResponse(SEAT_TIMED_ACK_ACCEPTED, + SEAT_TIMED_REJECT_NONE, + &command); + g_seatTimedAcceptedCount++; +} + +static bool SeatTimed_DispatchCommand(const seat_timed_command_t *command) +{ + /* No position, relative, homing, calibration or dynamic dispatch path + * exists in this image. */ + if((command == NULL) || + (command->opcode != SEAT_TIMED_OPCODE_MOVE_TIMED)) + { + return false; + } + return SeatMotor_MoveTimedDuty(SeatTimed_PhysicalMode(command->motor), + command->direction, + command->paramA, + SeatTimed_CommandDutyPercent(command), + command->sequence); +} + +static void SeatTimed_ServiceDispatcher(void) +{ + uint32_t now = Motor_GetTimeMs(); + uint8_t checked; + + if(SafetyManager_IsInhibited() || + !SeatTimed_TimeReached(now, s_nextStartMs)) + { + return; + } + for(checked = 0u; checked < SEAT_MOVE_MOTOR_COUNT; checked++) + { + uint8_t motor = (uint8_t) + ((s_dispatchCursor + checked) % SEAT_MOVE_MOTOR_COUNT); + + if(s_pending[motor].valid) + { + seat_timed_command_t command = s_pending[motor].command; + + s_pending[motor].valid = false; + g_seatTimedPendingMask &= (uint8_t)(~(uint8_t)(1u << motor)); + s_dispatchCursor = (uint8_t)((motor + 1u) % + SEAT_MOVE_MOTOR_COUNT); + s_nextStartMs = now + SEAT_MOVE_START_STAGGER_MS; + if(!SeatTimed_DispatchCommand(&command)) + { + SeatTimed_SetResponse(SEAT_TIMED_ACK_REJECTED, + SEAT_TIMED_REJECT_INTERNAL_ERROR, + &command); + } + s_motorStatusPendingMask |= (uint8_t)(1u << motor); + s_diagnosticPendingMask |= (uint8_t)(1u << motor); + s_systemStatusPending = true; + break; + } + } +} + +static bool SeatTimed_StatusChanged(const seat_motor_status_t *lhs, + const seat_motor_status_t *rhs) +{ + return (lhs->taskState != rhs->taskState) || + (lhs->result != rhs->result) || + (lhs->lastMotorEvent != rhs->lastMotorEvent) || + (lhs->direction != rhs->direction) || + (lhs->commandSequence != rhs->commandSequence) || + (lhs->dutyPercent != rhs->dutyPercent) || + (lhs->active != rhs->active) || + (lhs->retryCount != rhs->retryCount) || + (lhs->positionValid != rhs->positionValid); +} + +static void SeatTimed_ServiceStateChanges(void) +{ + uint8_t motor; + + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + seat_motor_status_t status; + + if(SeatTimed_GetStatus(motor, &status) && + (!s_lastStatusValid[motor] || + SeatTimed_StatusChanged(&status, &s_lastStatus[motor]))) + { + s_lastStatus[motor] = status; + s_lastStatusValid[motor] = true; + s_motorStatusPendingMask |= (uint8_t)(1u << motor); + s_diagnosticPendingMask |= (uint8_t)(1u << motor); + s_systemStatusPending = true; + } + } +} + +static uint8_t SeatTimed_MapTaskState(const seat_motor_status_t *status) +{ + switch(status->taskState) + { + case SEAT_MOTOR_TASK_IDLE: + return SEAT_TIMED_CAN_TASK_IDLE; + case SEAT_MOTOR_TASK_WAIT_START: + case SEAT_MOTOR_TASK_RETRY_DELAY: + return SEAT_TIMED_CAN_TASK_ACCEPTED; + case SEAT_MOTOR_TASK_TIMED: + return SEAT_TIMED_CAN_TASK_TIMED_RUN; + case SEAT_MOTOR_TASK_COMPLETE: + return SEAT_TIMED_CAN_TASK_COMPLETE; + case SEAT_MOTOR_TASK_POSITION: + case SEAT_MOTOR_TASK_RANGE_CALIBRATION: + case SEAT_MOTOR_TASK_SPEED_CAL_WARMUP: + case SEAT_MOTOR_TASK_SPEED_CAL_MEASURE: + case SEAT_MOTOR_TASK_SPEED_CAL_SETTLE: + case SEAT_MOTOR_TASK_FAILED: + default: + return SEAT_TIMED_CAN_TASK_FAULT; + } +} + +static uint8_t SeatTimed_MapTaskResult(const seat_motor_status_t *status) +{ + switch(status->result) + { + case SEAT_MOTOR_RESULT_NONE: + return SEAT_TIMED_CAN_RESULT_NONE; + case SEAT_MOTOR_RESULT_RUNNING: + return SEAT_TIMED_CAN_RESULT_RUNNING; + case SEAT_MOTOR_RESULT_OK: + return SEAT_TIMED_CAN_RESULT_OK; + case SEAT_MOTOR_RESULT_STOPPED: + return SEAT_TIMED_CAN_RESULT_STOPPED_BY_CMD; + case SEAT_MOTOR_RESULT_ENDPOINT: + return SEAT_TIMED_CAN_RESULT_ENDPOINT_REACHED; + case SEAT_MOTOR_RESULT_STALL: + return SEAT_TIMED_CAN_RESULT_STALL; + case SEAT_MOTOR_RESULT_TIMEOUT: + return SEAT_TIMED_CAN_RESULT_TIMEOUT; + case SEAT_MOTOR_RESULT_DRIVER_FAULT: + return SEAT_TIMED_CAN_RESULT_DRIVER_FAULT; + case SEAT_MOTOR_RESULT_CALIBRATION_FAILED: + return SEAT_TIMED_CAN_RESULT_CALIBRATION_FAILED; + case SEAT_MOTOR_RESULT_INVALID: + default: + return SEAT_TIMED_CAN_RESULT_INVALID_CMD; + } +} + +static uint8_t SeatTimed_SystemState(void) +{ + if(SafetyManager_IsInhibited()) + { + return (SafetyManager_GetFault() == SAFETY_FAULT_NONE) ? + SEAT_TIMED_SYSTEM_INHIBITED : SEAT_TIMED_SYSTEM_FAULT; + } + if((SeatTimed_FaultedMask() != 0u) || + (SafetyManager_GetFault() != SAFETY_FAULT_NONE)) + { + return SEAT_TIMED_SYSTEM_FAULT; + } + if(SeatTimed_ActiveMask() != 0u) + { + return SEAT_TIMED_SYSTEM_ACTIVE; + } + return SEAT_TIMED_SYSTEM_READY; +} + +static bool SeatTimed_SendSystemStatus(void) +{ + uint8_t activeMask = SeatTimed_ActiveMask(); + uint8_t faultedMask = SeatTimed_FaultedMask(); + uint8_t positionValidMask = SeatTimed_PositionValidMask(); + bool faultSummary = (faultedMask != 0u) || + (SafetyManager_GetFault() != SAFETY_FAULT_NONE); + uint8_t byte0 = (uint8_t)((g_seatTimedAck & 0x0Fu) | + ((SeatTimed_SystemState() & 0x0Fu) << 4u)); + uint8_t byte1 = g_seatTimedRejectReason; + uint8_t byte4 = g_seatTimedLastOpcode; + uint8_t byte5 = (uint8_t)((activeMask & 0x0Fu) | + (SafetyManager_IsInhibited() ? 0x10u : 0u) | + (faultSummary ? 0x80u : 0u)); + uint8_t byte6 = (uint8_t)((faultedMask & 0x0Fu) | + ((positionValidMask & 0x0Fu) << 4u)); + uint8_t byte7 = (uint8_t)((s_systemAlive & 0x0Fu) | + ((SEAT_MOVE_FIRMWARE_REVISION & 0x0Fu) << 4u)); + uint32_t data0 = (uint32_t)byte0 | + ((uint32_t)byte1 << 8u) | + ((uint32_t)g_seatTimedLastSequence << 16u); + uint32_t data1 = (uint32_t)byte4 | + ((uint32_t)byte5 << 8u) | + ((uint32_t)byte6 << 16u) | + ((uint32_t)byte7 << 24u); + + return BspSeatMoveCan_Send(SEAT_MOVE_CAN_SYSTEM_STATUS_ID, data0, data1); +} + +static bool SeatTimed_SendMotorStatus(uint8_t motor) +{ + seat_motor_status_t status; + seat_timed_command_t *pendingCommand = &s_pending[motor].command; + uint8_t taskState; + uint8_t result; + uint8_t direction; + uint8_t flags; + uint8_t duty; + uint16_t sequence; + uint16_t position; + uint32_t data0; + uint32_t data1; + + if(!SeatTimed_GetStatus(motor, &status)) + { + return false; + } + taskState = SeatTimed_MapTaskState(&status); + result = SeatTimed_MapTaskResult(&status); + direction = (uint8_t)status.direction; + duty = status.active ? status.dutyPercent : 0u; + sequence = (uint16_t)status.commandSequence; + if(s_pending[motor].valid) + { + taskState = SEAT_TIMED_CAN_TASK_ACCEPTED; + result = SEAT_TIMED_CAN_RESULT_RUNNING; + direction = (uint8_t)pendingCommand->direction; + sequence = pendingCommand->sequence; + duty = SeatTimed_CommandDutyPercent(pendingCommand); + } + flags = (uint8_t)((direction & 0x03u) | + ((status.active || s_pending[motor].valid) ? 0x04u : 0u) | + (status.positionValid ? 0x08u : 0u) | + ((status.positionValid && (status.hallCount == 0u)) ? 0x10u : 0u) | + ((status.positionValid && (status.maxHallCount != 0u) && + (status.hallCount >= status.maxHallCount)) ? 0x20u : 0u) | + ((status.result == SEAT_MOTOR_RESULT_STALL) ? 0x40u : 0u) | + (((status.result == SEAT_MOTOR_RESULT_DRIVER_FAULT) || + Motor_IsDriverFaultLatched()) ? 0x80u : 0u)); + position = status.positionValid ? + (uint16_t)((status.positionPercentX100 > 10000u) ? + 10000u : status.positionPercentX100) : UINT16_MAX; + data0 = (uint32_t)motor | + ((uint32_t)((taskState & 0x0Fu) | + ((result & 0x0Fu) << 4u)) << 8u) | + ((uint32_t)flags << 16u) | + ((uint32_t)(sequence & 0xFFu) << 24u); + data1 = (uint32_t)(sequence >> 8u) | + ((uint32_t)position << 8u) | + ((uint32_t)duty << 24u); + return BspSeatMoveCan_Send(SEAT_MOVE_CAN_MOTOR_STATUS_ID, data0, data1); +} + +static bool SeatTimed_SendCurrentDiagnostic(uint8_t motor) +{ + motor_current_info_t current; + uint8_t flags; + uint32_t data0; + uint32_t data1; + + if((motor >= SEAT_MOVE_MOTOR_COUNT) || + (MOTOR_API_SUCCESS != Motor_GetCurrentInfo( + SeatTimed_PhysicalMode(motor), ¤t))) + { + return false; + } + flags = (uint8_t) + ((current.sampleValid ? 0x01u : 0u) | + (current.baselineValid ? 0x02u : 0u) | + (current.highCurrent ? 0x04u : 0u) | + (current.hallActivityLow ? 0x08u : 0u) | + (current.calibrating ? 0x10u : 0u)); + data0 = (uint32_t)motor | + ((uint32_t)flags << 8u) | + ((uint32_t)current.raw << 16u); + data1 = (uint32_t)current.baselineRaw | + ((uint32_t)current.stallThresholdRaw << 16u); + return BspSeatMoveCan_Send(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID, + data0, + data1); +} + +static bool SeatTimed_SendDiagnostic(uint8_t motor) +{ + seat_motor_status_t status; + uint16_t hallCount; + uint16_t maxHallCount; + uint32_t speedRaw; + uint8_t faultFlags; + uint8_t byte1; + uint8_t byte7; + uint32_t data0; + uint32_t data1; + + if(!SeatTimed_GetStatus(motor, &status)) + { + return false; + } + hallCount = (status.hallCount > UINT16_MAX) ? + UINT16_MAX : (uint16_t)status.hallCount; + maxHallCount = (status.maxHallCount > UINT16_MAX) ? + UINT16_MAX : (uint16_t)status.maxHallCount; + speedRaw = (status.hallSpeedPpsX100 + 25u) / 50u; + if(speedRaw > UINT8_MAX) + { + speedRaw = UINT8_MAX; + } + faultFlags = (uint8_t) + (((status.result == SEAT_MOTOR_RESULT_STALL) ? 0x01u : 0u) | + (((status.result == SEAT_MOTOR_RESULT_DRIVER_FAULT) || + Motor_IsDriverFaultLatched()) ? 0x02u : 0u) | + ((status.result == SEAT_MOTOR_RESULT_INVALID) ? 0x04u : 0u) | + ((status.result == SEAT_MOTOR_RESULT_CALIBRATION_FAILED) ? + 0x08u : 0u)); + byte1 = (uint8_t)(((uint8_t)status.lastMotorEvent & 0x0Fu) | + ((faultFlags & 0x0Fu) << 4u)); + byte7 = (uint8_t)((status.retryCount & 0x0Fu) | + ((s_diagnosticAlive[motor] & 0x0Fu) << 4u)); + data0 = (uint32_t)motor | + ((uint32_t)byte1 << 8u) | + ((uint32_t)hallCount << 16u); + data1 = (uint32_t)maxHallCount | + ((uint32_t)((uint8_t)speedRaw) << 16u) | + ((uint32_t)byte7 << 24u); + if(!BspSeatMoveCan_Send(SEAT_MOVE_CAN_DIAGNOSTIC_ID, data0, data1)) + { + return false; + } + return SeatTimed_SendCurrentDiagnostic(motor); +} + +static bool SeatTimed_SendFirmwareIdentity(void) +{ + /* Bytes 0..3 are ASCII "PST1" (Passenger Seat Timed v1). */ + uint32_t data0 = (uint32_t)0x50u | + ((uint32_t)0x53u << 8u) | + ((uint32_t)0x54u << 16u) | + ((uint32_t)0x31u << 24u); + uint32_t data1 = (uint32_t)SEAT_MOVE_FIRMWARE_REVISION | + ((uint32_t)SEAT_MOVE_FIRMWARE_BUILD_ID << 8u) | + ((uint32_t)SEAT_MOVE_MOTOR_COUNT << 16u) | + ((uint32_t)SEAT_MOVE_CAN_PROTOCOL_VERSION << 24u); + + return BspSeatMoveCan_Send(SEAT_MOVE_CAN_FIRMWARE_ID_ID, data0, data1); +} + +static void SeatTimed_ServiceFeedback(void) +{ + uint32_t now = Motor_GetTimeMs(); + uint8_t motor; + + if(SeatTimed_TimeReached(now, s_nextSystemStatusMs)) + { + s_systemStatusPending = true; + s_nextSystemStatusMs = now + SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS; + } + if(SeatTimed_TimeReached(now, s_nextFirmwareIdMs)) + { + s_firmwareIdPending = true; + s_nextFirmwareIdMs = now + SEAT_MOVE_FIRMWARE_ID_PERIOD_MS; + } + if(!s_motorPeriodicPending && + SeatTimed_TimeReached(now, s_nextMotorPollMs)) + { + s_motorPeriodicId = s_motorPollCursor; + s_motorStatusPendingMask |= (uint8_t)(1u << s_motorPeriodicId); + s_motorPeriodicPending = true; + } + if(!s_diagnosticPeriodicPending && + SeatTimed_TimeReached(now, s_nextDiagnosticPollMs)) + { + s_diagnosticPeriodicId = s_diagnosticPollCursor; + s_diagnosticPendingMask |= (uint8_t)(1u << s_diagnosticPeriodicId); + s_diagnosticPeriodicPending = true; + } + + if(s_systemStatusPending) + { + if(SeatTimed_SendSystemStatus()) + { + s_systemStatusPending = false; + s_systemAlive = (uint8_t)((s_systemAlive + 1u) & 0x0Fu); + if(s_ackNeedsClear) + { + g_seatTimedAck = SEAT_TIMED_ACK_NONE; + g_seatTimedRejectReason = SEAT_TIMED_REJECT_NONE; + s_ackNeedsClear = false; + } + g_seatTimedFeedbackTxCount++; + } + else + { + g_seatTimedFeedbackTxFailureCount++; + } + return; + } + if(s_firmwareIdPending) + { + if(SeatTimed_SendFirmwareIdentity()) + { + s_firmwareIdPending = false; + g_seatTimedFeedbackTxCount++; + } + else + { + g_seatTimedFeedbackTxFailureCount++; + } + return; + } + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + uint8_t bit = (uint8_t)(1u << motor); + + if((s_motorStatusPendingMask & bit) != 0u) + { + if(SeatTimed_SendMotorStatus(motor)) + { + s_motorStatusPendingMask &= (uint8_t)(~bit); + if(s_motorPeriodicPending && + (motor == s_motorPeriodicId)) + { + s_motorPollCursor = (uint8_t)((s_motorPollCursor + 1u) % + SEAT_MOVE_MOTOR_COUNT); + s_motorPeriodicPending = false; + s_nextMotorPollMs = now + SEAT_MOVE_MOTOR_STATUS_POLL_MS; + } + g_seatTimedFeedbackTxCount++; + } + else + { + g_seatTimedFeedbackTxFailureCount++; + } + return; + } + } + for(motor = 0u; motor < SEAT_MOVE_MOTOR_COUNT; motor++) + { + uint8_t bit = (uint8_t)(1u << motor); + + if((s_diagnosticPendingMask & bit) != 0u) + { + if(SeatTimed_SendDiagnostic(motor)) + { + s_diagnosticPendingMask &= (uint8_t)(~bit); + s_diagnosticAlive[motor] = (uint8_t) + ((s_diagnosticAlive[motor] + 1u) & 0x0Fu); + if(s_diagnosticPeriodicPending && + (motor == s_diagnosticPeriodicId)) + { + s_diagnosticPollCursor = (uint8_t) + ((s_diagnosticPollCursor + 1u) % + SEAT_MOVE_MOTOR_COUNT); + s_diagnosticPeriodicPending = false; + s_nextDiagnosticPollMs = now + + SEAT_MOVE_DIAGNOSTIC_POLL_MS; + } + g_seatTimedFeedbackTxCount++; + } + else + { + g_seatTimedFeedbackTxFailureCount++; + } + return; + } + } +} + +bool SeatTimedApplication_Init(void) +{ + uint32_t now; + + (void)memset(s_pending, 0, sizeof(s_pending)); + (void)memset(s_lastStatus, 0, sizeof(s_lastStatus)); + (void)memset(s_lastStatusValid, 0, sizeof(s_lastStatusValid)); + (void)memset(s_diagnosticAlive, 0, sizeof(s_diagnosticAlive)); + SeatMotorManager_Init(); + if(!BspSeatMoveCan_Init()) + { + SafetyManager_LatchFault(SAFETY_FAULT_CAN_INIT); + return false; + } + + now = Motor_GetTimeMs(); + s_nextStartMs = now; + s_nextSystemStatusMs = now; + s_nextMotorPollMs = now; + s_nextDiagnosticPollMs = now; + s_nextFirmwareIdMs = now; + s_systemStatusPending = true; + s_motorStatusPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; + s_diagnosticPendingMask = SEAT_MOVE_ACTIVE_MOTOR_MASK; + s_firmwareIdPending = true; + return true; +} + +void SeatTimedApplication_Service(void) +{ + bsp_seat_move_can_frame_t frame; + + if(g_bspSeatMoveCanBusOff != 0u) + { + SafetyManager_LatchFault(SAFETY_FAULT_CAN_BUS_OFF); + } + SafetyManager_Service(); + if(SafetyManager_IsInhibited()) + { + if(!s_inhibitHandled) + { + SeatTimedApplication_StopAll(g_seatTimedLastSequence); + s_inhibitHandled = true; + } + } + else + { + s_inhibitHandled = false; + } + + while(BspSeatMoveCan_Read(&frame)) + { + if(frame.identifier == SEAT_MOVE_CAN_COMMAND_ID) + { + SeatTimed_HandleCommand(&frame); + } + } + SeatTimed_ServiceDispatcher(); + SeatMotorManager_Service(); + SeatTimed_ServiceStateChanges(); + SeatTimed_ServiceFeedback(); +} + +/* The low-level driver declares this hook for blocking service primitives. + * This timed-only application never invokes those primitives, but supplies + * the hook so STOP_ALL/status remain safe if a diagnostic build calls one. */ +void Motor_CalibrationServiceHook(void) +{ + bsp_seat_move_can_frame_t frame; + + while(BspSeatMoveCan_Read(&frame)) + { + if(frame.identifier == SEAT_MOVE_CAN_COMMAND_ID) + { + SeatTimed_HandleCommand(&frame); + } + } + if(g_bspSeatMoveCanBusOff != 0u) + { + SafetyManager_LatchFault(SAFETY_FAULT_CAN_BUS_OFF); + } + SafetyManager_Service(); + SeatTimed_ServiceStateChanges(); + SeatTimed_ServiceFeedback(); +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.h new file mode 100644 index 0000000..2a253a1 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.h @@ -0,0 +1,11 @@ +#ifndef SEAT_TIMED_APPLICATION_H +#define SEAT_TIMED_APPLICATION_H + +#include +#include + +bool SeatTimedApplication_Init(void); +void SeatTimedApplication_Service(void); +void SeatTimedApplication_StopAll(uint16_t sequence); + +#endif /* SEAT_TIMED_APPLICATION_H */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.c new file mode 100644 index 0000000..9482008 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.c @@ -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); +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.h new file mode 100644 index 0000000..c9f33da --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_backrest_heater.h @@ -0,0 +1,62 @@ +#ifndef BSP_SEAT_BACKREST_HEATER_H +#define BSP_SEAT_BACKREST_HEATER_H + +#include +#include + +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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.c new file mode 100644 index 0000000..8571897 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.c @@ -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); +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.h new file mode 100644 index 0000000..e1f5567 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_current_adc.h @@ -0,0 +1,27 @@ +#ifndef BSP_SEAT_CURRENT_ADC_H +#define BSP_SEAT_CURRENT_ADC_H + +#include +#include + +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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.c new file mode 100644 index 0000000..018f767 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.c @@ -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); +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.h new file mode 100644 index 0000000..abcdc15 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_fan.h @@ -0,0 +1,52 @@ +#ifndef BSP_SEAT_FAN_H +#define BSP_SEAT_FAN_H + +#include +#include + +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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.c new file mode 100644 index 0000000..7f15ed9 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.c @@ -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; + } +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.h new file mode 100644 index 0000000..98106a7 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Bsp/bsp_seat_move_can.h @@ -0,0 +1,29 @@ +#ifndef BSP_SEAT_MOVE_CAN_H +#define BSP_SEAT_MOVE_CAN_H + +#include +#include + +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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_backrest_heater_config.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_backrest_heater_config.h new file mode 100644 index 0000000..f7e2412 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_backrest_heater_config.h @@ -0,0 +1,155 @@ +#ifndef SEAT_BACKREST_HEATER_CONFIG_H +#define SEAT_BACKREST_HEATER_CONFIG_H + +#include "cy_project.h" +#include "cy_device_headers.h" + +/* + * Schematic-confirmed backrest-heater path: + * P16.7 / KBJRD -> R126 (1 kOhm) -> BTS7010-1EPA IN -> backrest heater. + * The rev_d SDK maps P16.7 to TCPWM1 line 65. + */ +#define SEAT_BACKREST_HEATER_PWM_PORT GPIO_PRT16 +#define SEAT_BACKREST_HEATER_PWM_PIN (7u) +#define SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM P16_7_GPIO +#define SEAT_BACKREST_HEATER_PWM_HSIOM P16_7_TCPWM1_LINE65 +#define SEAT_BACKREST_HEATER_PWM_TCPWM TCPWM1_GRP0_CNT65 +#define SEAT_BACKREST_HEATER_PWM_PCLK PCLK_TCPWM1_CLOCKS65 + +#define SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER (3u) +#define SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ (2000000u) +#define SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ (100u) +#define SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS \ + (SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ / \ + SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ) + +/* Three open-loop power levels. Tune only after thermal validation. */ +#define SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT (30u) +#define SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT (60u) +#define SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT (90u) +#define SEAT_BACKREST_HEATER_DEFAULT_LEVEL (0u) + +#define SEAT_BACKREST_HEATER_TASK_PERIOD_US (1000u) +/* + * Do not sample at the 10 ms PWM period. Equal periods phase-lock the ADC + * to one PWM point and can report zero current continuously for a healthy + * low-duty heater. Seven milliseconds walks through every PWM phase while + * remaining slow enough for the existing SAR ADC service. + */ +#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (7u) +#define SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS (3u) +#define SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS (600000u) + +/* + * Vehicle CAN4 wiring proven by D:\wcl\can and Seat_Fan_demo: + * P3.7 RX, P3.6 TX, P3.1 high enables the board TJA1044 circuit. + */ +#define SEAT_BACKREST_HEATER_CAN_TYPE CY_CANFD1_2_TYPE +#define SEAT_BACKREST_HEATER_CAN_RX_PORT GPIO_PRT3 +#define SEAT_BACKREST_HEATER_CAN_RX_PIN (7u) +#define SEAT_BACKREST_HEATER_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2 +#define SEAT_BACKREST_HEATER_CAN_TX_PORT GPIO_PRT3 +#define SEAT_BACKREST_HEATER_CAN_TX_PIN (6u) +#define SEAT_BACKREST_HEATER_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2 +#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT GPIO_PRT3 +#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN (1u) +#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO +#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE (1u) +#define SEAT_BACKREST_HEATER_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2 +#define SEAT_BACKREST_HEATER_CAN_IRQN canfd_1_interrupts0_2_IRQn +#define SEAT_BACKREST_HEATER_CAN_CPU_IRQ CPUIntIdx2_IRQn +#define SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER (0u) +#define SEAT_BACKREST_HEATER_CAN_CLOCK_HZ (40000000u) +#define SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT (4u) +#define SEAT_BACKREST_HEATER_CAN_RX_FIFO0_ELEMENTS (8u) +#define SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH (8u) + +/* Classic CAN matrix messages, 11-bit IDs, 8-byte payloads, Intel bit order. */ +#define SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID (0x29Eu) +#define SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID (0x2AAu) +#define SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID (0x3CEu) +#define SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID (0x42Au) +#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID (0x4FAu) +#define SEAT_BACKREST_HEATER_CAN_DLC (8u) +#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_PERIOD_MS (100u) +#define SEAT_BACKREST_HEATER_CAN_ACCEPT_42A_COMMAND (1u) +#define SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR (2u) + +/* Safety watchdog and retained C-SPY session latch, derived from Seat_Fan_demo. */ +#define SEAT_BACKREST_HEATER_WDT_TIMEOUT_TICKS (3200u) +#define SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX (3u) +#define SEAT_BACKREST_HEATER_DEBUG_ACTIVE_MAGIC (0x48444247u) /* "HDBG" */ +#define SEAT_BACKREST_HEATER_DEBUG_EXIT_MAGIC (0x484F4646u) /* "HOFF" */ + +/* + * Schematic-confirmed diagnostic current-sense path: + * KBJRD-AD -> P12.2 / ADC[1]_6. The BTS7010 IS pin uses a 1.2 kOhm + * sense resistor and the schematic annotates K=5000. The calculated current + * is for bench visibility only; production thresholds require calibration. + */ +#define SEAT_BACKREST_HEATER_SENSE_ENABLE (1u) +#define SEAT_BACKREST_HEATER_SENSE_PORT GPIO_PRT12 +#define SEAT_BACKREST_HEATER_SENSE_PIN (2u) +#define SEAT_BACKREST_HEATER_SENSE_GPIO_HSIOM P12_2_GPIO +#define SEAT_BACKREST_HEATER_SENSE_RESISTOR_OHM (1200u) +#define SEAT_BACKREST_HEATER_SENSE_KILIS (5000u) + +/* BTS7010 control facts confirmed on the power-driver schematic page. */ +#define SEAT_BACKREST_HEATER_BTS7010_IN_ACTIVE_HIGH (1u) +#define SEAT_BACKREST_HEATER_BTS7010_DEN_MCU_CONTROLLED (0u) + +#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d) +#error "Seat_Controller_demo requires CYT4BFBCJE rev_d headers." +#endif + +#if ((SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ % \ + SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ) != 0u) +#error "The heater counter clock must divide evenly by the PWM frequency." +#endif + +#if ((SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS == 0u) || \ + (SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS > 65535u)) +#error "The heater PWM period does not fit TCPWM1 group 0 counter 65." +#endif + +#if ((SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT == 0u) || \ + (SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT >= \ + SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT) || \ + (SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT >= \ + SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT) || \ + (SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT > 100u)) +#error "Heater duties must be ordered LOW < MEDIUM < HIGH <= 100." +#endif + +#if (SEAT_BACKREST_HEATER_DEFAULT_LEVEL > 3u) +#error "Default heater level must be 0 (off) through 3 (high)." +#endif + +#if ((SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID > 0x7FFu) || \ + (SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID > 0x7FFu) || \ + (SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID > 0x7FFu) || \ + (SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID > 0x7FFu) || \ + (SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID > 0x7FFu)) +#error "Seat heating CAN IDs must be 11-bit standard identifiers." +#endif + +#if (SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH < 5u) +#error "The heater CAN receive queue must hold at least five frames." +#endif + +#if ((SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR == 0u) || \ + (SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR > 3u)) +#error "Default heater CAN gear must be in the range 1 through 3." +#endif + +#if ((SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS == 0u) || \ + (SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS == 0u) || \ + (SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS == 0u)) +#error "Heater sample period and maximum active time must be non-zero." +#endif + +#if (SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX >= SRSS_BACKUP_NUM_BREG) +#error "The selected heater backup register is not present on this device." +#endif + +#endif /* SEAT_BACKREST_HEATER_CONFIG_H */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_current_adc_config.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_current_adc_config.h new file mode 100644 index 0000000..1862e6c --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_current_adc_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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_fan_config.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_fan_config.h new file mode 100644 index 0000000..ba591bf --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_fan_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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_move_config.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_move_config.h new file mode 100644 index 0000000..540fdab --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Config/seat_move_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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/BUILD_INFO_2026-09-16.md b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/BUILD_INFO_2026-09-16.md new file mode 100644 index 0000000..8cf0b17 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/BUILD_INFO_2026-09-16.md @@ -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. diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/README_Passenger_Seat_Timed_Controller.md b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/README_Passenger_Seat_Timed_Controller.md new file mode 100644 index 0000000..ddfcdd9 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Docs/README_Passenger_Seat_Timed_Controller.md @@ -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,以及位置/动态/热控模块未被链接。 diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.c b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.c new file mode 100644 index 0000000..6bcb378 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.c @@ -0,0 +1,4340 @@ +/***************************************************************************//** +* \file motor_control.c +* +* CYT4BFBCJE CM0+ low-level four-channel DRV8718S-Q1 motor control. +*******************************************************************************/ + +#include "cy_project.h" +#include "cy_device_headers.h" +#include "bsp_seat_current_adc.h" +#include "motor_control.h" +#include "seat_move_config.h" + +#define MOTOR_DIAGNOSTIC_LOG_ENABLE (0u) + +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) +#include +#define MOTOR_LOG(...) printf(__VA_ARGS__) +#else +#define MOTOR_LOG(...) ((void)0) +#endif + +#define DRV8718_REG_IC_STAT1 (0x00u) +#define DRV8718_REG_IC_CTRL1 (0x07u) +#define DRV8718_REG_IC_CTRL2 (0x08u) +#define DRV8718_REG_BRG_CTRL1 (0x09u) +#define DRV8718_REG_BRG_CTRL2 (0x0Au) +#define DRV8718_REG_PWM_CTRL1 (0x0Bu) +#define DRV8718_REG_PWM_CTRL2 (0x0Cu) +#define DRV8718_REG_PWM_CTRL3 (0x0Du) +#define DRV8718_REG_PWM_CTRL4 (0x0Eu) + +#define DRV8718_WRITE_FRAME(addr, data) \ + ((uint16_t)((((uint16_t)(addr) & 0x3Fu) << 8u) | ((uint16_t)(data) & 0xFFu))) +#define DRV8718_READ_FRAME(addr) \ + ((uint16_t)(0x4000u | ((((uint16_t)(addr)) & 0x3Fu) << 8u))) + +/* BRG_CTRL value encoding per half bridge: 0=Hi-Z, 1=Low, 2=High, 3=PWM. */ +#define HB_CTRL_HIZ (0u) +#define HB_CTRL_LOW (1u) +#define HB_CTRL_HIGH (2u) +#define HB_CTRL_PWM (3u) + +#define PWM_SRC_IN1 (0u) +#define PWM_SRC_IN2 (1u) +#define PWM_SRC_IN3 (2u) +#define PWM_SRC_IN4 (3u) + +#define MOTOR_COAST_BRG_CTRL1 (0x00u) +#define MOTOR_COAST_BRG_CTRL2 (0x00u) + +#define MOTOR_PROFILE_COUNT ((uint32_t)MOTOR_MODE_COUNT) +#define MOTOR_PWM_DUTY_PERCENT (50ul) +#define HALL_SAMPLE_INTERVAL_US (2000ul) +#define MOTOR_SAFETY_CHECK_INTERVAL_MS (50ul) +#define MOTOR_SAFETY_WINDOW_MS (500ul) +#define MOTOR_MODE0_STARTUP_GRACE_MS (1000ul) +#define MOTOR_MODE1_STARTUP_GRACE_MS (1000ul) +#define MOTOR_MODE2_STARTUP_GRACE_MS (1000ul) +#define MOTOR_MODE3_STARTUP_GRACE_MS (2000ul) +#define MOTOR_SAFETY_WINDOW_SAMPLES \ + (MOTOR_SAFETY_WINDOW_MS / MOTOR_SAFETY_CHECK_INTERVAL_MS) +#define MOTOR_ENDPOINT_EDGE_THRESHOLD (0ul) +#define MOTOR_STALL_EDGE_THRESHOLD (2ul) +#define MOTOR_OPERATION_TIMEOUT_MS (70000ul) +#define MOTOR_REVERSAL_COAST_MS (100ul) +#define MOTOR_COAST_HALL_QUIET_MS (100ul) +#define MOTOR_COAST_SETTLE_TIMEOUT_MS (5000ul) +#define MOTOR_MAX_AUTOMATIC_REVERSALS (1u) +#define MOTOR_CALIBRATION_RANGE_MIN_PERCENT (70ul) +#define MOTOR_CALIBRATION_RANGE_MAX_PERCENT (130ul) +#define MOTOR_HOME_PROBE_VERIFY_EDGES (4ul) +#define MOTOR_HOME_RETURN_EDGE_TOLERANCE (1ul) +#define MOTOR_MAX_CALIBRATED_SPEED_MULTIPLIER (2ul) +#define MOTOR_SPEED_CONTROL_START_DELAY_MS (500ul) +#define MOTOR_SPEED_CONTROL_CHECK_MS (100ul) +#define MOTOR_SPEED_CONTROL_WINDOW_MS (500ul) +#define MOTOR_SPEED_PROGRESS_CONTROL_START_MS (100ul) +#define MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT (5u) +#define MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT (0u) +#define MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES (1ul) +#define MOTOR_SPEED_CONTROL_HISTORY_SAMPLES \ + (MOTOR_SPEED_CONTROL_WINDOW_MS / MOTOR_SPEED_CONTROL_CHECK_MS) +#define MOTOR_SPEED_TOLERANCE_PERCENT (10ul) +#define MOTOR_MODE0_SPEED_STEP_PPS_X100 (500ul) +#define MOTOR_MODE1_SPEED_STEP_PPS_X100 (350ul) +#define MOTOR_MODE2_SPEED_STEP_PPS_X100 (500ul) +#define MOTOR_MODE3_SPEED_STEP_PPS_X100 (500ul) +#define DRV8718_SPI_TIMEOUT_LOOPS (100000ul) +#define DRV8718_STATUS_POLL_INTERVAL_MS (50ul) +#define DRV8718_SDO_HEADER_MASK (0xC000u) +#define DRV8718_SDO_HEADER_VALUE (0xC000u) +#define DRV8718_SDO_ALERT_MASK (0x3F00u) +#define DRV8718_IC_STAT1_SPI_OK (0x80u) +#define DRV8718_IC_STAT1_ALERT_MASK (0x3Fu) + +/* One fixed PWM mapping assigns one independent DRV input to each H-bridge. */ +#define DRV8718_PWM_CTRL1_ALL_MOTORS \ + ((uint8_t)((PWM_SRC_IN1 << 6u) | (PWM_SRC_IN1 << 4u) | \ + (PWM_SRC_IN4 << 2u) | PWM_SRC_IN4)) +#define DRV8718_PWM_CTRL2_ALL_MOTORS \ + ((uint8_t)((PWM_SRC_IN3 << 6u) | (PWM_SRC_IN3 << 4u) | \ + (PWM_SRC_IN2 << 2u) | PWM_SRC_IN2)) +#define MOTOR_PWM_CTRL3_HIGHSIDE (0x00u) +#define MOTOR_PWM_CTRL4_ACTIVE_HI (0x00u) +#define DRV8718_IC_CTRL1_ENABLE_UNLOCKED (0x86u) +#define DRV8718_IC_CTRL1_CLEAR_FAULTS (0x87u) +#define DRV8718_IC_CTRL2_DRVOFF_MODE (0x02u) + +#define SPI_BAUDRATE (1000000ul) +#define SPI_OVERSAMPLING (16ul) +#define SPI_INPUT_CLOCK_HZ (SPI_BAUDRATE * SPI_OVERSAMPLING) +#define SOURCE_CLOCK_HZ (80000000ul) +#define SCB4_DIVIDER_NO (1u) + +#define MOTOR_TIMER_INPUT_CLOCK_HZ (2000000ul) +#define MOTOR_TIMER_DIVIDER_NO (2u) +#define MOTOR_PWM_FREQUENCY_HZ (20000ul) +#define MOTOR_PWM_PERIOD_COUNTS (MOTOR_TIMER_INPUT_CLOCK_HZ / MOTOR_PWM_FREQUENCY_HZ) +#define MOTOR_PWM_COMPARE_COUNTS ((MOTOR_PWM_PERIOD_COUNTS * MOTOR_PWM_DUTY_PERCENT) / 100ul) + +#define HALL_TIMEBASE_TCPWM TCPWM1_GRP0_CNT20 +#define HALL_TIMEBASE_CLOCK PCLK_TCPWM1_CLOCKS20 +#define HALL_TIMEBASE_IRQ tcpwm_1_interrupts_20_IRQn +#define HALL_TIMEBASE_PERIOD_COUNTS \ + ((MOTOR_TIMER_INPUT_CLOCK_HZ / 1000000ul) * HALL_SAMPLE_INTERVAL_US) +#define MOTOR_MS_TO_TICKS(ms) \ + ((uint32_t)(((((uint64_t)(ms)) * 1000ull) + \ + HALL_SAMPLE_INTERVAL_US - 1ull) / HALL_SAMPLE_INTERVAL_US)) +#define MOTOR_TICKS_TO_MS(ticks) \ + ((uint32_t)((((uint64_t)(ticks)) * HALL_SAMPLE_INTERVAL_US) / 1000ull)) +#define MOTOR_SAFETY_CHECK_TICKS \ + MOTOR_MS_TO_TICKS(MOTOR_SAFETY_CHECK_INTERVAL_MS) + +#define DRV_DVDD_EN_PORT P3_1_GPIO_STD_PORT +#define DRV_DVDD_EN_PIN P3_1_GPIO_STD_PIN +#define DRV_NCS_PORT P31_2_GPIO_STD_PORT +#define DRV_NCS_PIN P31_2_GPIO_STD_PIN +#define DRV_NSLEEP_PORT P9_1_GPIO_STD_PORT +#define DRV_NSLEEP_PIN P9_1_GPIO_STD_PIN +#define DRV_DRVOFF_PORT P8_1_GPIO_STD_PORT +#define DRV_DRVOFF_PIN P8_1_GPIO_STD_PIN +#define DRV_IN1_PORT P1_0_GPIO_STD_PORT +#define DRV_IN1_PIN P1_0_GPIO_STD_PIN +#define DRV_IN1_PWM_MUX P1_0_TCPWM1_LINE12 +#define DRV_IN1_PWM_TCPWM TCPWM1_GRP0_CNT12 +#define DRV_IN1_PWM_CLOCK PCLK_TCPWM1_CLOCKS12 +#define DRV_IN2_PORT P5_4_GPIO_STD_PORT +#define DRV_IN2_PIN P5_4_GPIO_STD_PIN +#define DRV_IN2_PWM_MUX P5_4_TCPWM1_LINE13 +#define DRV_IN2_PWM_TCPWM TCPWM1_GRP0_CNT13 +#define DRV_IN2_PWM_CLOCK PCLK_TCPWM1_CLOCKS13 +#define DRV_IN3_PORT P5_5_GPIO_STD_PORT +#define DRV_IN3_PIN P5_5_GPIO_STD_PIN +#define DRV_IN3_PWM_MUX P5_5_TCPWM1_LINE14 +#define DRV_IN3_PWM_TCPWM TCPWM1_GRP0_CNT14 +#define DRV_IN3_PWM_CLOCK PCLK_TCPWM1_CLOCKS14 +#define DRV_IN4_PORT P8_0_GPIO_STD_PORT +#define DRV_IN4_PIN P8_0_GPIO_STD_PIN +#define DRV_IN4_PWM_MUX P8_0_TCPWM1_LINE19 +#define DRV_IN4_PWM_TCPWM TCPWM1_GRP0_CNT19 +#define DRV_IN4_PWM_CLOCK PCLK_TCPWM1_CLOCKS19 + +#define DRV_SPI_MISO_PORT P6_0_GPIO_STD_PORT +#define DRV_SPI_MISO_PIN P6_0_GPIO_STD_PIN +#define DRV_SPI_MISO_MUX P6_0_SCB4_SPI_MISO +#define DRV_SPI_MOSI_PORT P6_1_GPIO_STD_PORT +#define DRV_SPI_MOSI_PIN P6_1_GPIO_STD_PIN +#define DRV_SPI_MOSI_MUX P6_1_SCB4_SPI_MOSI +#define DRV_SPI_CLK_PORT P6_2_GPIO_STD_PORT +#define DRV_SPI_CLK_PIN P6_2_GPIO_STD_PIN +#define DRV_SPI_CLK_MUX P6_2_SCB4_SPI_CLK + +#define HALL_BACK_PORT P14_0_GPIO_STD_PORT +#define HALL_BACK_PIN P14_0_GPIO_STD_PIN +#define HALL_FOOT_2030_PORT P10_1_GPIO_STD_PORT +#define HALL_FOOT_2030_PIN P10_1_GPIO_STD_PIN +#define HALL_LEG_PORT P18_2_GPIO_STD_PORT +#define HALL_LEG_PIN P18_2_GPIO_STD_PIN +#define HALL_HEIGHT_PORT P12_5_GPIO_STD_PORT +#define HALL_HEIGHT_PIN P12_5_GPIO_STD_PIN + +#if ((SOURCE_CLOCK_HZ % MOTOR_TIMER_INPUT_CLOCK_HZ) != 0ul) +#error "MOTOR_TIMER_INPUT_CLOCK_HZ must divide SOURCE_CLOCK_HZ exactly." +#endif + +#if ((MOTOR_TIMER_INPUT_CLOCK_HZ % MOTOR_PWM_FREQUENCY_HZ) != 0ul) +#error "MOTOR_PWM_FREQUENCY_HZ must divide MOTOR_TIMER_INPUT_CLOCK_HZ exactly." +#endif + +#if ((HALL_SAMPLE_INTERVAL_US % 1000ul) != 0ul) +#error "HALL_SAMPLE_INTERVAL_US must be an integer number of milliseconds." +#endif + +#if ((MOTOR_SAFETY_WINDOW_MS % MOTOR_SAFETY_CHECK_INTERVAL_MS) != 0ul) +#error "MOTOR_SAFETY_WINDOW_MS must be a multiple of the safety check period." +#endif + +#if (((MOTOR_MODE0_STARTUP_GRACE_MS % \ + MOTOR_SAFETY_CHECK_INTERVAL_MS) != 0ul) || \ + ((MOTOR_MODE1_STARTUP_GRACE_MS % \ + MOTOR_SAFETY_CHECK_INTERVAL_MS) != 0ul) || \ + ((MOTOR_MODE2_STARTUP_GRACE_MS % \ + MOTOR_SAFETY_CHECK_INTERVAL_MS) != 0ul) || \ + ((MOTOR_MODE3_STARTUP_GRACE_MS % \ + MOTOR_SAFETY_CHECK_INTERVAL_MS) != 0ul)) +#error "Every axis startup grace must align with the safety check period." +#endif + +#if ((MOTOR_SAFETY_CHECK_INTERVAL_MS * 1000ul) < HALL_SAMPLE_INTERVAL_US) +#error "The safety task cannot run faster than the Hall sampling timebase." +#endif + +#if ((MOTOR_SPEED_CONTROL_WINDOW_MS % MOTOR_SPEED_CONTROL_CHECK_MS) != 0ul) +#error "MOTOR_SPEED_CONTROL_WINDOW_MS must be a multiple of the speed check period." +#endif + +#if ((MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT == 0u) || \ + (MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT >= MOTOR_SPEED_MIN_DUTY_PERCENT)) +#error "Adaptive speed duty must be below the fixed-duty command minimum." +#endif + +#if ((MOTOR_PWM_DUTY_PERCENT < MOTOR_SPEED_MIN_DUTY_PERCENT) || \ + (MOTOR_PWM_DUTY_PERCENT > MOTOR_SPEED_MAX_DUTY_PERCENT)) +#error "The default motor duty must stay inside the speed-control duty limits." +#endif + +typedef struct +{ + const char *name; + const char *hallName; + volatile stc_GPIO_PRT_t *pwmPort; + uint32_t pwmPin; + en_hsiom_sel_t pwmHsiom; + volatile stc_TCPWM_GRP_CNT_t *pwmTcpwm; + en_clk_dst_t pwmClock; + volatile stc_GPIO_PRT_t *hallPort; + uint32_t hallPin; + uint8_t bridgeRegister; + uint8_t bridgeMask; + uint8_t towardFarBridgeValue; + uint8_t towardNearBridgeValue; + uint32_t defaultMaxHallCount; + uint32_t maxPhysicalValueX100; + uint32_t maxSpeedPpsX100; + uint32_t speedStepPpsX100; + uint32_t startupGraceMs; + uint16_t stallCurrentDeltaRaw; +} motor_profile_t; + +typedef struct +{ + uint32_t rawRisingEdges; + uint32_t rawTotalEdges; + uint32_t positionHallCount; + uint32_t maxHallCount; + uint32_t targetHallCount; + uint32_t motionStartTicks; + uint32_t calibrationTimeoutTicks; + uint32_t scheduledStartTicks; + uint32_t coastStartTicks; + uint32_t activityStartTicks; + uint32_t activityRisingBaseline; + uint32_t activityTotalBaseline; + uint32_t activityMonitorStartTicks; + uint32_t activityWindowBaseline; + uint32_t currentSenseLastSampleTicks; + uint32_t lowHallStartTicks; + uint32_t lowHallEdgeBaseline; + uint32_t stallCurrentStartTicks; + uint32_t activityHistoryEdges[MOTOR_SAFETY_WINDOW_SAMPLES]; + uint32_t recentWindowEdges; + uint16_t currentSenseRaw; + uint16_t currentSenseBaselineRaw; + uint16_t currentSenseDeltaRaw; + uint16_t activityHistoryIndex; + uint16_t activityValidSamples; + motor_direction_t direction; + motor_direction_t eventDirection; + motor_direction_t scheduledDirection; + motor_direction_t coastDirection; + motor_event_t pendingEvent; + motor_event_t lastEvent; + motor_event_t moveResultEvent; + uint8_t lastLevel; + uint8_t automaticReverseCount; + uint8_t currentSenseErrorCount; + bool hallInitialized; + bool positionValid; + bool targetActive; + bool calibrating; + bool activityWindowReady; + bool activityMonitorStarted; + bool currentSenseValid; + bool currentSenseBaselineValid; + bool lowHallTracking; + bool stallCurrentTracking; + bool reverseAtEndpoint; + bool reverseAtStall; + bool startPending; + bool coastTracking; + bool operationActive; + bool eventWasTargetMove; + bool moveResultReady; + uint32_t moveCommandSequence; + uint32_t moveResultSequence; + uint32_t speedDurationMs; + uint32_t speedTravelValueX100; + uint32_t speedPlannedHallPulses; + uint32_t speedTargetPpsX100; + uint32_t speedActualPpsX100; + uint32_t maxSpeedPpsX100; + uint32_t speedControlStartTicks; + uint32_t speedLastSampleTicks; + uint32_t speedBaselineRawEdges; + uint32_t speedHistoryRawEdges[MOTOR_SPEED_CONTROL_HISTORY_SAMPLES]; + uint32_t speedHistoryTicks[MOTOR_SPEED_CONTROL_HISTORY_SAMPLES]; + uint8_t speedHistoryIndex; + uint8_t speedHistoryValidSamples; + uint8_t speedInitialDutyPercent; + uint8_t speedCurrentDutyPercent; + uint8_t fixedDutyPercent; + uint16_t latestAbortSequence; + uint16_t acceptedCommandSequence; + uint32_t cancellationGeneration; + uint32_t acceptedCancellationGeneration; + bool abortSequenceValid; + bool acceptedCommandValid; + bool speedControlEnabled; +} motor_runtime_t; + +static const motor_profile_t motorProfiles[] = +{ + { + "MOTOR_IN2_HB7_HB8_HORIZONTAL", + "HORIZONTAL_HALL", + DRV_IN2_PORT, + DRV_IN2_PIN, + DRV_IN2_PWM_MUX, + DRV_IN2_PWM_TCPWM, + DRV_IN2_PWM_CLOCK, + HALL_LEG_PORT, + HALL_LEG_PIN, + DRV8718_REG_BRG_CTRL2, + 0x0Fu, + (uint8_t)((HB_CTRL_PWM << 2u) | HB_CTRL_LOW), + (uint8_t)((HB_CTRL_LOW << 2u) | HB_CTRL_PWM), + MOTOR_MODE0_DEFAULT_MAX_HALL_COUNT, + MOTOR_MODE0_MAX_POSITION_MM_X100, + MOTOR_MODE0_MAX_SPEED_PPS_X100, + MOTOR_MODE0_SPEED_STEP_PPS_X100, + MOTOR_MODE0_STARTUP_GRACE_MS, + SEAT_MOVE_M0_STALL_CURRENT_DELTA_RAW, + }, + { + "MOTOR_IN3_HB5_HB6_SEAT_PAN", + "SEAT_PAN_HALL", + DRV_IN3_PORT, + DRV_IN3_PIN, + DRV_IN3_PWM_MUX, + DRV_IN3_PWM_TCPWM, + DRV_IN3_PWM_CLOCK, + HALL_FOOT_2030_PORT, + HALL_FOOT_2030_PIN, + DRV8718_REG_BRG_CTRL2, + 0xF0u, + (uint8_t)((HB_CTRL_PWM << 6u) | (HB_CTRL_LOW << 4u)), + (uint8_t)((HB_CTRL_LOW << 6u) | (HB_CTRL_PWM << 4u)), + MOTOR_MODE1_DEFAULT_MAX_HALL_COUNT, + MOTOR_MODE1_MAX_VERTICAL_MM_X100, + MOTOR_MODE1_MAX_SPEED_PPS_X100, + MOTOR_MODE1_SPEED_STEP_PPS_X100, + MOTOR_MODE1_STARTUP_GRACE_MS, + SEAT_MOVE_M1_STALL_CURRENT_DELTA_RAW, + }, + { + "MOTOR_IN4_HB3_HB4_BACKREST", + "BACKREST_HALL", + DRV_IN4_PORT, + DRV_IN4_PIN, + DRV_IN4_PWM_MUX, + DRV_IN4_PWM_TCPWM, + DRV_IN4_PWM_CLOCK, + HALL_BACK_PORT, + HALL_BACK_PIN, + DRV8718_REG_BRG_CTRL1, + 0x0Fu, + (uint8_t)((HB_CTRL_PWM << 2u) | HB_CTRL_LOW), + (uint8_t)((HB_CTRL_LOW << 2u) | HB_CTRL_PWM), + MOTOR_MODE2_DEFAULT_MAX_HALL_COUNT, + MOTOR_MODE2_MAX_ANGLE_DEG_X100, + MOTOR_MODE2_MAX_SPEED_PPS_X100, + MOTOR_MODE2_SPEED_STEP_PPS_X100, + MOTOR_MODE2_STARTUP_GRACE_MS, + SEAT_MOVE_M2_STALL_CURRENT_DELTA_RAW, + }, + { + "MOTOR_IN1_HB1_HB2_LEGREST", + "LEGREST_HALL", + DRV_IN1_PORT, + DRV_IN1_PIN, + DRV_IN1_PWM_MUX, + DRV_IN1_PWM_TCPWM, + DRV_IN1_PWM_CLOCK, + HALL_HEIGHT_PORT, + HALL_HEIGHT_PIN, + DRV8718_REG_BRG_CTRL1, + 0xF0u, + /* Keep direction aligned with the range-calibration firmware: + * near is legrest down/vertical and far is legrest up. */ + (uint8_t)((HB_CTRL_PWM << 6u) | (HB_CTRL_LOW << 4u)), + (uint8_t)((HB_CTRL_LOW << 6u) | (HB_CTRL_PWM << 4u)), + MOTOR_MODE3_DEFAULT_MAX_HALL_COUNT, + MOTOR_MODE3_MAX_ANGLE_DEG_X100, + MOTOR_MODE3_MAX_SPEED_PPS_X100, + MOTOR_MODE3_SPEED_STEP_PPS_X100, + MOTOR_MODE3_STARTUP_GRACE_MS, + SEAT_MOVE_M3_STALL_CURRENT_DELTA_RAW, + }, +}; + +/* Keep the public mode enumeration and the hardware profile table locked + * together. A missing profile must fail the build instead of indexing past + * the table from the 2 ms Hall ISR. */ +typedef char motor_profile_count_must_match_mode_count[ + ((sizeof(motorProfiles) / sizeof(motorProfiles[0])) == + (uint32_t)MOTOR_MODE_COUNT) ? 1 : -1]; + +static cy_stc_gpio_pin_config_t gpioOutCfg = +{ + .outVal = 0ul, + .driveMode = CY_GPIO_DM_STRONG_IN_OFF, + .hsiom = HSIOM_SEL_GPIO, + .intEdge = 0ul, + .intMask = 0ul, + .vtrip = 0ul, + .slewRate = 0ul, + .driveSel = 0ul, +}; + +static cy_stc_gpio_pin_config_t spiPinCfg = +{ + .outVal = 0ul, + .driveMode = CY_GPIO_DM_STRONG_IN_OFF, + .hsiom = HSIOM_SEL_GPIO, + .intEdge = 0ul, + .intMask = 0ul, + .vtrip = 0ul, + .slewRate = 0ul, + .driveSel = 0ul, +}; + +static cy_stc_gpio_pin_config_t gpioInCfg = +{ + .outVal = 0ul, + .driveMode = CY_GPIO_DM_HIGHZ, + .hsiom = HSIOM_SEL_GPIO, + .intEdge = 0ul, + .intMask = 0ul, + .vtrip = 0ul, + .slewRate = 0ul, + .driveSel = 0ul, +}; + +static cy_stc_gpio_pin_config_t pwmPinCfg = +{ + .outVal = 0ul, + .driveMode = CY_GPIO_DM_STRONG_IN_OFF, + .hsiom = HSIOM_SEL_GPIO, + .intEdge = 0ul, + .intMask = 0ul, + .vtrip = 0ul, + .slewRate = 0ul, + .driveSel = 0ul, +}; + +static volatile motor_runtime_t motorRuntime[MOTOR_MODE_COUNT]; +static volatile uint32_t hallTimebaseTicks = 0ul; +static volatile uint32_t safetyTickDivider = 0ul; +static uint8_t bridgeCtrl1Shadow = MOTOR_COAST_BRG_CTRL1; +static uint8_t bridgeCtrl2Shadow = MOTOR_COAST_BRG_CTRL2; +static bool motorControlReady = false; +static bool pwmTimersInitialized = false; +static volatile bool driverFaultLatched = false; +static volatile bool safetyInhibitLatched = false; +static volatile bool motorServiceActive = false; +static volatile bool driverSpiActive = false; +static volatile uint8_t abortRequestedMask = 0u; +static bool watchdogServiceEnabled = false; +static bool driverStatusMonitoringEnabled = false; +static uint32_t driverStatusLastPollTicks = 0ul; +static uint32_t currentSenseLastServiceTicks = 0ul; + +static bool Motor_CalibrationRangeIsPlausible(uint32_t motorIndex, + uint32_t measuredPulses); +static bool Motor_WaitForCoastToSettle(motor_mode_t mode); + +CY_WEAK void Motor_CalibrationServiceHook(void) +{ + /* Optional application service point. */ +} + +static const cy_stc_tcpwm_pwm_config_t motorPwmCfg = +{ + .pwmMode = CY_TCPWM_PWM_MODE_PWM, + .clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1, + .debug_pause = false, + .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, + .deadTime = 0ul, + .deadTimeComp = 0ul, + .runMode = CY_TCPWM_PWM_CONTINUOUS, + .period = MOTOR_PWM_PERIOD_COUNTS - 1ul, + .period_buff = 0ul, + .enablePeriodSwap = false, + .compare0 = MOTOR_PWM_COMPARE_COUNTS, + .compare1 = 0ul, + .enableCompare0Swap = false, + .enableCompare1Swap = false, + .interruptSources = CY_TCPWM_INT_NONE, + .invertPWMOut = 0ul, + .invertPWMOutN = 0ul, + .killMode = CY_TCPWM_PWM_STOP_ON_KILL, + .switchInputMode = CY_TCPWM_INPUT_LEVEL, + .switchInput = 0ul, + .reloadInputMode = CY_TCPWM_INPUT_LEVEL, + .reloadInput = 0ul, + .startInputMode = CY_TCPWM_INPUT_LEVEL, + .startInput = 0ul, + .kill0InputMode = CY_TCPWM_INPUT_LEVEL, + .kill0Input = 0ul, + .kill1InputMode = CY_TCPWM_INPUT_LEVEL, + .kill1Input = 0ul, + .countInputMode = CY_TCPWM_INPUT_LEVEL, + .countInput = 1ul, + .immediateKill = true, + .pwmOnDisable = CY_TCPWM_PWM_OUT_MODE_LOW, +}; + +static const cy_stc_tcpwm_counter_config_t hallTimebaseCfg = +{ + .period = HALL_TIMEBASE_PERIOD_COUNTS - 1ul, + .clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1, + .runMode = CY_TCPWM_COUNTER_CONTINUOUS, + .countDirection = CY_TCPWM_COUNTER_COUNT_UP, + .debug_pause = false, + .compareOrCapture = CY_TCPWM_COUNTER_MODE_COMPARE, + .compare0 = 0ul, + .compare0_buff = 0ul, + .compare1 = 0ul, + .compare1_buff = 0ul, + .enableCompare0Swap = false, + .enableCompare1Swap = false, + .interruptSources = CY_TCPWM_INT_NONE, + .capture0InputMode = CY_TCPWM_INPUT_LEVEL, + .capture0Input = 0ul, + .reloadInputMode = CY_TCPWM_INPUT_LEVEL, + .reloadInput = 0ul, + .startInputMode = CY_TCPWM_INPUT_LEVEL, + .startInput = 0ul, + .stopInputMode = CY_TCPWM_INPUT_LEVEL, + .stopInput = 0ul, + .capture1InputMode = CY_TCPWM_INPUT_LEVEL, + .capture1Input = 0ul, + .countInputMode = CY_TCPWM_INPUT_LEVEL, + .countInput = 1ul, + .trigger0EventCfg = CY_TCPWM_COUNTER_OVERFLOW, + .trigger1EventCfg = CY_TCPWM_COUNTER_OVERFLOW, +}; + +static const cy_stc_sysint_irq_t hallTimebaseIrqCfg = +{ + .sysIntSrc = HALL_TIMEBASE_IRQ, + .intIdx = CPUIntIdx3_IRQn, + .isEnabled = true, +}; + +static const cy_stc_scb_spi_config_t spiCfg = +{ + .spiMode = CY_SCB_SPI_MASTER, + .subMode = CY_SCB_SPI_MOTOROLA, + .sclkMode = CY_SCB_SPI_CPHA0_CPOL0, + .oversample = SPI_OVERSAMPLING, + .rxDataWidth = 16ul, + .txDataWidth = 16ul, + .enableMsbFirst = true, + .enableFreeRunSclk = false, + .enableInputFilter = false, + .enableMisoLateSample = true, + .enableTransferSeperation = false, + .ssPolarity0 = false, + .ssPolarity1 = false, + .ssPolarity2 = false, + .ssPolarity3 = false, + .enableWakeFromSleep = false, + .rxFifoTriggerLevel = 0ul, + .rxFifoIntEnableMask = 0ul, + .txFifoTriggerLevel = 0ul, + .txFifoIntEnableMask = 0ul, + .masterSlaveIntEnableMask = 0ul, + .enableSpiDoneInterrupt = false, + .enableSpiBusErrorInterrupt = false, +}; + +static void SetPeripheFracDiv24_5(uint64_t targetFreq, uint64_t sourceFreq, uint8_t divNum) +{ + uint64_t temp = ((uint64_t)sourceFreq << 5ull); + uint32_t divSetting = (uint32_t)(temp / targetFreq); + + Cy_SysClk_PeriphSetFracDivider(Cy_SysClk_GetClockGroup(PCLK_SCB4_CLOCK), + CY_SYSCLK_DIV_24_5_BIT, + divNum, + (((divSetting >> 5ul) & 0x00000FFFul) - 1ul), + (divSetting & 0x0000001Ful)); +} + +static void InitOutputPin(volatile stc_GPIO_PRT_t *port, uint32_t pin, uint32_t initialValue) +{ + gpioOutCfg.outVal = initialValue; + gpioOutCfg.hsiom = HSIOM_SEL_GPIO; + Cy_GPIO_Pin_Init(port, pin, &gpioOutCfg); +} + +static bool Motor_ModeIsValid(motor_mode_t mode) +{ + return ((uint32_t)mode < MOTOR_PROFILE_COUNT); +} + +static bool Motor_SequenceIsNewer(uint16_t candidate, uint16_t reference) +{ + return ((int16_t)((uint16_t)(candidate - reference)) > 0); +} + +static bool Motor_CommandIsCurrentUnsafe( + const volatile motor_runtime_t *state) +{ + if(false != safetyInhibitLatched) + { + return false; + } + if(false != state->abortSequenceValid) + { + return false; + } + if(false == state->acceptedCommandValid) + { + return (0ul == state->cancellationGeneration); + } + return (state->acceptedCancellationGeneration == + state->cancellationGeneration); +} + +static uint32_t Motor_DutyPercentToCompare(uint8_t dutyPercent) +{ + return (MOTOR_PWM_PERIOD_COUNTS * (uint32_t)dutyPercent) / 100ul; +} + +static void Motor_ApplyDutyPercent(uint32_t motorIndex, uint8_t dutyPercent) +{ + Cy_Tcpwm_Pwm_SetCompare0(motorProfiles[motorIndex].pwmTcpwm, + Motor_DutyPercentToCompare(dutyPercent)); +} + +static void Motor_ResetSpeedMeasurementUnsafe(volatile motor_runtime_t *state) +{ + state->speedActualPpsX100 = 0ul; + state->speedControlStartTicks = hallTimebaseTicks; + state->speedLastSampleTicks = hallTimebaseTicks; + state->speedBaselineRawEdges = state->rawRisingEdges; + state->speedHistoryIndex = 0u; + state->speedHistoryValidSamples = 0u; +} + +static bool Motor_CalculateSpeedPlan(uint32_t motorIndex, + uint32_t maxHallCount, + uint32_t durationMs, + uint32_t travelValueX100, + uint32_t *plannedHallPulses, + uint32_t *targetPpsX100, + uint8_t *initialDutyPercent) +{ + const motor_profile_t *profile = &motorProfiles[motorIndex]; + uint64_t pulseNumerator; + uint64_t speedNumerator; + uint64_t speedDenominator; + uint32_t calculatedHallPulses; + uint32_t calculatedTargetPpsX100; + uint32_t initialDuty; + uint32_t dutySteps; + + if((NULL == plannedHallPulses) || (NULL == targetPpsX100) || + (NULL == initialDutyPercent) || (0ul == durationMs) || + (0ul == travelValueX100) || + (travelValueX100 > profile->maxPhysicalValueX100) || + (0ul == maxHallCount) || (0ul == profile->speedStepPpsX100)) + { + return false; + } + + pulseNumerator = ((uint64_t)travelValueX100) * maxHallCount; + calculatedHallPulses = (uint32_t) + ((pulseNumerator + (profile->maxPhysicalValueX100 / 2ul)) / + profile->maxPhysicalValueX100); + + speedNumerator = pulseNumerator * 1000ull * MOTOR_SPEED_VALUE_SCALE; + speedDenominator = ((uint64_t)profile->maxPhysicalValueX100) * + durationMs; + calculatedTargetPpsX100 = (uint32_t) + ((speedNumerator + (speedDenominator / 2ull)) / speedDenominator); + + if((0ul == calculatedHallPulses) || + (0ul == calculatedTargetPpsX100) || + (calculatedTargetPpsX100 > + motorRuntime[motorIndex].maxSpeedPpsX100)) + { + return false; + } + + dutySteps = (calculatedTargetPpsX100 + + profile->speedStepPpsX100 - 1ul) / + profile->speedStepPpsX100; + initialDuty = dutySteps * MOTOR_SPEED_DUTY_STEP_PERCENT; + if(initialDuty < MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT) + { + initialDuty = MOTOR_SPEED_CONTROL_MIN_DUTY_PERCENT; + } + else if(initialDuty > MOTOR_SPEED_MAX_DUTY_PERCENT) + { + initialDuty = MOTOR_SPEED_MAX_DUTY_PERCENT; + } + else + { + /* The estimated duty is already inside the allowed range. */ + } + + *plannedHallPulses = calculatedHallPulses; + *targetPpsX100 = calculatedTargetPpsX100; + *initialDutyPercent = (uint8_t)initialDuty; + return true; +} + +static uint8_t Hall_ReadLevel(uint32_t motorIndex) +{ + const motor_profile_t *motor = &motorProfiles[motorIndex]; + + return (Cy_GPIO_Read(motor->hallPort, motor->hallPin) != 0ul) ? 1u : 0u; +} + +static void Motor_ResetActivityWindowUnsafe(volatile motor_runtime_t *state) +{ + state->activityStartTicks = hallTimebaseTicks; + state->activityRisingBaseline = state->rawRisingEdges; + state->activityTotalBaseline = state->rawTotalEdges; + state->activityMonitorStartTicks = hallTimebaseTicks; + state->activityWindowBaseline = state->rawTotalEdges; + state->recentWindowEdges = 0ul; + state->activityHistoryIndex = 0u; + state->activityValidSamples = 0u; + state->activityWindowReady = false; + state->activityMonitorStarted = false; + state->lowHallStartTicks = hallTimebaseTicks; + state->lowHallEdgeBaseline = state->rawTotalEdges; + state->stallCurrentStartTicks = hallTimebaseTicks; + state->lowHallTracking = false; + state->stallCurrentTracking = false; +} + +static uint16_t Motor_AbsRawDelta(uint16_t sample, uint16_t baseline) +{ + return (sample >= baseline) ? + (uint16_t)(sample - baseline) : + (uint16_t)(baseline - sample); +} + +static bool Motor_CurrentSampleFreshFromIsr(uint32_t motorIndex) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + + return state->currentSenseValid && state->currentSenseBaselineValid && + ((uint32_t)(hallTimebaseTicks - + state->currentSenseLastSampleTicks) <= + MOTOR_MS_TO_TICKS(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS)); +} + +static bool Motor_PrimeCurrentSensing(void) +{ + uint32_t index; + + if(!BspSeatCurrentAdc_Init()) + { + return false; + } + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + uint16_t raw; + + if(!BspSeatCurrentAdc_Sample( + (bsp_seat_current_adc_signal_t) + ((uint32_t)BSP_SEAT_CURRENT_ADC_MOTOR0 + index), + &raw)) + { + return false; + } + motorRuntime[index].currentSenseRaw = raw; + motorRuntime[index].currentSenseBaselineRaw = raw; + motorRuntime[index].currentSenseDeltaRaw = 0u; + motorRuntime[index].currentSenseLastSampleTicks = hallTimebaseTicks; + motorRuntime[index].currentSenseErrorCount = 0u; + motorRuntime[index].currentSenseValid = true; + motorRuntime[index].currentSenseBaselineValid = true; + } + currentSenseLastServiceTicks = hallTimebaseTicks; + return true; +} + +static void Motor_ServiceCurrentSensing(void) +{ + uint32_t index; + + if((uint32_t)(hallTimebaseTicks - currentSenseLastServiceTicks) < + MOTOR_MS_TO_TICKS(SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS)) + { + return; + } + currentSenseLastServiceTicks = hallTimebaseTicks; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + uint16_t raw; + uint32_t savedIntrStatus; + bool stopped; + + if(!BspSeatCurrentAdc_Sample( + (bsp_seat_current_adc_signal_t) + ((uint32_t)BSP_SEAT_CURRENT_ADC_MOTOR0 + index), + &raw)) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(motorRuntime[index].currentSenseErrorCount < UINT8_MAX) + { + motorRuntime[index].currentSenseErrorCount++; + } + if(motorRuntime[index].currentSenseErrorCount >= + SEAT_MOVE_MOTOR_CURRENT_MAX_ERRORS) + { + motorRuntime[index].currentSenseValid = false; + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + continue; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + stopped = (motorRuntime[index].direction == MOTOR_DIRECTION_STOPPED) && + !motorRuntime[index].startPending; + motorRuntime[index].currentSenseRaw = raw; + motorRuntime[index].currentSenseLastSampleTicks = hallTimebaseTicks; + motorRuntime[index].currentSenseErrorCount = 0u; + motorRuntime[index].currentSenseValid = true; + + if(!motorRuntime[index].currentSenseBaselineValid) + { + motorRuntime[index].currentSenseBaselineRaw = raw; + motorRuntime[index].currentSenseBaselineValid = true; + } + else if(stopped) + { + uint32_t filtered = + (((uint32_t)motorRuntime[index].currentSenseBaselineRaw * + ((1ul << SEAT_MOVE_MOTOR_CURRENT_BASELINE_FILTER_SHIFT) - + 1ul)) + + (uint32_t)raw) >> + SEAT_MOVE_MOTOR_CURRENT_BASELINE_FILTER_SHIFT; + motorRuntime[index].currentSenseBaselineRaw = (uint16_t)filtered; + } + motorRuntime[index].currentSenseDeltaRaw = Motor_AbsRawDelta( + raw, motorRuntime[index].currentSenseBaselineRaw); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + } +} + +static void Motor_RequestEventFromIsr(uint32_t motorIndex, motor_event_t event) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + + if((state->pendingEvent == MOTOR_EVENT_NONE) && + ((state->direction != MOTOR_DIRECTION_STOPPED) || + ((event == MOTOR_EVENT_TIMEOUT) && (false != state->startPending)))) + { + if(state->direction != MOTOR_DIRECTION_STOPPED) + { + state->eventDirection = state->direction; + } + else if(false != state->coastTracking) + { + /* A timeout during a reversal pause must keep counting any + * remaining inertia in the old physical direction, not in the + * direction that had only been scheduled. */ + state->eventDirection = state->coastDirection; + } + else + { + state->eventDirection = MOTOR_DIRECTION_STOPPED; + } + state->eventWasTargetMove = state->targetActive; + state->pendingEvent = event; + state->targetActive = false; + state->startPending = false; + + /* Hardware PWM is stopped immediately; SPI bridge cleanup stays in main. */ + if(state->direction != MOTOR_DIRECTION_STOPPED) + { + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[motorIndex].pwmTcpwm); + } + } +} + +static void Motor_CheckOperationTimeoutsFromIsr(void) +{ + uint32_t index; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + uint32_t timeoutTicks = + ((false != motorRuntime[index].calibrating) && + (0ul != motorRuntime[index].calibrationTimeoutTicks)) ? + motorRuntime[index].calibrationTimeoutTicks : + MOTOR_MS_TO_TICKS(MOTOR_OPERATION_TIMEOUT_MS); + if((false != motorRuntime[index].operationActive) && + ((uint32_t)(hallTimebaseTicks - + motorRuntime[index].motionStartTicks) >= + timeoutTicks)) + { + Motor_RequestEventFromIsr(index, MOTOR_EVENT_TIMEOUT); + } + } +} + +static void Hall_AllInit(void) +{ + uint32_t index; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + Cy_GPIO_Pin_Init(motorProfiles[index].hallPort, + motorProfiles[index].hallPin, + &gpioInCfg); + + motorRuntime[index].rawRisingEdges = 0ul; + motorRuntime[index].rawTotalEdges = 0ul; + motorRuntime[index].positionHallCount = 0ul; + motorRuntime[index].maxHallCount = motorProfiles[index].defaultMaxHallCount; + motorRuntime[index].targetHallCount = 0ul; + motorRuntime[index].motionStartTicks = 0ul; + motorRuntime[index].calibrationTimeoutTicks = 0ul; + motorRuntime[index].scheduledStartTicks = 0ul; + motorRuntime[index].coastStartTicks = 0ul; + motorRuntime[index].currentSenseLastSampleTicks = 0ul; + motorRuntime[index].lowHallStartTicks = 0ul; + motorRuntime[index].lowHallEdgeBaseline = 0ul; + motorRuntime[index].stallCurrentStartTicks = 0ul; + motorRuntime[index].currentSenseRaw = 0u; + motorRuntime[index].currentSenseBaselineRaw = 0u; + motorRuntime[index].currentSenseDeltaRaw = 0u; + motorRuntime[index].direction = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].eventDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].scheduledDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].coastDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].pendingEvent = MOTOR_EVENT_NONE; + motorRuntime[index].lastEvent = MOTOR_EVENT_NONE; + motorRuntime[index].moveResultEvent = MOTOR_EVENT_NONE; + motorRuntime[index].lastLevel = Hall_ReadLevel(index); + motorRuntime[index].automaticReverseCount = 0u; + motorRuntime[index].currentSenseErrorCount = 0u; + motorRuntime[index].hallInitialized = true; + motorRuntime[index].positionValid = false; + motorRuntime[index].targetActive = false; + motorRuntime[index].calibrating = false; + motorRuntime[index].currentSenseValid = false; + motorRuntime[index].currentSenseBaselineValid = false; + motorRuntime[index].lowHallTracking = false; + motorRuntime[index].stallCurrentTracking = false; + motorRuntime[index].reverseAtEndpoint = false; + motorRuntime[index].reverseAtStall = false; + motorRuntime[index].startPending = false; + motorRuntime[index].coastTracking = false; + motorRuntime[index].operationActive = false; + motorRuntime[index].eventWasTargetMove = false; + motorRuntime[index].moveResultReady = false; + motorRuntime[index].moveCommandSequence = 0ul; + motorRuntime[index].moveResultSequence = 0ul; + motorRuntime[index].speedDurationMs = 0ul; + motorRuntime[index].speedTravelValueX100 = 0ul; + motorRuntime[index].speedPlannedHallPulses = 0ul; + motorRuntime[index].speedTargetPpsX100 = 0ul; + motorRuntime[index].speedActualPpsX100 = 0ul; + motorRuntime[index].maxSpeedPpsX100 = + motorProfiles[index].maxSpeedPpsX100; + motorRuntime[index].speedInitialDutyPercent = + (uint8_t)MOTOR_PWM_DUTY_PERCENT; + motorRuntime[index].speedCurrentDutyPercent = + (uint8_t)MOTOR_PWM_DUTY_PERCENT; + motorRuntime[index].fixedDutyPercent = + (uint8_t)MOTOR_PWM_DUTY_PERCENT; + motorRuntime[index].latestAbortSequence = 0u; + motorRuntime[index].acceptedCommandSequence = 0u; + motorRuntime[index].cancellationGeneration = 0ul; + motorRuntime[index].acceptedCancellationGeneration = 0ul; + motorRuntime[index].abortSequenceValid = false; + motorRuntime[index].acceptedCommandValid = false; + motorRuntime[index].speedControlEnabled = false; + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + Motor_ResetActivityWindowUnsafe(&motorRuntime[index]); + } +} + +static void Hall_SampleMotorFromIsr(uint32_t motorIndex) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + motor_direction_t countingDirection; + uint8_t level = Hall_ReadLevel(motorIndex); + + if(false == state->hallInitialized) + { + state->lastLevel = level; + state->hallInitialized = true; + return; + } + + if(level != state->lastLevel) + { + state->rawTotalEdges++; + + if((state->direction == MOTOR_DIRECTION_STOPPED) && + (false != state->coastTracking)) + { + /* Any sampled Hall transition proves the mechanism is still + * moving. Extend direction attribution and delayed restart until + * a complete quiet interval has elapsed. */ + state->coastStartTicks = hallTimebaseTicks; + } + + if(level != 0u) + { + state->rawRisingEdges++; + + countingDirection = state->direction; + if((countingDirection == MOTOR_DIRECTION_STOPPED) && + (false != state->coastTracking)) + { + countingDirection = state->coastDirection; + } + + /* Keep the global position aligned with every sampled motor + * pulse, including the short interval after ISR kill and before + * main-loop event service. */ + if(countingDirection == MOTOR_DIRECTION_TOWARD_FAR) + { + if((false == state->positionValid) && + (state->positionHallCount < UINT32_MAX)) + { + /* An uncalibrated/invalid position has no trustworthy + * range ceiling. Keep every inferred far-direction pulse + * so calibration and failure recovery do not lose motion. */ + state->positionHallCount++; + } + else if((false != state->positionValid) && + (state->positionHallCount < state->maxHallCount)) + { + state->positionHallCount++; + } + } + else if(countingDirection == MOTOR_DIRECTION_TOWARD_NEAR) + { + if(state->positionHallCount > 0ul) + { + state->positionHallCount--; + } + } + else + { + /* Position is not changed while the motor is stopped. */ + } + + if((state->pendingEvent == MOTOR_EVENT_NONE) && + (false != state->targetActive) && + (state->direction != MOTOR_DIRECTION_STOPPED)) + { + if(((state->direction == MOTOR_DIRECTION_TOWARD_FAR) && + (state->positionHallCount >= state->targetHallCount)) || + ((state->direction == MOTOR_DIRECTION_TOWARD_NEAR) && + (state->positionHallCount <= state->targetHallCount))) + { + Motor_RequestEventFromIsr(motorIndex, + MOTOR_EVENT_TARGET_REACHED); + } + } + } + + state->lastLevel = level; + } + + /* A fixed delay alone can drop late inertia pulses. Close the inferred + * old-direction window only after no Hall edge has been sampled for the + * full quiet interval; an edge on the boundary resets this timer. */ + if((false != state->coastTracking) && + ((uint32_t)(hallTimebaseTicks - state->coastStartTicks) >= + MOTOR_MS_TO_TICKS(MOTOR_COAST_HALL_QUIET_MS))) + { + state->coastTracking = false; + state->coastDirection = MOTOR_DIRECTION_STOPPED; + } +} + +static void Motor_UpdateActivityWindowFromIsr(uint32_t motorIndex) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + uint32_t oldestCount; + uint16_t nextIndex; + + if(state->direction == MOTOR_DIRECTION_STOPPED) + { + return; + } + + /* Keep sampling Hall position every 2 ms from the physical PWM start, but + * do not use startup transients in the stall/endpoint window. Each axis + * has its own mechanical startup grace; after that grace establish a fresh + * raw-edge baseline and require one complete activity window. Calibration + * still maps missing Hall to the requested endpoint, while normal moves + * map it to stall. */ + if(false == state->activityMonitorStarted) + { + if((uint32_t)(hallTimebaseTicks - state->activityStartTicks) < + MOTOR_MS_TO_TICKS(motorProfiles[motorIndex].startupGraceMs)) + { + return; + } + + state->activityMonitorStarted = true; + state->activityMonitorStartTicks = hallTimebaseTicks; + state->activityWindowBaseline = state->rawTotalEdges; + state->recentWindowEdges = 0ul; + state->activityHistoryIndex = 0u; + state->activityValidSamples = 0u; + state->activityWindowReady = false; + return; + } + + if(state->activityValidSamples < MOTOR_SAFETY_WINDOW_SAMPLES) + { + state->activityHistoryEdges[state->activityHistoryIndex] = + state->rawTotalEdges; + state->activityValidSamples++; + state->recentWindowEdges = + state->rawTotalEdges - state->activityWindowBaseline; + } + else + { + oldestCount = + state->activityHistoryEdges[state->activityHistoryIndex]; + state->activityHistoryEdges[state->activityHistoryIndex] = + state->rawTotalEdges; + state->recentWindowEdges = state->rawTotalEdges - oldestCount; + } + + nextIndex = (uint16_t)(state->activityHistoryIndex + 1u); + if(nextIndex >= MOTOR_SAFETY_WINDOW_SAMPLES) + { + nextIndex = 0u; + } + state->activityHistoryIndex = nextIndex; + + if((state->activityValidSamples >= MOTOR_SAFETY_WINDOW_SAMPLES) && + ((uint32_t)(hallTimebaseTicks - state->activityMonitorStartTicks) >= + MOTOR_MS_TO_TICKS(MOTOR_SAFETY_WINDOW_MS))) + { + state->activityWindowReady = true; + } +} + +static void Motor_SafetyTaskFromIsr(void) +{ + uint32_t index; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + volatile motor_runtime_t *state = &motorRuntime[index]; + + Motor_UpdateActivityWindowFromIsr(index); + + if((state->direction == MOTOR_DIRECTION_STOPPED) || + (state->pendingEvent != MOTOR_EVENT_NONE) || + (false == state->activityWindowReady)) + { + continue; + } + + /* Long synchronized trajectories use pulse-density control whenever + * their requested speed is below the loaded mechanism's proven 50% + * breakaway duty. A zero-duty pause is intentional de-energization, + * not a no-motion fault. The absolute task/phase deadline still + * bounds the pause, and the activity window is reset when drive + * resumes. */ + if((false != state->speedControlEnabled) && + (state->speedCurrentDutyPercent == + MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT)) + { + state->lowHallTracking = false; + state->stallCurrentTracking = false; + continue; + } + + if(false != state->calibrating) + { + state->lowHallTracking = false; + state->stallCurrentTracking = false; + /* An endpoint requires a completely quiet Hall input. Counting + * both edges avoids classifying a slow motor as stopped merely + * because a 500 ms window contains a falling edge but no rising + * edge. Rising edges remain the calibrated position unit. */ + if(state->recentWindowEdges <= MOTOR_ENDPOINT_EDGE_THRESHOLD) + { + Motor_RequestEventFromIsr( + index, + (state->direction == MOTOR_DIRECTION_TOWARD_NEAR) ? + MOTOR_EVENT_NEAR_ENDPOINT : MOTOR_EVENT_FAR_ENDPOINT); + } + continue; + } + + /* + * A normal move is classified as STALL only when both independent + * observations overlap: low Hall activity and sustained high current. + * Low Hall with low/stale current is a separate no-motion/Hall-path + * failure and is stopped with TIMEOUT instead of being mislabeled as + * a mechanical stall. Endpoint seeking above deliberately remains + * Hall-based because no Hall at the requested end is its success + * condition, even though endpoint current can be high. + */ + if(state->recentWindowEdges <= MOTOR_STALL_EDGE_THRESHOLD) + { + bool currentFresh = Motor_CurrentSampleFreshFromIsr(index); + bool currentHigh = currentFresh && + (state->currentSenseDeltaRaw >= + motorProfiles[index].stallCurrentDeltaRaw); + + if(false == state->lowHallTracking) + { + state->lowHallTracking = true; + state->lowHallStartTicks = hallTimebaseTicks; + state->lowHallEdgeBaseline = state->rawTotalEdges; + state->stallCurrentTracking = false; + } + else if(state->rawTotalEdges != state->lowHallEdgeBaseline) + { + /* A slow but healthy mechanism may produce no more than two + * edges in every rolling 500 ms window. Any real Hall edge + * is progress and restarts the no-motion clock; otherwise a + * full-stroke M0 move is incorrectly stopped after five + * seconds even though its Hall count keeps increasing. */ + state->lowHallEdgeBaseline = state->rawTotalEdges; + state->lowHallStartTicks = hallTimebaseTicks; + } + + if(false != currentHigh) + { + if(false == state->stallCurrentTracking) + { + state->stallCurrentTracking = true; + state->stallCurrentStartTicks = hallTimebaseTicks; + } + else if((uint32_t)(hallTimebaseTicks - + state->stallCurrentStartTicks) >= + MOTOR_MS_TO_TICKS( + SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS)) + { + Motor_RequestEventFromIsr(index, MOTOR_EVENT_STALL); + } + else + { + /* High current confirmation is still accumulating. */ + } + } + else + { + state->stallCurrentTracking = false; + } + + if((state->pendingEvent == MOTOR_EVENT_NONE) && + ((uint32_t)(hallTimebaseTicks - state->lowHallStartTicks) >= + MOTOR_MS_TO_TICKS( + SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS))) + { + Motor_RequestEventFromIsr(index, MOTOR_EVENT_TIMEOUT); + } + } + else + { + state->lowHallTracking = false; + state->stallCurrentTracking = false; + } + } +} + +static void Hall_TimebaseIrqHandler(void) +{ + uint32_t index; + + if(Cy_Tcpwm_Counter_GetTC_IntrMasked(HALL_TIMEBASE_TCPWM) != 0ul) + { + Cy_Tcpwm_Counter_ClearTC_Intr(HALL_TIMEBASE_TCPWM); + hallTimebaseTicks++; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + Hall_SampleMotorFromIsr(index); + } + + Motor_CheckOperationTimeoutsFromIsr(); + + safetyTickDivider++; + if(safetyTickDivider >= MOTOR_SAFETY_CHECK_TICKS) + { + safetyTickDivider = 0ul; + Motor_SafetyTaskFromIsr(); + } + } +} + +static uint32_t MotorControl_GetTicks(void) +{ + return hallTimebaseTicks; +} + +static void MotorControlTimers_Init(void) +{ + uint32_t index; + uint32_t divider = SOURCE_CLOCK_HZ / MOTOR_TIMER_INPUT_CLOCK_HZ; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + Cy_SysClk_PeriphAssignDivider(motorProfiles[index].pwmClock, + CY_SYSCLK_DIV_16_BIT, + MOTOR_TIMER_DIVIDER_NO); + } + Cy_SysClk_PeriphAssignDivider(HALL_TIMEBASE_CLOCK, + CY_SYSCLK_DIV_16_BIT, + MOTOR_TIMER_DIVIDER_NO); + Cy_SysClk_PeriphSetDivider(Cy_SysClk_GetClockGroup(motorProfiles[0].pwmClock), + CY_SYSCLK_DIV_16_BIT, + MOTOR_TIMER_DIVIDER_NO, + divider - 1ul); + Cy_SysClk_PeriphEnableDivider(Cy_SysClk_GetClockGroup(motorProfiles[0].pwmClock), + CY_SYSCLK_DIV_16_BIT, + MOTOR_TIMER_DIVIDER_NO); + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + (void)Cy_Tcpwm_Pwm_Init(motorProfiles[index].pwmTcpwm, &motorPwmCfg); + Cy_Tcpwm_Pwm_Enable(motorProfiles[index].pwmTcpwm); + Cy_Tcpwm_Pwm_SetCounter(motorProfiles[index].pwmTcpwm, 0ul); + } + pwmTimersInitialized = true; + + hallTimebaseTicks = 0ul; + safetyTickDivider = 0ul; + (void)Cy_Tcpwm_Counter_Init(HALL_TIMEBASE_TCPWM, &hallTimebaseCfg); + Cy_Tcpwm_Counter_ClearTC_Intr(HALL_TIMEBASE_TCPWM); + Cy_Tcpwm_Counter_SetTC_IntrMask(HALL_TIMEBASE_TCPWM); + + Cy_SysInt_InitIRQ(&hallTimebaseIrqCfg); + Cy_SysInt_SetSystemIrqVector(hallTimebaseIrqCfg.sysIntSrc, + Hall_TimebaseIrqHandler); + NVIC_SetPriority(hallTimebaseIrqCfg.intIdx, 3ul); + NVIC_ClearPendingIRQ(hallTimebaseIrqCfg.intIdx); + NVIC_EnableIRQ(hallTimebaseIrqCfg.intIdx); + + Cy_Tcpwm_Counter_Enable(HALL_TIMEBASE_TCPWM); + Cy_Tcpwm_TriggerStart(HALL_TIMEBASE_TCPWM); + motorControlReady = true; +} + +static void BoardPins_Init(void) +{ + InitOutputPin(DRV_DVDD_EN_PORT, DRV_DVDD_EN_PIN, 0ul); + InitOutputPin(DRV_NCS_PORT, DRV_NCS_PIN, 1ul); + InitOutputPin(DRV_NSLEEP_PORT, DRV_NSLEEP_PIN, 0ul); + /* Keep all bridge outputs disabled until initialization is verified. */ + InitOutputPin(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN, 1ul); + InitOutputPin(DRV_IN1_PORT, DRV_IN1_PIN, 0ul); + InitOutputPin(DRV_IN2_PORT, DRV_IN2_PIN, 0ul); + InitOutputPin(DRV_IN3_PORT, DRV_IN3_PIN, 0ul); + InitOutputPin(DRV_IN4_PORT, DRV_IN4_PIN, 0ul); + Hall_AllInit(); + + spiPinCfg.driveMode = CY_GPIO_DM_HIGHZ; + spiPinCfg.hsiom = DRV_SPI_MISO_MUX; + Cy_GPIO_Pin_Init(DRV_SPI_MISO_PORT, DRV_SPI_MISO_PIN, &spiPinCfg); + + spiPinCfg.driveMode = CY_GPIO_DM_STRONG_IN_OFF; + spiPinCfg.hsiom = DRV_SPI_MOSI_MUX; + Cy_GPIO_Pin_Init(DRV_SPI_MOSI_PORT, DRV_SPI_MOSI_PIN, &spiPinCfg); + + spiPinCfg.driveMode = CY_GPIO_DM_STRONG_IN_OFF; + spiPinCfg.hsiom = DRV_SPI_CLK_MUX; + Cy_GPIO_Pin_Init(DRV_SPI_CLK_PORT, DRV_SPI_CLK_PIN, &spiPinCfg); +} + +void Motor_ForceSafeOff(void) +{ + cy_stc_gpio_pin_config_t safeOutput = + { + .outVal = 0ul, + .driveMode = CY_GPIO_DM_STRONG_IN_OFF, + .hsiom = HSIOM_SEL_GPIO, + .intEdge = 0ul, + .intMask = 0ul, + .vtrip = 0ul, + .slewRate = 0ul, + .driveSel = CY_GPIO_DRIVE_FULL, + }; + + if(false != pwmTimersInitialized) + { + Cy_Tcpwm_TriggerStopOrKill(DRV_IN1_PWM_TCPWM); + Cy_Tcpwm_TriggerStopOrKill(DRV_IN2_PWM_TCPWM); + Cy_Tcpwm_TriggerStopOrKill(DRV_IN3_PWM_TCPWM); + Cy_Tcpwm_TriggerStopOrKill(DRV_IN4_PWM_TCPWM); + } + + (void)Cy_GPIO_Pin_Init(DRV_IN1_PORT, DRV_IN1_PIN, &safeOutput); + (void)Cy_GPIO_Pin_Init(DRV_IN2_PORT, DRV_IN2_PIN, &safeOutput); + (void)Cy_GPIO_Pin_Init(DRV_IN3_PORT, DRV_IN3_PIN, &safeOutput); + (void)Cy_GPIO_Pin_Init(DRV_IN4_PORT, DRV_IN4_PIN, &safeOutput); + Cy_GPIO_Clr(DRV_IN1_PORT, DRV_IN1_PIN); + Cy_GPIO_Clr(DRV_IN2_PORT, DRV_IN2_PIN); + Cy_GPIO_Clr(DRV_IN3_PORT, DRV_IN3_PIN); + Cy_GPIO_Clr(DRV_IN4_PORT, DRV_IN4_PIN); + + safeOutput.outVal = 1ul; + (void)Cy_GPIO_Pin_Init(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN, &safeOutput); + Cy_GPIO_Set(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN); +} + +static void Scb4Spi_Init(void) +{ + Cy_SysClk_PeriphAssignDivider(PCLK_SCB4_CLOCK, CY_SYSCLK_DIV_24_5_BIT, SCB4_DIVIDER_NO); + SetPeripheFracDiv24_5(SPI_INPUT_CLOCK_HZ, SOURCE_CLOCK_HZ, SCB4_DIVIDER_NO); + Cy_SysClk_PeriphEnableDivider(Cy_SysClk_GetClockGroup(PCLK_SCB4_CLOCK), + CY_SYSCLK_DIV_24_5_BIT, + SCB4_DIVIDER_NO); + + Cy_SCB_SPI_DeInit(SCB4); + (void)Cy_SCB_SPI_Init(SCB4, &spiCfg, NULL); + Cy_SCB_SPI_Enable(SCB4); +} + +static bool Drv8718_SpiTransfer16(uint16_t txWord, uint16_t *rxWord) +{ + uint32_t timeout; + uint32_t savedIntrStatus; + bool success = false; + + if(NULL == rxWord) + { + return false; + } + *rxWord = 0u; + + /* SCB4, nSCS and the shared DRV8718 register file have one owner. The + * current application calls this only from the main loop; the guard also + * prevents a future callback or nested service from interleaving frames. */ + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false != driverSpiActive) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return false; + } + driverSpiActive = true; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + Cy_SCB_SPI_ClearRxFifo(SCB4); + Cy_SCB_SPI_ClearTxFifo(SCB4); + + Cy_GPIO_Clr(DRV_NCS_PORT, DRV_NCS_PIN); + Cy_SysTick_DelayInUs(1ul); + + if(Cy_SCB_SPI_Write(SCB4, txWord) != 1ul) + { + goto transfer_done; + } + + timeout = DRV8718_SPI_TIMEOUT_LOOPS; + while((false == Cy_SCB_SPI_IsTxComplete(SCB4)) && (timeout > 0ul)) + { + timeout--; + } + + if(false == Cy_SCB_SPI_IsTxComplete(SCB4)) + { + goto transfer_done; + } + + timeout = DRV8718_SPI_TIMEOUT_LOOPS; + while((0ul == Cy_SCB_SPI_GetNumInRxFifo(SCB4)) && (timeout > 0ul)) + { + timeout--; + } + + if((Cy_SCB_SPI_GetNumInRxFifo(SCB4) > 0ul) && + (Cy_SCB_SPI_ReadArray(SCB4, rxWord, 1ul) == 1ul)) + { + success = ((*rxWord & DRV8718_SDO_HEADER_MASK) == + DRV8718_SDO_HEADER_VALUE); + if((false != success) && + (false != driverStatusMonitoringEnabled) && + ((*rxWord & DRV8718_SDO_ALERT_MASK) != 0u)) + { + success = false; + } + } + +transfer_done: + Cy_SysTick_DelayInUs(1ul); + Cy_GPIO_Set(DRV_NCS_PORT, DRV_NCS_PIN); + Cy_SysTick_DelayInUs(1ul); + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + driverSpiActive = false; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + return success; +} + +static bool Drv8718_WriteRegRaw(uint8_t addr, uint8_t data) +{ + uint16_t frame = DRV8718_WRITE_FRAME(addr, data); + uint16_t rxWord; + + return Drv8718_SpiTransfer16(frame, &rxWord); +} + +static bool Drv8718_ReadRegRaw(uint8_t addr, uint8_t *data) +{ + uint16_t frame = DRV8718_READ_FRAME(addr); + uint16_t rxWord; + + if((NULL == data) || + (false == Drv8718_SpiTransfer16(frame, &rxWord))) + { + return false; + } + + *data = (uint8_t)(rxWord & 0xFFu); + return true; +} + +static bool Drv8718_WriteRegVerified(uint8_t addr, uint8_t data) +{ + uint8_t readback; + + if(false == Drv8718_WriteRegRaw(addr, data)) + { + return false; + } + + return (Drv8718_ReadRegRaw(addr, &readback) && (readback == data)); +} + +static void Motor_EnablePwmInput(const motor_profile_t *motor) +{ + pwmPinCfg.outVal = 0ul; + pwmPinCfg.hsiom = motor->pwmHsiom; + Cy_GPIO_Pin_Init(motor->pwmPort, motor->pwmPin, &pwmPinCfg); +} + +static bool Drv8718_Init(void) +{ + bool success = true; + uint8_t driverStatus = 0u; + + driverStatusMonitoringEnabled = false; + + Cy_GPIO_Set(DRV_DVDD_EN_PORT, DRV_DVDD_EN_PIN); + Cy_GPIO_Set(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN); + Cy_SysTick_DelayInUs(10000ul); + + Cy_GPIO_Set(DRV_NSLEEP_PORT, DRV_NSLEEP_PIN); + Cy_SysTick_DelayInUs(10000ul); + + success = Drv8718_WriteRegVerified(DRV8718_REG_IC_CTRL2, + DRV8718_IC_CTRL2_DRVOFF_MODE); + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_BRG_CTRL1, + MOTOR_COAST_BRG_CTRL1); + } + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_BRG_CTRL2, + MOTOR_COAST_BRG_CTRL2); + } + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_PWM_CTRL1, + DRV8718_PWM_CTRL1_ALL_MOTORS); + } + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_PWM_CTRL2, + DRV8718_PWM_CTRL2_ALL_MOTORS); + } + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_PWM_CTRL3, + MOTOR_PWM_CTRL3_HIGHSIDE); + } + if(false != success) + { + success = Drv8718_WriteRegVerified(DRV8718_REG_PWM_CTRL4, + MOTOR_PWM_CTRL4_ACTIVE_HI); + } + if(false != success) + { + success = Drv8718_WriteRegRaw(DRV8718_REG_IC_CTRL1, + DRV8718_IC_CTRL1_CLEAR_FAULTS); + Cy_SysTick_DelayInUs(1000ul); + } + if(false != success) + { + success = Drv8718_WriteRegVerified( + DRV8718_REG_IC_CTRL1, + DRV8718_IC_CTRL1_ENABLE_UNLOCKED); + } + if(false != success) + { + success = Drv8718_ReadRegRaw(DRV8718_REG_IC_STAT1, &driverStatus) && + ((driverStatus & DRV8718_IC_STAT1_SPI_OK) != 0u) && + ((driverStatus & DRV8718_IC_STAT1_ALERT_MASK) == 0u); + } + + if(false != success) + { + bridgeCtrl1Shadow = MOTOR_COAST_BRG_CTRL1; + bridgeCtrl2Shadow = MOTOR_COAST_BRG_CTRL2; + driverFaultLatched = false; + driverStatusMonitoringEnabled = true; + driverStatusLastPollTicks = hallTimebaseTicks; + Cy_GPIO_Clr(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN); + } + else + { + driverFaultLatched = true; + driverStatusMonitoringEnabled = false; + Cy_GPIO_Set(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN); + } + + return success; +} + +static void Motor_SetPwmInputLow(const motor_profile_t *motor) +{ + pwmPinCfg.outVal = 0ul; + pwmPinCfg.hsiom = HSIOM_SEL_GPIO; + Cy_GPIO_Pin_Init(motor->pwmPort, motor->pwmPin, &pwmPinCfg); + Cy_GPIO_Clr(motor->pwmPort, motor->pwmPin); +} + +static void Motor_EnterDriverFault(void) +{ + uint32_t index; + uint32_t savedIntrStatus; + + driverFaultLatched = true; + driverStatusMonitoringEnabled = false; + Cy_GPIO_Set(DRV_DRVOFF_PORT, DRV_DRVOFF_PIN); + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + if(false != pwmTimersInitialized) + { + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + } + Motor_SetPwmInputLow(&motorProfiles[index]); + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + volatile motor_runtime_t *state = &motorRuntime[index]; + bool hadTarget = state->targetActive || state->eventWasTargetMove || + state->moveResultReady; + + state->direction = MOTOR_DIRECTION_STOPPED; + state->eventDirection = MOTOR_DIRECTION_STOPPED; + state->scheduledDirection = MOTOR_DIRECTION_STOPPED; + state->pendingEvent = MOTOR_EVENT_NONE; + state->lastEvent = MOTOR_EVENT_DRIVER_FAULT; + state->targetActive = false; + state->calibrating = false; + state->startPending = false; + state->coastTracking = false; + state->operationActive = false; + state->eventWasTargetMove = false; + /* The status is shared by all bridges. Once it faults, software + * cannot prove which axis moved before DRVOFF or assign later inertia + * pulses to a reliable direction, so no absolute count remains clean. */ + state->positionValid = false; + if(false != hadTarget) + { + state->moveResultEvent = MOTOR_EVENT_DRIVER_FAULT; + state->moveResultSequence = state->moveCommandSequence; + state->moveResultReady = true; + } + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); +} + +static void Motor_PollDriverStatus(void) +{ + uint8_t driverStatus; + + if((false == driverStatusMonitoringEnabled) || + (false != driverFaultLatched) || + ((uint32_t)(hallTimebaseTicks - driverStatusLastPollTicks) < + MOTOR_MS_TO_TICKS(DRV8718_STATUS_POLL_INTERVAL_MS))) + { + return; + } + + driverStatusLastPollTicks = hallTimebaseTicks; + if((false == Drv8718_ReadRegRaw(DRV8718_REG_IC_STAT1, &driverStatus)) || + ((driverStatus & DRV8718_IC_STAT1_SPI_OK) == 0u) || + ((driverStatus & DRV8718_IC_STAT1_ALERT_MASK) != 0u)) + { + Motor_EnterDriverFault(); + } +} + +static bool Motor_WriteBridgeBits(uint32_t motorIndex, uint8_t bridgeValue) +{ + const motor_profile_t *motor = &motorProfiles[motorIndex]; + uint8_t oldValue; + uint8_t newValue; + bool success; + + if(false != driverFaultLatched) + { + return false; + } + + if(motor->bridgeRegister == DRV8718_REG_BRG_CTRL1) + { + oldValue = bridgeCtrl1Shadow; + newValue = (uint8_t)((oldValue & (uint8_t)(~motor->bridgeMask)) | + (bridgeValue & motor->bridgeMask)); + success = Drv8718_WriteRegVerified(DRV8718_REG_BRG_CTRL1, newValue); + if(false != success) + { + bridgeCtrl1Shadow = newValue; + } + } + else + { + oldValue = bridgeCtrl2Shadow; + newValue = (uint8_t)((oldValue & (uint8_t)(~motor->bridgeMask)) | + (bridgeValue & motor->bridgeMask)); + success = Drv8718_WriteRegVerified(DRV8718_REG_BRG_CTRL2, newValue); + if(false != success) + { + bridgeCtrl2Shadow = newValue; + } + } + + if(false == success) + { + Motor_EnterDriverFault(); + } + return success; +} + +static bool Motor_StopHardware(uint32_t motorIndex) +{ + const motor_profile_t *motor = &motorProfiles[motorIndex]; + + Cy_Tcpwm_TriggerStopOrKill(motor->pwmTcpwm); + Motor_SetPwmInputLow(motor); + return Motor_WriteBridgeBits(motorIndex, 0u); +} + +static bool Motor_PrepareHardwareStart(uint32_t motorIndex, + motor_direction_t direction) +{ + const motor_profile_t *motor = &motorProfiles[motorIndex]; + uint8_t bridgeValue = + (direction == MOTOR_DIRECTION_TOWARD_FAR) ? + motor->towardFarBridgeValue : motor->towardNearBridgeValue; + + /* Last-line application isolation: a build-disabled bridge can never be + * energized, even if a corrupted command reaches this layer. */ + if((SEAT_MOVE_ACTIVE_MOTOR_MASK & + (uint8_t)(1u << motorIndex)) == 0u) + { + return false; + } + + Cy_Tcpwm_TriggerStopOrKill(motor->pwmTcpwm); + Motor_SetPwmInputLow(motor); + if(false == Motor_WriteBridgeBits(motorIndex, bridgeValue)) + { + return false; + } + return true; +} + +static void Motor_CommitPreparedHardwareStartUnsafe(uint32_t motorIndex) +{ + const motor_profile_t *motor = &motorProfiles[motorIndex]; + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + uint8_t dutyPercent = state->fixedDutyPercent; + + /* The bridge was configured while the PWM pin remained GPIO-low. The + * caller holds the Hall ISR critical section while publishing the running + * state and enabling PWM, so a timeout/event cannot be inserted between + * the state transition and the physical start. */ + if((false != state->speedControlEnabled) && + (false == state->calibrating)) + { + dutyPercent = state->speedInitialDutyPercent; + /* Dynamic trajectories may need a low steady-state PWM to stretch a + * short Hall travel over tens of seconds. Give every loaded axis the + * proven 50% breakaway pulse first; after the 500 ms measurement delay + * the closed loop may ramp below the public fixed-duty command floor. */ + if(dutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT) + { + dutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT; + } + } + state->speedCurrentDutyPercent = dutyPercent; + Motor_ResetSpeedMeasurementUnsafe(state); + Motor_ApplyDutyPercent(motorIndex, dutyPercent); + Cy_Tcpwm_Pwm_SetCounter(motor->pwmTcpwm, 0ul); + Cy_Tcpwm_TriggerStart(motor->pwmTcpwm); + Motor_EnablePwmInput(motor); +} + +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) +static const char *Motor_EventToString(motor_event_t event) +{ + switch(event) + { + case MOTOR_EVENT_NONE: + return "none"; + case MOTOR_EVENT_TARGET_REACHED: + return "target_reached"; + case MOTOR_EVENT_NEAR_ENDPOINT: + return "near_endpoint"; + case MOTOR_EVENT_FAR_ENDPOINT: + return "far_endpoint"; + case MOTOR_EVENT_STALL: + return "stall"; + case MOTOR_EVENT_TIMEOUT: + return "timeout"; + case MOTOR_EVENT_ABORTED: + return "aborted"; + case MOTOR_EVENT_DRIVER_FAULT: + return "driver_fault"; + case MOTOR_EVENT_CALIBRATION_FAILED: + return "calibration_failed"; + default: + return "unknown"; + } +} + +static const char *Motor_DirectionToString(motor_direction_t direction) +{ + switch(direction) + { + case MOTOR_DIRECTION_TOWARD_FAR: + return "toward_far"; + case MOTOR_DIRECTION_TOWARD_NEAR: + return "toward_near"; + default: + return "stopped"; + } +} +#endif + +static void Motor_BeginCoastTrackingUnsafe(volatile motor_runtime_t *state, + motor_direction_t direction) +{ + if(direction != MOTOR_DIRECTION_STOPPED) + { + state->coastDirection = direction; + state->coastStartTicks = hallTimebaseTicks; + state->coastTracking = true; + } +} + +static void Motor_SetMoveResultUnsafe(volatile motor_runtime_t *state, + motor_event_t event) +{ + state->moveResultEvent = event; + state->moveResultSequence = state->moveCommandSequence; + state->moveResultReady = true; +} + +static void Motor_BeginMoveCommandUnsafe(volatile motor_runtime_t *state) +{ + state->moveCommandSequence++; + if(0ul == state->moveCommandSequence) + { + /* Reserve zero for "no move command has ever been accepted". */ + state->moveCommandSequence = 1ul; + } + state->moveResultEvent = MOTOR_EVENT_NONE; + state->moveResultSequence = 0ul; + state->moveResultReady = false; +} + +static bool Motor_OperationTimeoutExpiredUnsafe( + const volatile motor_runtime_t *state) +{ + uint32_t timeoutTicks = + ((false != state->calibrating) && + (0ul != state->calibrationTimeoutTicks)) ? + state->calibrationTimeoutTicks : + MOTOR_MS_TO_TICKS(MOTOR_OPERATION_TIMEOUT_MS); + + return (false != state->operationActive) && + ((uint32_t)(hallTimebaseTicks - state->motionStartTicks) >= + timeoutTicks); +} + +static void Motor_CompleteTargetMoveUnsafe(volatile motor_runtime_t *state) +{ + state->direction = MOTOR_DIRECTION_STOPPED; + state->eventDirection = MOTOR_DIRECTION_STOPPED; + state->scheduledDirection = MOTOR_DIRECTION_STOPPED; + state->startPending = false; + state->coastTracking = false; + state->coastDirection = MOTOR_DIRECTION_STOPPED; + state->operationActive = false; + state->targetActive = false; + state->eventWasTargetMove = false; + state->lastEvent = MOTOR_EVENT_TARGET_REACHED; + Motor_SetMoveResultUnsafe(state, MOTOR_EVENT_TARGET_REACHED); +} + +static bool Motor_TryStartScheduledHardware(uint32_t motorIndex, + bool requireCoastElapsed) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + motor_direction_t direction = MOTOR_DIRECTION_STOPPED; + motor_direction_t finalDirection = MOTOR_DIRECTION_STOPPED; + uint32_t savedIntrStatus; + bool eligible = false; + bool targetCompleted = false; + bool committed = false; + bool accepted = false; + + /* Phase one only selects a candidate direction. The PWM input remains + * GPIO-low throughout the (comparatively slow) verified SPI write. */ + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((false != state->startPending) && + (state->pendingEvent == MOTOR_EVENT_NONE) && + (false == driverFaultLatched) && + Motor_CommandIsCurrentUnsafe(state) && + ((false == requireCoastElapsed) || + (((uint32_t)(hallTimebaseTicks - state->scheduledStartTicks) >= + MOTOR_MS_TO_TICKS(MOTOR_REVERSAL_COAST_MS)) && + (false == state->coastTracking)))) + { + if(false != Motor_OperationTimeoutExpiredUnsafe(state)) + { + Motor_RequestEventFromIsr(motorIndex, MOTOR_EVENT_TIMEOUT); + } + else if((false != state->targetActive) && + (state->positionHallCount == state->targetHallCount)) + { + Motor_CompleteTargetMoveUnsafe(state); + targetCompleted = true; + } + else + { + direction = (false != state->targetActive) ? + ((state->targetHallCount > state->positionHallCount) ? + MOTOR_DIRECTION_TOWARD_FAR : + MOTOR_DIRECTION_TOWARD_NEAR) : + state->scheduledDirection; + eligible = (direction != MOTOR_DIRECTION_STOPPED); + } + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != targetCompleted) + { + /* A completed settling cycle must leave both the PWM input and bridge + * high impedance, even if a future caller never services again. */ + return Motor_StopHardware(motorIndex); + } + if(false == eligible) + { + return false; + } + if(false == Motor_PrepareHardwareStart(motorIndex, direction)) + { + return false; + } + + /* Phase two rechecks every ISR-owned condition after SPI. PWM and the + * published running direction are committed together with interrupts + * masked, closing the timeout/target race at hardware start. */ + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((false != state->startPending) && + (state->pendingEvent == MOTOR_EVENT_NONE) && + (false == driverFaultLatched) && + Motor_CommandIsCurrentUnsafe(state)) + { + if(false != Motor_OperationTimeoutExpiredUnsafe(state)) + { + Motor_RequestEventFromIsr(motorIndex, MOTOR_EVENT_TIMEOUT); + } + else + { + if(false != state->targetActive) + { + if(state->positionHallCount == state->targetHallCount) + { + Motor_CompleteTargetMoveUnsafe(state); + targetCompleted = true; + } + else + { + finalDirection = + (state->targetHallCount > state->positionHallCount) ? + MOTOR_DIRECTION_TOWARD_FAR : + MOTOR_DIRECTION_TOWARD_NEAR; + } + } + else + { + finalDirection = state->scheduledDirection; + } + + if((false == targetCompleted) && + (finalDirection == direction)) + { + state->direction = direction; + state->scheduledDirection = MOTOR_DIRECTION_STOPPED; + state->startPending = false; + state->coastTracking = false; + state->coastDirection = MOTOR_DIRECTION_STOPPED; + Motor_ResetActivityWindowUnsafe(state); + Motor_CommitPreparedHardwareStartUnsafe(motorIndex); + committed = true; + accepted = true; + } + else if(false == targetCompleted) + { + /* A last coast pulse changed which side of the target we are + * on during SPI. Keep the command pending and retry from the + * newly observed side on the next service pass. */ + state->scheduledDirection = finalDirection; + accepted = true; + } + else + { + accepted = true; + } + } + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false == committed) + { + if((false != driverFaultLatched) || + (false == Motor_StopHardware(motorIndex))) + { + return false; + } + } + + return accepted; +} + +static uint8_t Motor_StartDirectionInternal(motor_mode_t mode, + motor_direction_t direction, + bool targetActive, + uint32_t targetHallCount, + bool calibrating, + bool clearLastEvent, + motor_direction_t coastFromDirection, + bool preserveOperationTimer) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + motor_direction_t currentDirection; + bool needsCoast; + bool stopCurrentHardware; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != safetyInhibitLatched) || (false != driverFaultLatched) || + ((direction != MOTOR_DIRECTION_TOWARD_FAR) && + (direction != MOTOR_DIRECTION_TOWARD_NEAR))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE) || + (false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index]))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + currentDirection = motorRuntime[index].direction; + + if(coastFromDirection == MOTOR_DIRECTION_STOPPED) + { + coastFromDirection = currentDirection; + if((coastFromDirection == MOTOR_DIRECTION_STOPPED) && + (false != motorRuntime[index].coastTracking)) + { + coastFromDirection = motorRuntime[index].coastDirection; + } + } + stopCurrentHardware = (currentDirection != MOTOR_DIRECTION_STOPPED); + needsCoast = (coastFromDirection != MOTOR_DIRECTION_STOPPED) && + ((currentDirection == MOTOR_DIRECTION_STOPPED) || + (coastFromDirection != direction)); + + if(false != stopCurrentHardware) + { + /* Close the ISR race before the slower SPI bridge update below. */ + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + } + motorRuntime[index].pendingEvent = MOTOR_EVENT_NONE; + motorRuntime[index].eventDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].targetActive = targetActive; + motorRuntime[index].targetHallCount = targetHallCount; + motorRuntime[index].calibrating = calibrating; + motorRuntime[index].eventWasTargetMove = false; + if((false != targetActive) && (false != clearLastEvent)) + { + Motor_BeginMoveCommandUnsafe(&motorRuntime[index]); + } + if((false == preserveOperationTimer) || + (false == motorRuntime[index].operationActive)) + { + motorRuntime[index].motionStartTicks = hallTimebaseTicks; + motorRuntime[index].operationActive = true; + motorRuntime[index].automaticReverseCount = 0u; + } + if(false != clearLastEvent) + { + motorRuntime[index].lastEvent = MOTOR_EVENT_NONE; + } + if(false != needsCoast) + { + motorRuntime[index].direction = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].scheduledDirection = direction; + motorRuntime[index].scheduledStartTicks = hallTimebaseTicks; + motorRuntime[index].startPending = true; + Motor_BeginCoastTrackingUnsafe(&motorRuntime[index], + coastFromDirection); + } + else + { + /* Direct starts use the same two-phase commit as delayed reversals. + * Keeping startPending set lets the 1 ms ISR cancel an expired + * operation while the verified bridge SPI write is in progress. */ + motorRuntime[index].direction = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].scheduledDirection = direction; + motorRuntime[index].scheduledStartTicks = hallTimebaseTicks; + motorRuntime[index].startPending = true; + motorRuntime[index].coastTracking = false; + motorRuntime[index].coastDirection = MOTOR_DIRECTION_STOPPED; + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != stopCurrentHardware) + { + Motor_SetPwmInputLow(&motorProfiles[index]); + if(false == Motor_WriteBridgeBits(index, 0u)) + { + return MOTOR_API_FAILURE; + } + } + + if(false != needsCoast) + { + return MOTOR_API_SUCCESS; + } + + return (false != Motor_TryStartScheduledHardware(index, false)) ? + MOTOR_API_SUCCESS : MOTOR_API_FAILURE; +} + +static void Motor_ServiceScheduledStart(uint32_t motorIndex) +{ + (void)Motor_TryStartScheduledHardware(motorIndex, true); +} + +static void Motor_ServicePendingEvent(uint32_t motorIndex) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + motor_event_t event; + motor_direction_t eventDirection; + motor_direction_t reverseDirection = MOTOR_DIRECTION_STOPPED; +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + uint32_t recentPulses; + uint32_t position; +#endif + uint32_t savedIntrStatus; + bool wasCalibrating; + bool hadTargetMove; + bool shouldReverse = false; + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + event = state->pendingEvent; + if(event == MOTOR_EVENT_NONE) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + + eventDirection = state->eventDirection; + wasCalibrating = state->calibrating; + hadTargetMove = state->eventWasTargetMove; +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + recentPulses = state->recentWindowEdges; +#endif + state->pendingEvent = MOTOR_EVENT_NONE; + state->direction = MOTOR_DIRECTION_STOPPED; + state->eventDirection = MOTOR_DIRECTION_STOPPED; + state->scheduledDirection = MOTOR_DIRECTION_STOPPED; + state->startPending = false; + state->targetActive = false; + state->lastEvent = event; + state->eventWasTargetMove = false; + Motor_BeginCoastTrackingUnsafe(state, eventDirection); + + if(false != hadTargetMove) + { + /* Crossing the requested target completes the move without automatic + * reverse correction. Inertia Hall pulses remain assigned to the old + * direction and are preserved in the global position; the final count + * is allowed to remain beyond the requested target. */ + Motor_SetMoveResultUnsafe(state, event); + } + + if(event == MOTOR_EVENT_NEAR_ENDPOINT) + { + reverseDirection = MOTOR_DIRECTION_TOWARD_FAR; + shouldReverse = (false == wasCalibrating) && + state->reverseAtEndpoint && + (state->automaticReverseCount < + MOTOR_MAX_AUTOMATIC_REVERSALS); + } + else if(event == MOTOR_EVENT_FAR_ENDPOINT) + { + reverseDirection = MOTOR_DIRECTION_TOWARD_NEAR; + shouldReverse = (false == wasCalibrating) && + state->reverseAtEndpoint && + (state->automaticReverseCount < + MOTOR_MAX_AUTOMATIC_REVERSALS); + } + else if(event == MOTOR_EVENT_STALL) + { + reverseDirection = + (eventDirection == MOTOR_DIRECTION_TOWARD_FAR) ? + MOTOR_DIRECTION_TOWARD_NEAR : MOTOR_DIRECTION_TOWARD_FAR; + shouldReverse = (false == wasCalibrating) && + state->reverseAtStall && + (state->automaticReverseCount < + MOTOR_MAX_AUTOMATIC_REVERSALS); + } + else + { + /* Target and timeout events always stop. */ + } + if(false != shouldReverse) + { + state->automaticReverseCount++; + } + else + { + state->operationActive = false; + } +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + position = state->positionHallCount; +#endif + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false == Motor_StopHardware(motorIndex)) + { + return; + } + + MOTOR_LOG("MOTOR_EVENT mode=%lu profile=%s event=%s direction=%s " + "recent_%lums_edges=%lu hall_position=%lu reverse=%lu\r\n", + (unsigned long)motorIndex, + motorProfiles[motorIndex].name, + Motor_EventToString(event), + Motor_DirectionToString(eventDirection), + (unsigned long)MOTOR_SAFETY_WINDOW_MS, + (unsigned long)recentPulses, + (unsigned long)position, + (unsigned long)((false != shouldReverse) ? 1ul : 0ul)); + + if(false != shouldReverse) + { + (void)Motor_StartDirectionInternal((motor_mode_t)motorIndex, + reverseDirection, + false, + 0ul, + false, + false, + eventDirection, + true); + } +} + +static void Motor_SpeedControlService(uint32_t motorIndex) +{ + volatile motor_runtime_t *state = &motorRuntime[motorIndex]; + uint32_t nowTicks; + uint32_t baseTicks; + uint32_t baseRawEdges; + uint32_t rawEdges; + uint32_t elapsedTicks; + uint32_t elapsedMs; + uint32_t actualPpsX100; + uint32_t targetPpsX100; + uint32_t planElapsedMs; + uint32_t plannedProgressPulses; + uint32_t actualProgressPulses; + uint32_t savedIntrStatus; + uint8_t historyIndex; + uint8_t newDutyPercent; + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + nowTicks = hallTimebaseTicks; + if((false == state->speedControlEnabled) || + (state->direction == MOTOR_DIRECTION_STOPPED) || + (false != state->calibrating) || + (false != state->startPending) || + (state->pendingEvent != MOTOR_EVENT_NONE) || + (false != driverFaultLatched) || + ((uint32_t)(nowTicks - state->speedLastSampleTicks) < + MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_CHECK_MS))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + + rawEdges = state->rawRisingEdges; + historyIndex = state->speedHistoryIndex; + if(state->speedHistoryValidSamples < + MOTOR_SPEED_CONTROL_HISTORY_SAMPLES) + { + baseRawEdges = state->speedBaselineRawEdges; + baseTicks = state->speedControlStartTicks; + state->speedHistoryValidSamples++; + } + else + { + baseRawEdges = state->speedHistoryRawEdges[historyIndex]; + baseTicks = state->speedHistoryTicks[historyIndex]; + } + + state->speedHistoryRawEdges[historyIndex] = rawEdges; + state->speedHistoryTicks[historyIndex] = nowTicks; + historyIndex++; + if(historyIndex >= MOTOR_SPEED_CONTROL_HISTORY_SAMPLES) + { + historyIndex = 0u; + } + state->speedHistoryIndex = historyIndex; + state->speedLastSampleTicks = nowTicks; + + elapsedTicks = (uint32_t)(nowTicks - baseTicks); + planElapsedMs = MOTOR_TICKS_TO_MS( + (uint32_t)(nowTicks - state->speedControlStartTicks)); + if(planElapsedMs > state->speedDurationMs) + { + planElapsedMs = state->speedDurationMs; + } + plannedProgressPulses = (uint32_t) + (((uint64_t)state->speedPlannedHallPulses * planElapsedMs + + (state->speedDurationMs / 2ul)) / state->speedDurationMs); + actualProgressPulses = rawEdges - state->speedBaselineRawEdges; + + /* Loaded axes need a short 50% breakaway pulse, but waiting for the full + * 500 ms speed window lets a short-travel axis build an unrecoverable + * lead. Position progress is meaningful from the first 100 ms sample, + * so de-energize an already-ahead axis immediately. Driving it at an + * arbitrary sub-breakaway duty can create current without Hall progress + * and was the cause of false cradle STALLs. */ + if(((uint32_t)(nowTicks - state->speedControlStartTicks) >= + MOTOR_MS_TO_TICKS(MOTOR_SPEED_PROGRESS_CONTROL_START_MS)) && + (actualProgressPulses > + (plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES))) + { + if(state->speedCurrentDutyPercent != + MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT) + { + state->speedCurrentDutyPercent = + MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT; + Motor_ApplyDutyPercent(motorIndex, + MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT); + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + if(((uint32_t)(nowTicks - state->speedControlStartTicks) < + MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_START_DELAY_MS)) || + (elapsedTicks < MOTOR_MS_TO_TICKS(MOTOR_SPEED_CONTROL_WINDOW_MS))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + + elapsedMs = MOTOR_TICKS_TO_MS(elapsedTicks); + if(0ul == elapsedMs) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + + actualPpsX100 = (uint32_t) + (((((uint64_t)(rawEdges - baseRawEdges)) * 1000ull * + MOTOR_SPEED_VALUE_SCALE) + (elapsedMs / 2ul)) / elapsedMs); + targetPpsX100 = state->speedTargetPpsX100; + newDutyPercent = state->speedCurrentDutyPercent; + state->speedActualPpsX100 = actualPpsX100; + + /* Speed-only regulation cannot synchronize very short and long axes: the + * loaded motor breakaway pulse puts a short travel several seconds ahead + * even when its eventual speed is correct. Track the time-position line + * as well. An axis ahead of the line drops immediately to the adaptive + * floor; an axis behind it ramps up. Within one Hall pulse, the ordinary + * measured-speed loop keeps each energized burst bounded. */ + if(actualProgressPulses > + (plannedProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES)) + { + newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT; + } + else if((actualProgressPulses + MOTOR_SPEED_PROGRESS_TOLERANCE_PULSES) < + plannedProgressPulses) + { + if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT) + { + newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT; + } + else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT) + { + newDutyPercent = (uint8_t) + (newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT); + if(newDutyPercent > MOTOR_SPEED_MAX_DUTY_PERCENT) + { + newDutyPercent = MOTOR_SPEED_MAX_DUTY_PERCENT; + } + } + } + else if((((uint64_t)actualPpsX100) * 100ull) < + (((uint64_t)targetPpsX100) * + (100ul - MOTOR_SPEED_TOLERANCE_PERCENT))) + { + if(newDutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT) + { + newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT; + } + else if(newDutyPercent < MOTOR_SPEED_MAX_DUTY_PERCENT) + { + newDutyPercent = (uint8_t) + (newDutyPercent + MOTOR_SPEED_DUTY_STEP_PERCENT); + if(newDutyPercent > MOTOR_SPEED_MAX_DUTY_PERCENT) + { + newDutyPercent = MOTOR_SPEED_MAX_DUTY_PERCENT; + } + } + } + else if((((uint64_t)actualPpsX100) * 100ull) > + (((uint64_t)targetPpsX100) * + (100ul + MOTOR_SPEED_TOLERANCE_PERCENT))) + { + if(newDutyPercent > MOTOR_SPEED_MIN_DUTY_PERCENT) + { + if(newDutyPercent >= (MOTOR_SPEED_MIN_DUTY_PERCENT + + MOTOR_SPEED_DUTY_STEP_PERCENT)) + { + newDutyPercent = (uint8_t) + (newDutyPercent - MOTOR_SPEED_DUTY_STEP_PERCENT); + } + else + { + newDutyPercent = MOTOR_SPEED_MIN_DUTY_PERCENT; + } + } + else if(newDutyPercent != MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT) + { + newDutyPercent = MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT; + } + } + else + { + /* The measured speed is inside the +/-10 percent deadband. */ + } + + if(newDutyPercent != state->speedCurrentDutyPercent) + { + if((state->speedCurrentDutyPercent == + MOTOR_SPEED_CONTROL_PAUSE_DUTY_PERCENT) && + (newDutyPercent >= MOTOR_SPEED_MIN_DUTY_PERCENT)) + { + /* Resume gets a fresh 500 ms activity window, matching a normal + * hardware start. This prevents the first loaded-current sample + * from being compared with the intentionally quiet pause. */ + Motor_ResetActivityWindowUnsafe(state); + } + state->speedCurrentDutyPercent = newDutyPercent; + Motor_ApplyDutyPercent(motorIndex, newDutyPercent); + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); +} + +void Motor_ServiceAll(void) +{ + uint32_t index; + uint32_t savedIntrStatus; + uint8_t abortMask; + + if(false == motorControlReady) + { + return; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false != motorServiceActive) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return; + } + motorServiceActive = true; + abortMask = abortRequestedMask; + abortRequestedMask = 0u; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(0u != abortMask) + { + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + if((abortMask & (uint8_t)(1u << index)) != 0u) + { + bool stopRequested; + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + stopRequested = motorRuntime[index].abortSequenceValid && + (false == Motor_CommandIsCurrentUnsafe( + &motorRuntime[index])); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + if(false != stopRequested) + { + (void)Motor_StopMode((motor_mode_t)index); + } + } + } + } + + /* ADC polling is main-loop-only. The 2 ms Hall ISR consumes only the + * atomically published cache and can never block on a conversion. */ + Motor_ServiceCurrentSensing(); + Motor_PollDriverStatus(); + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + Motor_ServicePendingEvent(index); + Motor_ServiceScheduledStart(index); + Motor_SpeedControlService(index); + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + motorServiceActive = false; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); +} + +bool Motor_IsDriverFaultLatched(void) +{ + return driverFaultLatched; +} + +uint8_t Motor_AcknowledgeCommand(motor_mode_t mode, uint16_t sequence) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + volatile motor_runtime_t *state; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + state = &motorRuntime[index]; + if(false != safetyInhibitLatched) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + if((false != state->acceptedCommandValid) && + Motor_SequenceIsNewer(state->acceptedCommandSequence, sequence)) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + if(false != state->abortSequenceValid) + { + if(state->latestAbortSequence == sequence) + { + state->abortSequenceValid = false; + abortRequestedMask &= (uint8_t)(~(uint8_t)(1u << index)); + } + else + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + } + state->acceptedCommandSequence = sequence; + state->acceptedCancellationGeneration = state->cancellationGeneration; + state->acceptedCommandValid = true; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +bool Motor_IsCommandCurrent(motor_mode_t mode, uint16_t sequence) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + bool current; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return false; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + current = Motor_CommandIsCurrentUnsafe(&motorRuntime[index]) && + motorRuntime[index].acceptedCommandValid && + (motorRuntime[index].acceptedCommandSequence == sequence); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return current; +} + +void Motor_RequestAbortMaskFromIsr(uint8_t motorMask, uint16_t sequence) +{ + uint32_t index; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + if((motorMask & (uint8_t)(1u << index)) != 0u) + { + volatile motor_runtime_t *state = &motorRuntime[index]; + bool staleAccepted = state->acceptedCommandValid && + Motor_SequenceIsNewer(state->acceptedCommandSequence, + sequence); + bool staleAbort = state->abortSequenceValid && + Motor_SequenceIsNewer(state->latestAbortSequence, sequence); + + if((false == staleAccepted) && (false == staleAbort)) + { + state->latestAbortSequence = sequence; + state->cancellationGeneration++; + state->abortSequenceValid = true; + abortRequestedMask |= (uint8_t)(1u << index); + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + } + } + } +} + +void Motor_RequestAbortAllFromIsr(uint16_t sequence) +{ + uint32_t index; + + /* System/STOP_ALL cancellation is also the protocol resynchronization + * barrier. It must remain effective even when sequence restarts at zero. */ + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + motorRuntime[index].latestAbortSequence = sequence; + motorRuntime[index].cancellationGeneration++; + motorRuntime[index].abortSequenceValid = true; + motorRuntime[index].acceptedCommandValid = false; + abortRequestedMask |= (uint8_t)(1u << index); + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + } +} + +void Motor_LatchSafetyInhibitFromIsr(uint16_t sequence) +{ + /* Publish the independent gate before the sequence-based abort. A later + * STOP may replace that sequence for resynchronization, but it must not + * clear an independent latched safety shutdown. */ + safetyInhibitLatched = true; + Motor_RequestAbortAllFromIsr(sequence); +} + +void Motor_StopAll(void) +{ + uint32_t index; + + for(index = 0ul; index < MOTOR_PROFILE_COUNT; index++) + { + (void)Motor_StopMode((motor_mode_t)index); + } +} + +uint8_t Motor_StopMode(motor_mode_t mode) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + motor_direction_t oldDirection; + motor_event_t priorLastEvent; + bool hadTargetMove; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + oldDirection = motorRuntime[index].direction; + priorLastEvent = motorRuntime[index].lastEvent; + hadTargetMove = motorRuntime[index].targetActive || + motorRuntime[index].eventWasTargetMove; + Motor_BeginCoastTrackingUnsafe(&motorRuntime[index], oldDirection); + motorRuntime[index].direction = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].eventDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].scheduledDirection = MOTOR_DIRECTION_STOPPED; + motorRuntime[index].pendingEvent = MOTOR_EVENT_NONE; + if(false != driverFaultLatched) + { + motorRuntime[index].lastEvent = MOTOR_EVENT_DRIVER_FAULT; + } + else if(false != hadTargetMove) + { + motorRuntime[index].lastEvent = MOTOR_EVENT_ABORTED; + } + else if((priorLastEvent == MOTOR_EVENT_NEAR_ENDPOINT) || + (priorLastEvent == MOTOR_EVENT_FAR_ENDPOINT) || + (priorLastEvent == MOTOR_EVENT_STALL) || + (priorLastEvent == MOTOR_EVENT_TIMEOUT) || + (priorLastEvent == MOTOR_EVENT_DRIVER_FAULT) || + (priorLastEvent == MOTOR_EVENT_CALIBRATION_FAILED)) + { + /* The bridge has already stopped for this terminal event. Preserve + * it across the manager's cleanup stop and a later STOP_ALL so 0x303 + * keeps the physical failure reason available to the host. Starting + * a genuinely new motion command still clears lastEvent. */ + motorRuntime[index].lastEvent = priorLastEvent; + } + else + { + motorRuntime[index].lastEvent = MOTOR_EVENT_NONE; + } + motorRuntime[index].targetActive = false; + motorRuntime[index].calibrating = false; + motorRuntime[index].startPending = false; + motorRuntime[index].operationActive = false; + motorRuntime[index].eventWasTargetMove = false; + if(false != hadTargetMove) + { + Motor_SetMoveResultUnsafe(&motorRuntime[index], MOTOR_EVENT_ABORTED); + } + else + { + /* An explicit stop invalidates any unconsumed completion from an + * older command, so a later Wait cannot report a stale result. */ + motorRuntime[index].moveResultEvent = MOTOR_EVENT_NONE; + motorRuntime[index].moveResultSequence = 0ul; + motorRuntime[index].moveResultReady = false; + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != driverFaultLatched) + { + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + Motor_SetPwmInputLow(&motorProfiles[index]); + return MOTOR_API_FAILURE; + } + + return (false != Motor_StopHardware(index)) ? + MOTOR_API_SUCCESS : MOTOR_API_FAILURE; +} + +uint8_t Motor_SetFixedDutyPercent(motor_mode_t mode, uint8_t dutyPercent) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + bool applyImmediately; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != safetyInhibitLatched) || + (dutyPercent < MOTOR_SPEED_MIN_DUTY_PERCENT) || + (dutyPercent > MOTOR_SPEED_MAX_DUTY_PERCENT) || + (false != driverFaultLatched)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((false != safetyInhibitLatched) || (false != driverFaultLatched) || + (false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index]))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + motorRuntime[index].fixedDutyPercent = dutyPercent; + motorRuntime[index].speedControlEnabled = false; + motorRuntime[index].speedDurationMs = 0ul; + motorRuntime[index].speedTravelValueX100 = 0ul; + motorRuntime[index].speedPlannedHallPulses = 0ul; + motorRuntime[index].speedTargetPpsX100 = 0ul; + motorRuntime[index].speedInitialDutyPercent = dutyPercent; + motorRuntime[index].speedCurrentDutyPercent = dutyPercent; + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + applyImmediately = + (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) && + (false == motorRuntime[index].calibrating) && + (motorRuntime[index].pendingEvent == MOTOR_EVENT_NONE); + if(false != applyImmediately) + { + Motor_ApplyDutyPercent(index, dutyPercent); + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_StartOpenLoop(motor_mode_t mode, motor_direction_t direction) +{ + return Motor_StartDirectionInternal(mode, + direction, + false, + 0ul, + false, + true, + MOTOR_DIRECTION_STOPPED, + false); +} + +bool Motor_IsMoving(motor_mode_t mode) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + bool moving; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return false; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + moving = (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) || + motorRuntime[index].startPending || + motorRuntime[index].coastTracking; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return moving; +} + +bool Motor_WaitUntilStopped(motor_mode_t mode) +{ + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return false; + } + if(Motor_IsMoving(mode)) + { + (void)Motor_StopMode(mode); + } + return Motor_WaitForCoastToSettle(mode) && !Motor_IsMoving(mode); +} + +uint8_t Motor_ValidatePersistentState(motor_mode_t mode, + const motor_persistent_state_t *state) +{ + uint32_t index = (uint32_t)mode; + + if((NULL == state) || (false == Motor_ModeIsValid(mode)) || + (0ul == state->maxHallCount) || + (false == Motor_CalibrationRangeIsPlausible( + index, state->maxHallCount)) || + (state->positionValid && + (state->positionHallCount > state->maxHallCount)) || + (0ul == state->maxSpeedPulsesPerSecondX100) || + (state->maxSpeedPulsesPerSecondX100 > + (motorProfiles[index].maxSpeedPpsX100 * + MOTOR_MAX_CALIBRATED_SPEED_MULTIPLIER))) + { + return MOTOR_API_FAILURE; + } + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_ApplyPersistentState(motor_mode_t mode, + const motor_persistent_state_t *state) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + + if((false == motorControlReady) || + (MOTOR_API_SUCCESS != Motor_ValidatePersistentState(mode, state))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) || + motorRuntime[index].startPending || + motorRuntime[index].coastTracking || + (motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE)) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + motorRuntime[index].positionHallCount = state->positionHallCount; + motorRuntime[index].maxHallCount = state->maxHallCount; + motorRuntime[index].maxSpeedPpsX100 = + state->maxSpeedPulsesPerSecondX100; + motorRuntime[index].rawRisingEdges = state->rawRisingEdges; + motorRuntime[index].positionValid = state->positionValid; + motorRuntime[index].lastEvent = MOTOR_EVENT_NONE; + Motor_ResetActivityWindowUnsafe(&motorRuntime[index]); + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_GetPersistentState(motor_mode_t mode, + motor_persistent_state_t *state) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + + if((NULL == state) || (false == motorControlReady) || + (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + state->positionHallCount = motorRuntime[index].positionHallCount; + state->maxHallCount = motorRuntime[index].maxHallCount; + state->maxSpeedPulsesPerSecondX100 = + motorRuntime[index].maxSpeedPpsX100; + state->rawRisingEdges = motorRuntime[index].rawRisingEdges; + state->positionValid = motorRuntime[index].positionValid; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_InvalidatePosition(motor_mode_t mode) +{ + motor_persistent_state_t state; + + if(mode >= MOTOR_MODE_COUNT) + { + return MOTOR_API_FAILURE; + } + /* The CAN hook may run in the same owner-loop iteration that accepted a + * STOP. Finish the bounded stop and Hall-coast transition before + * enforcing the persistent-state API's stopped-axis precondition. */ + if(Motor_IsMoving(mode)) + { + (void)Motor_StopMode(mode); + } + if((false == Motor_WaitForCoastToSettle(mode)) || + Motor_IsMoving(mode) || + (MOTOR_API_SUCCESS != Motor_GetPersistentState(mode, &state))) + { + return MOTOR_API_FAILURE; + } + state.positionValid = false; + return Motor_ApplyPersistentState(mode, &state); +} + +uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode, + uint32_t maxSpeedPpsX100) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != safetyInhibitLatched) || + (false != driverFaultLatched) || (0ul == maxSpeedPpsX100) || + (maxSpeedPpsX100 > + (motorProfiles[index].maxSpeedPpsX100 * + MOTOR_MAX_CALIBRATED_SPEED_MULTIPLIER))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) || + motorRuntime[index].startPending || + motorRuntime[index].coastTracking || + (motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE) || + (false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index]))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + motorRuntime[index].maxSpeedPpsX100 = maxSpeedPpsX100; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint32_t Motor_GetTimeMs(void) +{ + return MOTOR_TICKS_TO_MS(hallTimebaseTicks); +} + +uint8_t Motor_GetCurrentInfo(motor_mode_t mode, motor_current_info_t *info) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + uint32_t ageTicks; + + if((NULL == info) || (false == motorControlReady) || + (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + ageTicks = hallTimebaseTicks - + motorRuntime[index].currentSenseLastSampleTicks; + info->mode = mode; + info->sampleValid = motorRuntime[index].currentSenseValid && + (ageTicks <= + MOTOR_MS_TO_TICKS(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS)); + info->baselineValid = motorRuntime[index].currentSenseBaselineValid; + info->raw = motorRuntime[index].currentSenseRaw; + info->baselineRaw = motorRuntime[index].currentSenseBaselineRaw; + info->deltaRaw = motorRuntime[index].currentSenseDeltaRaw; + info->stallThresholdRaw = motorProfiles[index].stallCurrentDeltaRaw; + info->highCurrent = info->sampleValid && info->baselineValid && + (info->deltaRaw >= info->stallThresholdRaw); + info->hallActivityLow = motorRuntime[index].activityWindowReady && + (motorRuntime[index].recentWindowEdges <= + MOTOR_STALL_EDGE_THRESHOLD); + info->calibrating = motorRuntime[index].calibrating; + info->sampleAgeMs = MOTOR_TICKS_TO_MS(ageTicks); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +void Motor_SetWatchdogServiceEnabled(bool enabled) +{ + watchdogServiceEnabled = enabled; +} + +uint8_t Motor_GetPositionInfo(motor_mode_t mode, motor_position_info_t *info) +{ + uint32_t index = (uint32_t)mode; + uint32_t hallCount; + uint32_t scaledHallCount; + uint32_t maxHallCount; + uint32_t maxValueX100; + uint32_t savedIntrStatus; + + if((NULL == info) || (false == motorControlReady) || + (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + hallCount = motorRuntime[index].positionHallCount; + maxHallCount = motorRuntime[index].maxHallCount; + info->positionValid = motorRuntime[index].positionValid; + info->direction = motorRuntime[index].direction; + info->lastEvent = motorRuntime[index].lastEvent; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(0ul == maxHallCount) + { + return MOTOR_API_FAILURE; + } + scaledHallCount = hallCount; + if(scaledHallCount > maxHallCount) + { + scaledHallCount = maxHallCount; + } + + maxValueX100 = motorProfiles[index].maxPhysicalValueX100; + info->mode = mode; + info->hallCount = hallCount; + info->maxHallCount = maxHallCount; + info->maxValueX100 = maxValueX100; + info->valueX100 = (uint32_t) + (((((uint64_t)scaledHallCount) * maxValueX100) + + (maxHallCount / 2ul)) / + maxHallCount); + info->percentX100 = (uint32_t) + (((((uint64_t)scaledHallCount) * 10000ul) + + (maxHallCount / 2ul)) / + maxHallCount); + info->horizontalMmX100 = 0ul; + info->verticalMmX100 = 0ul; + if(mode == MOTOR_MODE_0_LINEAR) + { + info->positionMmX100 = info->valueX100; + info->horizontalMmX100 = info->valueX100; + info->angleDegX100 = 0ul; + } + else if(mode == MOTOR_MODE_1_ANGLE) + { + info->positionMmX100 = info->valueX100; + info->horizontalMmX100 = (uint32_t) + (((((uint64_t)scaledHallCount) * + MOTOR_MODE1_MAX_HORIZONTAL_MM_X100) + + (maxHallCount / 2ul)) / + maxHallCount); + info->verticalMmX100 = info->valueX100; + info->angleDegX100 = 0ul; + } + else + { + info->positionMmX100 = 0ul; + info->angleDegX100 = info->valueX100; + } + + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_SetSafetyOptions(motor_mode_t mode, + const motor_safety_options_t *options) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + + if((NULL == options) || (false == motorControlReady) || + (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + motorRuntime[index].reverseAtEndpoint = options->reverseAtEndpoint; + motorRuntime[index].reverseAtStall = options->reverseAtStall; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_SetSpeedPlan(motor_mode_t mode, + uint32_t durationMs, + uint32_t travelValueX100) +{ + uint32_t index = (uint32_t)mode; + uint32_t maxHallCount; + uint32_t plannedHallPulses; + uint32_t targetPpsX100; + uint32_t savedIntrStatus; + uint8_t initialDutyPercent; + bool applyImmediately; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != safetyInhibitLatched) || (false != driverFaultLatched)) + { + return MOTOR_API_FAILURE; + } + + Motor_ServiceAll(); + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + maxHallCount = motorRuntime[index].maxHallCount; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false == Motor_CalculateSpeedPlan(index, + maxHallCount, + durationMs, + travelValueX100, + &plannedHallPulses, + &targetPpsX100, + &initialDutyPercent)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((false != safetyInhibitLatched) || (false != driverFaultLatched) || + (false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index]))) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + + motorRuntime[index].speedDurationMs = durationMs; + motorRuntime[index].speedTravelValueX100 = travelValueX100; + motorRuntime[index].speedPlannedHallPulses = plannedHallPulses; + motorRuntime[index].speedTargetPpsX100 = targetPpsX100; + motorRuntime[index].speedInitialDutyPercent = initialDutyPercent; + motorRuntime[index].speedCurrentDutyPercent = initialDutyPercent; + motorRuntime[index].speedControlEnabled = true; + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + + applyImmediately = + (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) && + (false == motorRuntime[index].calibrating) && + (motorRuntime[index].pendingEvent == MOTOR_EVENT_NONE); + if(false != applyImmediately) + { + Motor_ApplyDutyPercent(index, initialDutyPercent); + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_DisableSpeedControl(motor_mode_t mode) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + bool applyImmediately; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + Motor_ServiceAll(); + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + motorRuntime[index].speedControlEnabled = false; + motorRuntime[index].speedDurationMs = 0ul; + motorRuntime[index].speedTravelValueX100 = 0ul; + motorRuntime[index].speedPlannedHallPulses = 0ul; + motorRuntime[index].speedTargetPpsX100 = 0ul; + motorRuntime[index].speedInitialDutyPercent = + motorRuntime[index].fixedDutyPercent; + motorRuntime[index].speedCurrentDutyPercent = + motorRuntime[index].fixedDutyPercent; + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + + applyImmediately = + (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) && + (false == motorRuntime[index].calibrating) && + (motorRuntime[index].pendingEvent == MOTOR_EVENT_NONE) && + (false == driverFaultLatched); + if(false != applyImmediately) + { + Motor_ApplyDutyPercent(index, motorRuntime[index].fixedDutyPercent); + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return (false == driverFaultLatched) ? + MOTOR_API_SUCCESS : MOTOR_API_FAILURE; +} + +uint8_t Motor_GetSpeedInfo(motor_mode_t mode, motor_speed_info_t *info) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + + if((NULL == info) || (false == motorControlReady) || + (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + info->mode = mode; + info->enabled = motorRuntime[index].speedControlEnabled; + info->durationMs = motorRuntime[index].speedDurationMs; + info->travelValueX100 = motorRuntime[index].speedTravelValueX100; + info->plannedHallPulses = motorRuntime[index].speedPlannedHallPulses; + info->targetPulsesPerSecondX100 = + motorRuntime[index].speedTargetPpsX100; + info->actualPulsesPerSecondX100 = + motorRuntime[index].speedActualPpsX100; + info->initialDutyPercent = motorRuntime[index].speedInitialDutyPercent; + info->currentDutyPercent = motorRuntime[index].speedCurrentDutyPercent; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_StartMoveToValue(motor_mode_t mode, uint32_t targetValueX100) +{ + uint32_t index = (uint32_t)mode; + uint32_t maxValueX100; + uint32_t targetHallCount = 0ul; + uint32_t savedIntrStatus; + volatile motor_runtime_t *state; + motor_direction_t directStartDirection = MOTOR_DIRECTION_STOPPED; + motor_direction_t actualDirection; + bool stopHardware = false; + bool startDirectly = false; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != driverFaultLatched)) + { + return MOTOR_API_FAILURE; + } + + Motor_ServiceAll(); + maxValueX100 = motorProfiles[index].maxPhysicalValueX100; + if(targetValueX100 > maxValueX100) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + state = &motorRuntime[index]; + if((false == state->positionValid) || (0ul == state->maxHallCount) || + (false != state->calibrating) || + (state->pendingEvent != MOTOR_EVENT_NONE) || + (false == Motor_CommandIsCurrentUnsafe(state)) || + (false != driverFaultLatched)) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + return MOTOR_API_FAILURE; + } + + targetHallCount = (uint32_t) + (((((uint64_t)targetValueX100) * state->maxHallCount) + + (maxValueX100 / 2ul)) / maxValueX100); + actualDirection = state->direction; + + /* All target replacement decisions use this single atomic position + * snapshot. This avoids selecting a direction from one Hall count and + * committing the command after an ISR has already crossed the target. */ + Motor_BeginMoveCommandUnsafe(state); + state->targetHallCount = targetHallCount; + state->targetActive = true; + state->calibrating = false; + state->eventWasTargetMove = false; + state->lastEvent = MOTOR_EVENT_NONE; + + if(false != state->startPending) + { + /* An explicit direction-change coast is already in progress. Keep + * its original coast and absolute timeout clocks; the delayed-start + * service recomputes the direction requested by the latest command + * from the final settled Hall count. */ + state->scheduledDirection = + (targetHallCount == state->positionHallCount) ? + MOTOR_DIRECTION_STOPPED : + ((targetHallCount > state->positionHallCount) ? + MOTOR_DIRECTION_TOWARD_FAR : MOTOR_DIRECTION_TOWARD_NEAR); + if(false == state->operationActive) + { + state->motionStartTicks = hallTimebaseTicks; + state->operationActive = true; + state->automaticReverseCount = 0u; + } + } + else if(actualDirection != MOTOR_DIRECTION_STOPPED) + { + bool targetStillAhead = + ((actualDirection == MOTOR_DIRECTION_TOWARD_FAR) && + (targetHallCount > state->positionHallCount)) || + ((actualDirection == MOTOR_DIRECTION_TOWARD_NEAR) && + (targetHallCount < state->positionHallCount)); + + if(false == state->operationActive) + { + state->motionStartTicks = hallTimebaseTicks; + state->operationActive = true; + state->automaticReverseCount = 0u; + } + + if(false == targetStillAhead) + { + /* The replacement target is at or behind the live position. Kill + * PWM in the same critical section, count inertia in the old + * direction, and apply the explicitly requested direction change + * only after settling. */ + Cy_Tcpwm_TriggerStopOrKill(motorProfiles[index].pwmTcpwm); + state->direction = MOTOR_DIRECTION_STOPPED; + state->scheduledDirection = MOTOR_DIRECTION_STOPPED; + state->scheduledStartTicks = hallTimebaseTicks; + state->startPending = true; + Motor_BeginCoastTrackingUnsafe(state, actualDirection); + stopHardware = true; + } + /* A target still ahead in the actual direction is an atomic target + * replacement only: PWM, safety window, and timeout are preserved. */ + } + else if(false != state->coastTracking) + { + /* Even a same-direction retarget waits for residual motion to settle. + * Otherwise a coast pulse during SPI can cross the target just before + * PWM is enabled and force an avoidable one-pulse overshoot. */ + state->scheduledDirection = + (targetHallCount == state->positionHallCount) ? + MOTOR_DIRECTION_STOPPED : + ((targetHallCount > state->positionHallCount) ? + MOTOR_DIRECTION_TOWARD_FAR : MOTOR_DIRECTION_TOWARD_NEAR); + state->scheduledStartTicks = state->coastStartTicks; + state->startPending = true; + if(false == state->operationActive) + { + state->motionStartTicks = hallTimebaseTicks; + state->operationActive = true; + state->automaticReverseCount = 0u; + } + } + else if(targetHallCount == state->positionHallCount) + { + /* With no running or coasting direction, Hall transitions cannot alter + * the inferred global position; immediate exact completion is safe. */ + state->automaticReverseCount = 0u; + Motor_CompleteTargetMoveUnsafe(state); + } + else + { + directStartDirection = + (targetHallCount > state->positionHallCount) ? + MOTOR_DIRECTION_TOWARD_FAR : MOTOR_DIRECTION_TOWARD_NEAR; + startDirectly = true; + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != stopHardware) + { + return (false != Motor_StopHardware(index)) ? + MOTOR_API_SUCCESS : MOTOR_API_FAILURE; + } + if(false != startDirectly) + { + /* The command sequence was already created atomically above. */ + return Motor_StartDirectionInternal(mode, + directStartDirection, + true, + targetHallCount, + false, + false, + MOTOR_DIRECTION_STOPPED, + false); + } + + return MOTOR_API_SUCCESS; +} + +uint8_t Motor_WaitForMove(motor_mode_t mode) +{ + uint32_t index = (uint32_t)mode; + uint32_t savedIntrStatus; + uint32_t waitSequence; + uint32_t currentSequence; + motor_event_t resultEvent; + bool resultReady; + bool movePending; + bool motorStillRunning; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode))) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + waitSequence = motorRuntime[index].moveCommandSequence; + resultReady = motorRuntime[index].moveResultReady && + (motorRuntime[index].moveResultSequence == waitSequence); + movePending = motorRuntime[index].targetActive || + motorRuntime[index].startPending || + ((motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE) && + motorRuntime[index].eventWasTargetMove); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if((0ul == waitSequence) || ((false == resultReady) && + (false == movePending))) + { + return MOTOR_API_FAILURE; + } + + for(;;) + { + Motor_ServiceAll(); + Motor_CalibrationServiceHook(); + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + currentSequence = motorRuntime[index].moveCommandSequence; + resultReady = motorRuntime[index].moveResultReady && + (motorRuntime[index].moveResultSequence == waitSequence); + resultEvent = motorRuntime[index].moveResultEvent; + movePending = motorRuntime[index].targetActive || + motorRuntime[index].startPending || + ((motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE) && + motorRuntime[index].eventWasTargetMove); + motorStillRunning = + (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) || + motorRuntime[index].startPending || + motorRuntime[index].coastTracking; + if((false != resultReady) && (false == motorStillRunning)) + { + motorRuntime[index].moveResultReady = false; + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(currentSequence != waitSequence) + { + return MOTOR_API_FAILURE; + } + if(false != resultReady) + { + if(false == motorStillRunning) + { + return (resultEvent == MOTOR_EVENT_TARGET_REACHED) ? + MOTOR_API_SUCCESS : MOTOR_API_FAILURE; + } + + /* A requested safety reverse is part of the same operation. + * Wait remains blocking until the one permitted reverse stops at + * its next safety event or the absolute operation timeout. */ + movePending = true; + } + if(false == movePending) + { + return MOTOR_API_FAILURE; + } + + if(false != watchdogServiceEnabled) + { + Cy_WDT_ClearWatchdog(); + } + __WFI(); + } +} + +uint8_t Motor_MoveToValue(motor_mode_t mode, uint32_t targetValueX100) +{ + if(MOTOR_API_SUCCESS != Motor_StartMoveToValue(mode, targetValueX100)) + { + return MOTOR_API_FAILURE; + } + + return Motor_WaitForMove(mode); +} + +static bool Motor_WaitForCoastToSettle(motor_mode_t mode) +{ + uint32_t index = (uint32_t)mode; + uint32_t waitStartTicks = MotorControl_GetTicks(); + uint32_t savedIntrStatus; + bool coastTracking; + bool invalidState; + + for(;;) + { + Motor_ServiceAll(); + Motor_CalibrationServiceHook(); + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + coastTracking = motorRuntime[index].coastTracking; + invalidState = (false != driverFaultLatched) || + (false == Motor_CommandIsCurrentUnsafe( + &motorRuntime[index])) || + (motorRuntime[index].direction != + MOTOR_DIRECTION_STOPPED) || + (false != motorRuntime[index].startPending) || + (motorRuntime[index].pendingEvent != MOTOR_EVENT_NONE); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != invalidState) + { + return false; + } + if(false == coastTracking) + { + return true; + } + if((uint32_t)(MotorControl_GetTicks() - waitStartTicks) >= + MOTOR_MS_TO_TICKS(MOTOR_COAST_SETTLE_TIMEOUT_MS)) + { + return false; + } + + if(false != watchdogServiceEnabled) + { + Cy_WDT_ClearWatchdog(); + } + __WFI(); + } +} + +static void Motor_UpdateCalibrationTravel(uint32_t motorIndex, + uint32_t rawRisingStart, + uint32_t rawTotalStart, + uint32_t *travelPulses, + uint32_t *travelEdges) +{ + uint32_t rawRisingEnd; + uint32_t rawTotalEnd; + uint32_t savedIntrStatus; + + if((NULL != travelPulses) || (NULL != travelEdges)) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + rawRisingEnd = motorRuntime[motorIndex].rawRisingEdges; + rawTotalEnd = motorRuntime[motorIndex].rawTotalEdges; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + if(NULL != travelPulses) + { + *travelPulses = rawRisingEnd - rawRisingStart; + } + if(NULL != travelEdges) + { + *travelEdges = rawTotalEnd - rawTotalStart; + } + } +} + +static uint8_t Motor_CalibrationProbeMotion( + motor_mode_t mode, + motor_direction_t direction, + uint32_t legTimeoutMs, + uint32_t *travelEdges) +{ + uint32_t index = (uint32_t)mode; + uint32_t verifyEdges = MOTOR_HOME_PROBE_VERIFY_EDGES; + uint32_t rawTotalStart; + uint32_t rawTotalEnd; + uint32_t savedIntrStatus; + motor_event_t lastEvent; + motor_direction_t currentDirection; + bool startPending; + + if(NULL != travelEdges) + { + *travelEdges = 0ul; + } + + /* A Hall-only endpoint search cannot distinguish "already at endpoint" + * from "bridge energized but the loaded motor never started". When a + * near seek reports an endpoint with too little travel, move across a few + * real Hall transitions and then let the caller seek near again. Both + * edges are used for this proof so its result does not depend on Hall + * phase or travel direction. */ + if(false == Motor_WaitForCoastToSettle(mode)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + motorRuntime[index].calibrationTimeoutTicks = + MOTOR_MS_TO_TICKS(legTimeoutMs); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(MOTOR_API_SUCCESS != + Motor_StartDirectionInternal(mode, + direction, + false, + 0ul, + true, + true, + MOTOR_DIRECTION_STOPPED, + false)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + /* Captured atomically with PWM commit by + * Motor_ResetActivityWindowUnsafe(). */ + rawTotalStart = motorRuntime[index].activityTotalBaseline; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + for(;;) + { + Motor_ServiceAll(); + Motor_CalibrationServiceHook(); + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + rawTotalEnd = motorRuntime[index].rawTotalEdges; + lastEvent = motorRuntime[index].lastEvent; + currentDirection = motorRuntime[index].direction; + startPending = motorRuntime[index].startPending; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if((rawTotalEnd - rawTotalStart) >= verifyEdges) + { + if((MOTOR_API_SUCCESS != Motor_StopMode(mode)) || + (false == Motor_WaitForCoastToSettle(mode))) + { + Motor_UpdateCalibrationTravel(index, + 0ul, + rawTotalStart, + NULL, + travelEdges); + return MOTOR_API_FAILURE; + } + Motor_UpdateCalibrationTravel(index, + 0ul, + rawTotalStart, + NULL, + travelEdges); + return MOTOR_API_SUCCESS; + } + + if((lastEvent != MOTOR_EVENT_NONE) || + ((currentDirection == MOTOR_DIRECTION_STOPPED) && + (false == startPending))) + { + (void)Motor_StopMode(mode); + (void)Motor_WaitForCoastToSettle(mode); + Motor_UpdateCalibrationTravel(index, + 0ul, + rawTotalStart, + NULL, + travelEdges); + return MOTOR_API_FAILURE; + } + + if(false != watchdogServiceEnabled) + { + Cy_WDT_ClearWatchdog(); + } + __WFI(); + } +} + +static uint8_t Motor_CalibrationSeekEndpoint(motor_mode_t mode, + motor_direction_t direction, + uint32_t legTimeoutMs, + uint32_t *travelPulses, + uint32_t *travelEdges) +{ + uint32_t index = (uint32_t)mode; + uint32_t rawRisingStart; + uint32_t rawRisingEnd; + uint32_t rawTotalStart; + uint32_t rawTotalEnd; + uint32_t savedIntrStatus; + motor_event_t expectedEvent = + (direction == MOTOR_DIRECTION_TOWARD_NEAR) ? + MOTOR_EVENT_NEAR_ENDPOINT : MOTOR_EVENT_FAR_ENDPOINT; + motor_event_t lastEvent; + motor_direction_t currentDirection; + bool startPending; + bool coastTracking; + + if(NULL != travelPulses) + { + *travelPulses = 0ul; + } + if(NULL != travelEdges) + { + *travelEdges = 0ul; + } + + /* Exclude residual motion from a previous operation or calibration leg. + * This wait is Hall-only and bounded, matching the observable hardware. */ + if(false == Motor_WaitForCoastToSettle(mode)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + motorRuntime[index].calibrationTimeoutTicks = + MOTOR_MS_TO_TICKS(legTimeoutMs); + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(MOTOR_API_SUCCESS != + Motor_StartDirectionInternal(mode, + direction, + false, + 0ul, + true, + true, + MOTOR_DIRECTION_STOPPED, + false)) + { + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + /* Motor_ResetActivityWindowUnsafe captured both baselines in the same + * critical section that committed PWM start, after the bridge SPI phase. */ + rawRisingStart = motorRuntime[index].activityRisingBaseline; + rawTotalStart = motorRuntime[index].activityTotalBaseline; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + for(;;) + { + Motor_ServiceAll(); + Motor_CalibrationServiceHook(); + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + lastEvent = motorRuntime[index].lastEvent; + currentDirection = motorRuntime[index].direction; + startPending = motorRuntime[index].startPending; + coastTracking = motorRuntime[index].coastTracking; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if((lastEvent == expectedEvent) && + (currentDirection == MOTOR_DIRECTION_STOPPED) && + (false == startPending)) + { + if((false != coastTracking) && + (false == Motor_WaitForCoastToSettle(mode))) + { + (void)Motor_StopMode(mode); + Motor_UpdateCalibrationTravel(index, + rawRisingStart, + rawTotalStart, + travelPulses, + travelEdges); + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((motorRuntime[index].lastEvent != expectedEvent) || + (motorRuntime[index].direction != MOTOR_DIRECTION_STOPPED) || + (false != motorRuntime[index].startPending) || + (false != motorRuntime[index].coastTracking) || + (false == Motor_CommandIsCurrentUnsafe( + &motorRuntime[index])) || + (false != driverFaultLatched)) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + (void)Motor_StopMode(mode); + Motor_UpdateCalibrationTravel(index, + rawRisingStart, + rawTotalStart, + travelPulses, + travelEdges); + return MOTOR_API_FAILURE; + } + rawRisingEnd = motorRuntime[index].rawRisingEdges; + rawTotalEnd = motorRuntime[index].rawTotalEdges; + motorRuntime[index].calibrating = false; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + if(NULL != travelPulses) + { + *travelPulses = rawRisingEnd - rawRisingStart; + } + if(NULL != travelEdges) + { + *travelEdges = rawTotalEnd - rawTotalStart; + } + return MOTOR_API_SUCCESS; + } + + if((lastEvent != MOTOR_EVENT_NONE) || + ((currentDirection == MOTOR_DIRECTION_STOPPED) && + (false == startPending))) + { + (void)Motor_StopMode(mode); + (void)Motor_WaitForCoastToSettle(mode); + Motor_UpdateCalibrationTravel(index, + rawRisingStart, + rawTotalStart, + travelPulses, + travelEdges); + return MOTOR_API_FAILURE; + } + + if(false != watchdogServiceEnabled) + { + Cy_WDT_ClearWatchdog(); + } + __WFI(); + } +} + +static bool Motor_CalibrationRangeIsPlausible(uint32_t motorIndex, + uint32_t measuredPulses) +{ + uint64_t nominal = motorProfiles[motorIndex].defaultMaxHallCount; + uint64_t minimum = + (nominal * MOTOR_CALIBRATION_RANGE_MIN_PERCENT) / 100ull; + uint64_t maximum = + (nominal * MOTOR_CALIBRATION_RANGE_MAX_PERCENT) / 100ull; + + return (measuredPulses >= 8ul) && + (((uint64_t)measuredPulses) >= minimum) && + (((uint64_t)measuredPulses) <= maximum); +} + +uint8_t Motor_Calibrate(motor_mode_t mode, + motor_calibration_action_t action, + uint32_t legTimeoutMs, + motor_calibration_result_t *result) +{ + motor_calibration_result_t localResult; + uint32_t index = (uint32_t)mode; + uint32_t measuredPulses = 0ul; + uint32_t measuredEdges = 0ul; + uint32_t probeEdges = 0ul; + uint32_t preloadEdges = 0ul; + uint8_t probeAttempt = 0u; +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + uint32_t firstNearPulses = 0ul; + uint32_t firstNearEdges = 0ul; +#endif + uint32_t previousMaxHallCount = 0ul; + uint32_t speedDurationMs = 0ul; + uint32_t speedTravelValueX100 = 0ul; + uint32_t speedPlannedHallPulses = 0ul; + uint32_t speedTargetPpsX100 = 0ul; + uint32_t savedIntrStatus; + uint8_t speedInitialDutyPercent = (uint8_t)MOTOR_PWM_DUTY_PERCENT; + uint8_t status = MOTOR_API_FAILURE; + bool speedPlanEnabled = false; + bool speedPlanValid = false; + + localResult.nearEndpointFound = false; + localResult.farEndpointFound = false; + localResult.measuredMaxHallCount = 0ul; + + if((false == motorControlReady) || (false == Motor_ModeIsValid(mode)) || + (false != safetyInhibitLatched) || (false != driverFaultLatched) || + (0ul == legTimeoutMs) || + ((action != MOTOR_CALIBRATE_FIND_NEAR) && + (action != MOTOR_CALIBRATE_FIND_FAR) && + (action != MOTOR_CALIBRATE_FIND_BOTH))) + { + if(NULL != result) + { + *result = localResult; + } + return MOTOR_API_FAILURE; + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index])) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + if(NULL != result) + { + *result = localResult; + } + return MOTOR_API_FAILURE; + } + previousMaxHallCount = motorRuntime[index].maxHallCount; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(MOTOR_API_SUCCESS != Motor_StopMode(mode)) + { + goto calibration_failed; + } + + MOTOR_LOG("CAL_START mode=%lu profile=%s action=%lu\r\n", + (unsigned long)index, + motorProfiles[index].name, + (unsigned long)action); + + if(action == MOTOR_CALIBRATE_FIND_NEAR) + { + status = Motor_CalibrationSeekEndpoint( + mode, MOTOR_DIRECTION_TOWARD_NEAR, + legTimeoutMs, &measuredPulses, &measuredEdges); +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + firstNearPulses = measuredPulses; + firstNearEdges = measuredEdges; +#endif + if((status == MOTOR_API_SUCCESS) && + (measuredEdges < MOTOR_HOME_PROBE_VERIFY_EDGES)) + { + /* Do not turn a non-start into a fake Hall zero. A genuine + * already-near condition is proved by moving across several Hall + * edges toward far and seeking the same path back toward near. */ + /* A loaded mechanism can need more than one bounded energize + * cycle to leave a mechanical hard stop. Retry the departure, + * but still require real Hall edges before accepting the home. + * This improves endpoint breakaway without weakening the open + * Hall / disconnected-motor diagnostic. Keep the same four-edge + * proof on M3 as the other axes: accepting a single complete Hall + * pulse can make a temporarily stuck legrest look like a real + * mechanical endpoint and incorrectly rewrite its position to + * Hall=0 before the linkage is fully lowered. */ + for(probeAttempt = 0u; probeAttempt < 3u; probeAttempt++) + { + status = Motor_CalibrationProbeMotion( + mode, + MOTOR_DIRECTION_TOWARD_FAR, + legTimeoutMs, + &probeEdges); + if(status == MOTOR_API_SUCCESS) + { + break; + } + if((mode == MOTOR_MODE_3_LINEAR_TRACK) && + (MOTOR_API_SUCCESS != Motor_SetFixedDutyPercent( + mode, SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT))) + { + status = MOTOR_API_FAILURE; + break; + } + /* A failed probe has already stopped and settled the bridge. + * Briefly re-energize toward the known hard stop before the + * next departure. On the loaded M3 linkage this alternating + * preload releases static lash which repeated starts in the + * same direction do not. Its result is deliberately ignored: + * only a later far probe with real Hall edges can pass. */ + if(probeAttempt < 2u) + { + preloadEdges = 0ul; + (void)Motor_CalibrationProbeMotion( + mode, + MOTOR_DIRECTION_TOWARD_NEAR, + legTimeoutMs, + &preloadEdges); + } + } + if(status == MOTOR_API_SUCCESS) + { + status = Motor_CalibrationSeekEndpoint( + mode, MOTOR_DIRECTION_TOWARD_NEAR, + legTimeoutMs, &measuredPulses, &measuredEdges); + if((status == MOTOR_API_SUCCESS) && + (((uint64_t)measuredEdges + + MOTOR_HOME_RETURN_EDGE_TOLERANCE) < + (uint64_t)probeEdges)) + { + status = MOTOR_API_FAILURE; + } + } + } +#if (MOTOR_DIAGNOSTIC_LOG_ENABLE != 0u) + MOTOR_LOG("CAL_HOME_VERIFY mode=%lu first_rising=%lu " + "first_edges=%lu probe_edges=%lu return_rising=%lu " + "return_edges=%lu status=%lu\r\n", + (unsigned long)index, + (unsigned long)firstNearPulses, + (unsigned long)firstNearEdges, + (unsigned long)probeEdges, + (unsigned long)measuredPulses, + (unsigned long)measuredEdges, + (unsigned long)status); +#endif + if(status == MOTOR_API_SUCCESS) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index])) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + status = MOTOR_API_FAILURE; + goto calibration_failed; + } + motorRuntime[index].positionHallCount = 0ul; + motorRuntime[index].positionValid = true; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + localResult.nearEndpointFound = true; + } + else + { + status = MOTOR_API_FAILURE; + } + } + else if(action == MOTOR_CALIBRATE_FIND_FAR) + { + status = Motor_CalibrationSeekEndpoint( + mode, MOTOR_DIRECTION_TOWARD_FAR, + legTimeoutMs, &measuredPulses, NULL); + if(status == MOTOR_API_SUCCESS) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index])) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + status = MOTOR_API_FAILURE; + goto calibration_failed; + } + motorRuntime[index].positionHallCount = previousMaxHallCount; + motorRuntime[index].positionValid = true; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + localResult.farEndpointFound = true; + } + else + { + status = MOTOR_API_FAILURE; + } + } + else + { + status = Motor_CalibrationSeekEndpoint( + mode, MOTOR_DIRECTION_TOWARD_NEAR, legTimeoutMs, NULL, NULL); + if(status == MOTOR_API_SUCCESS) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index])) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + status = MOTOR_API_FAILURE; + goto calibration_failed; + } + motorRuntime[index].positionHallCount = 0ul; + motorRuntime[index].positionValid = false; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + localResult.nearEndpointFound = true; + status = Motor_CalibrationSeekEndpoint( + mode, MOTOR_DIRECTION_TOWARD_FAR, + legTimeoutMs, &measuredPulses, NULL); + } + + if((status == MOTOR_API_SUCCESS) && + Motor_CalibrationRangeIsPlausible(index, measuredPulses)) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + speedPlanEnabled = motorRuntime[index].speedControlEnabled; + speedDurationMs = motorRuntime[index].speedDurationMs; + speedTravelValueX100 = + motorRuntime[index].speedTravelValueX100; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + + if(false != speedPlanEnabled) + { + speedPlanValid = Motor_CalculateSpeedPlan( + index, + measuredPulses, + speedDurationMs, + speedTravelValueX100, + &speedPlannedHallPulses, + &speedTargetPpsX100, + &speedInitialDutyPercent); + } + + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if(false == Motor_CommandIsCurrentUnsafe(&motorRuntime[index])) + { + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + status = MOTOR_API_FAILURE; + goto calibration_failed; + } + motorRuntime[index].maxHallCount = measuredPulses; + motorRuntime[index].positionHallCount = measuredPulses; + motorRuntime[index].positionValid = true; + if(false != speedPlanEnabled) + { + if(false != speedPlanValid) + { + motorRuntime[index].speedPlannedHallPulses = + speedPlannedHallPulses; + motorRuntime[index].speedTargetPpsX100 = + speedTargetPpsX100; + motorRuntime[index].speedInitialDutyPercent = + speedInitialDutyPercent; + motorRuntime[index].speedCurrentDutyPercent = + speedInitialDutyPercent; + Motor_ResetSpeedMeasurementUnsafe(&motorRuntime[index]); + } + else + { + /* A range change can make an old physical speed plan + * exceed the measured motor maximum. Disable it rather + * than silently commanding an impossible speed. */ + motorRuntime[index].speedControlEnabled = false; + motorRuntime[index].speedDurationMs = 0ul; + motorRuntime[index].speedTravelValueX100 = 0ul; + motorRuntime[index].speedPlannedHallPulses = 0ul; + motorRuntime[index].speedTargetPpsX100 = 0ul; + motorRuntime[index].speedActualPpsX100 = 0ul; + motorRuntime[index].speedInitialDutyPercent = + (uint8_t)MOTOR_PWM_DUTY_PERCENT; + motorRuntime[index].speedCurrentDutyPercent = + (uint8_t)MOTOR_PWM_DUTY_PERCENT; + } + } + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + localResult.farEndpointFound = true; + localResult.measuredMaxHallCount = measuredPulses; + } + else + { + status = MOTOR_API_FAILURE; + } + } + + if((status == MOTOR_API_SUCCESS) && + (action != MOTOR_CALIBRATE_FIND_BOTH)) + { + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + localResult.measuredMaxHallCount = motorRuntime[index].maxHallCount; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + } + +calibration_failed: + if(status != MOTOR_API_SUCCESS) + { + if(false == driverFaultLatched) + { + (void)Motor_StopMode(mode); + } + + /* Physical motion cannot be rolled back. Preserve the live Hall + * count, but invalidate the absolute position after any failed + * calibration. Only the uncommitted range parameter is restored. */ + savedIntrStatus = Cy_SysLib_EnterCriticalSection(); + if((action == MOTOR_CALIBRATE_FIND_BOTH) && + (false != localResult.nearEndpointFound)) + { + /* The near endpoint established a new zero. Keep every observed + * far-leg pulse even when it exceeds the previous range. */ + motorRuntime[index].positionHallCount = measuredPulses; + } + motorRuntime[index].maxHallCount = previousMaxHallCount; + motorRuntime[index].positionValid = false; + motorRuntime[index].calibrating = false; + motorRuntime[index].operationActive = false; + motorRuntime[index].lastEvent = (false != driverFaultLatched) ? + MOTOR_EVENT_DRIVER_FAULT : + MOTOR_EVENT_CALIBRATION_FAILED; + Cy_SysLib_ExitCriticalSection(savedIntrStatus); + } + + if(NULL != result) + { + *result = localResult; + } + + MOTOR_LOG("CAL_DONE mode=%lu status=%lu near=%lu far=%lu max_hall=%lu\r\n", + (unsigned long)index, + (unsigned long)status, + (unsigned long)((false != localResult.nearEndpointFound) ? 1ul : 0ul), + (unsigned long)((false != localResult.farEndpointFound) ? 1ul : 0ul), + (unsigned long)localResult.measuredMaxHallCount); + return status; +} + +uint8_t Motor0_GetPosition(motor_position_info_t *info) +{ + return Motor_GetPositionInfo(MOTOR_MODE_0_LINEAR, info); +} + +uint8_t Motor1_GetPosition(motor_position_info_t *info) +{ + return Motor_GetPositionInfo(MOTOR_MODE_1_ANGLE, info); +} + +uint8_t Motor2_GetAngle(motor_position_info_t *info) +{ + return Motor_GetPositionInfo(MOTOR_MODE_2_ANGLE, info); +} + +uint8_t Motor3_GetAngle(motor_position_info_t *info) +{ + return Motor_GetPositionInfo(MOTOR_MODE_3_LINEAR_TRACK, info); +} + +uint8_t Motor0_MoveToPositionMmX100(uint32_t positionMmX100) +{ + return Motor_MoveToValue(MOTOR_MODE_0_LINEAR, positionMmX100); +} + +uint8_t Motor1_MoveToVerticalMmX100(uint32_t verticalMmX100) +{ + return Motor_MoveToValue(MOTOR_MODE_1_ANGLE, verticalMmX100); +} + +uint8_t Motor2_MoveToAngleDegX100(uint32_t angleDegX100) +{ + return Motor_MoveToValue(MOTOR_MODE_2_ANGLE, angleDegX100); +} + +uint8_t Motor3_MoveToAngleDegX100(uint32_t angleDegX100) +{ + return Motor_MoveToValue(MOTOR_MODE_3_LINEAR_TRACK, angleDegX100); +} + +uint8_t Motor0_SetSpeedPlan(uint32_t durationMs, uint32_t distanceMmX100) +{ + return Motor_SetSpeedPlan(MOTOR_MODE_0_LINEAR, + durationMs, + distanceMmX100); +} + +uint8_t Motor1_SetSpeedPlan(uint32_t durationMs, uint32_t verticalMmX100) +{ + return Motor_SetSpeedPlan(MOTOR_MODE_1_ANGLE, + durationMs, + verticalMmX100); +} + +uint8_t Motor2_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100) +{ + return Motor_SetSpeedPlan(MOTOR_MODE_2_ANGLE, + durationMs, + angleDegX100); +} + +uint8_t Motor3_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100) +{ + return Motor_SetSpeedPlan(MOTOR_MODE_3_LINEAR_TRACK, + durationMs, + angleDegX100); +} + +uint8_t Motor_Init(void) +{ + bool driverInitOk; + bool currentSenseInitOk; + + MOTOR_LOG("\r\nCYT4BFBCJE CM0+ DRV8718S-Q1 motor demo start\r\n"); + + safetyInhibitLatched = false; + BoardPins_Init(); + Scb4Spi_Init(); + driverInitOk = Drv8718_Init(); + currentSenseInitOk = BspSeatCurrentAdc_Init(); + MotorControlTimers_Init(); + if((false == driverInitOk) || (false == currentSenseInitOk) || + !Motor_PrimeCurrentSensing()) + { + Motor_EnterDriverFault(); + return MOTOR_API_FAILURE; + } + + MOTOR_LOG("MOTOR_TIMING pwm_hz=%lu hall_sample_us=%lu safety_check_ms=%lu " + "window_ms=%lu startup_grace_ms=%lu/%lu/%lu/%lu " + "endpoint_edge_threshold=%lu stall_edge_threshold=%lu\r\n", + (unsigned long)MOTOR_PWM_FREQUENCY_HZ, + (unsigned long)HALL_SAMPLE_INTERVAL_US, + (unsigned long)MOTOR_SAFETY_CHECK_INTERVAL_MS, + (unsigned long)MOTOR_SAFETY_WINDOW_MS, + (unsigned long)MOTOR_MODE0_STARTUP_GRACE_MS, + (unsigned long)MOTOR_MODE1_STARTUP_GRACE_MS, + (unsigned long)MOTOR_MODE2_STARTUP_GRACE_MS, + (unsigned long)MOTOR_MODE3_STARTUP_GRACE_MS, + (unsigned long)MOTOR_ENDPOINT_EDGE_THRESHOLD, + (unsigned long)MOTOR_STALL_EDGE_THRESHOLD); + + MOTOR_LOG("MOTOR_CURRENT sample_ms=%lu max_age_ms=%lu " + "confirm_ms=%lu no_motion_ms=%lu thresholds=%u/%u/%u/%u\r\n", + (unsigned long)SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS, + (unsigned long)SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS, + (unsigned long)SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS, + (unsigned long)SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS, + (unsigned int)motorProfiles[0].stallCurrentDeltaRaw, + (unsigned int)motorProfiles[1].stallCurrentDeltaRaw, + (unsigned int)motorProfiles[2].stallCurrentDeltaRaw, + (unsigned int)motorProfiles[3].stallCurrentDeltaRaw); + + MOTOR_LOG("MOTOR_READY position_is_invalid_until_endpoint_calibration\r\n"); + + return MOTOR_API_SUCCESS; +} + +/* [] END OF FILE */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.h b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.h new file mode 100644 index 0000000..ffe68d4 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Driver/motor_control.h @@ -0,0 +1,277 @@ +#ifndef MOTOR_CONTROL_H +#define MOTOR_CONTROL_H + +#include +#include + +#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 */ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewd b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewd new file mode 100644 index 0000000..557cdc1 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewd @@ -0,0 +1,1619 @@ + + + 4 + + rev_d + + ARM + + 1 + + C-SPY + 2 + + 33 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ARMSIM_ID + 2 + + 1 + 1 + 1 + + + + + + + + CADI_ID + 2 + + 0 + 1 + 1 + + + + + + + + + CMSISDAP_ID + 2 + + 4 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + E2_ID + 2 + + 0 + 1 + 1 + + + + + + + + + + GDBSERVER_ID + 2 + + 0 + 1 + 1 + + + + + + + + + + + IJET_ID + 2 + + 9 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + JLINK_ID + 2 + + 16 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LMIFTDI_ID + 2 + + 3 + 1 + 1 + + + + + + + + + + + + + NULINK_ID + 2 + + 0 + 1 + 1 + + + + + + + PEMICRO_ID + 2 + + 3 + 1 + 1 + + + + + + + + STLINK_ID + 2 + + 8 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + THIRDPARTY_ID + 2 + + 0 + 1 + 1 + + + + + + + + TIFET_ID + 2 + + 1 + 1 + 1 + + + + + + + + + + + + + + + + + + + XDS100_ID + 2 + + 9 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $TOOLKIT_DIR$\plugins\rtos\CMX\CmxArmPlugin.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\CMX\CmxTinyArmPlugin.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\embOS\embOSPlugin.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\FreeRtos\FreeRtosArmPlugin.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\Mbed\MbedArmPlugin.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\Mbed\MbedArmPlugin2.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\OpenRTOS\OpenRTOSPlugin.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\SafeRTOS\SafeRTOSPlugin.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\SMX\smxAwareIarArm9.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\SMX\smxAwareIarArm9BE.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\ThreadX\ThreadXArmPlugin.ENU.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\uCOS-II\uCOS-II-286-KA-CSpy.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\uCOS-II\uCOS-II-KA-CSpy.ewplugin + 0 + + + $TOOLKIT_DIR$\plugins\rtos\uCOS-III\uCOS-III-KA-CSpy.ewplugin + 0 + + + $EW_DIR$\common\plugins\Orti\Orti.ENU.ewplugin + 0 + + + $EW_DIR$\common\plugins\TargetAccessServer\TargetAccessServer.ENU.ewplugin + 0 + + + $EW_DIR$\common\plugins\uCProbe\uCProbePlugin.ENU.ewplugin + 0 + + + + diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewp b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewp new file mode 100644 index 0000000..88a9886 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.ewp @@ -0,0 +1,2359 @@ + + + 4 + + rev_d + + ARM + + 1 + + General + 3 + + 36 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ICCARM + 2 + + 38 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AARM + 2 + + 12 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OBJCOPY + 0 + + 1 + 1 + 1 + + + + + + + + + CUSTOM + 3 + + + + 0 + inputOutputBased + + + + ILINK + 0 + + 27 + 1 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + IARCHIVE + 0 + + 0 + 1 + 1 + + + + + + + BUILDACTION + 2 + + + + + hdr + + cmsis + + include + + $PROJ_DIR$\..\..\common\hdr\cmsis\include\cmsis_compiler.h + + + $PROJ_DIR$\..\..\common\hdr\cmsis\include\core_cm0plus.h + + + + + rev_b + + rev_c + rev_d + rtos + + + ip + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_backup_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_canfd.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_cpuss.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_crypto.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_dmac.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_dw.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_efuse_data_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_efuse_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_eth.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_evtgen.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_fault.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_flashc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_flexray.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_gpio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_hsiom_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_i2s_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_ipc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_lin.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_pass.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_peri_ms_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_peri_pclk_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_peri_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_prot.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_scb_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_sdhc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_sflash_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_smartio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_smif_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_srss_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\ip\cyip_tcpwm_v2.h + + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\bb_bsp_tviibh8m.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\cy_device_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_b\cyt4bf_config.h + + + + rev_c + + rev_b + rev_d + rtos + + + ip + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_backup_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_canfd.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_cpuss.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_crypto.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_dmac.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_dw.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_efuse_data_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_efuse_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_eth.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_evtgen.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_fault.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_flashc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_flexray.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_gpio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_hsiom_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_i2s_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_ipc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_lin.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_pass.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_peri_ms_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_peri_pclk_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_peri_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_prot.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_scb_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_sdhc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_sflash_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_smartio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_smif_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_srss_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\ip\cyip_tcpwm_v2.h + + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\bb_bsp_tviibh8m.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\cy_device_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_c\cyt4bf_config.h + + + + rev_d + + rev_b + rev_c + + + ip + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_backup_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_canfd.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_coredefines.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_cpuss.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_crypto.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_dmac.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_dw.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_efuse_data_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_efuse_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_eth.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_evtgen.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_fault.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_flashc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_flexray.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_gpio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_hsiom_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_i2s_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_ipc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_lin.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_pass.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_peri_ms_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_peri_pclk_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_peri_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_prot.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_scb_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_sdhc.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_sflash_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_smartio_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_smif_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_srss_v3.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\ip\cyip_tcpwm_v2.h + + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\bb_bsp_tviibh8m.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\cy_device_headers.h + + + $PROJ_DIR$\..\..\tviibh8m\hdr\rev_d\cyt4bf_config.h + + + + rtos + + rev_b + rev_c + rev_d + + + $PROJ_DIR$\..\..\common\src\rtos\include\atomic.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\croutine.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\deprecated_definitions.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\event_groups.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\FreeRTOS.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\FreeRTOSConfig.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\list.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\message_buffer.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\mpu_prototypes.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\mpu_wrappers.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\portable.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\projdefs.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\queue.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\semphr.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\stack_macros.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\StackMacros.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\stream_buffer.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\task.h + + + $PROJ_DIR$\..\..\common\src\rtos\include\timers.h + + + + $PROJ_DIR$\..\..\common\hdr\cy_project.h + + + + src + + drivers + + adc + + $PROJ_DIR$\..\..\common\src\drivers\adc\cy_adc.c + + + $PROJ_DIR$\..\..\common\src\drivers\adc\cy_adc.h + + + + audioss + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\audioss\cy_i2s.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\audioss\cy_i2s.h + + + + canfd + + $PROJ_DIR$\..\..\common\src\drivers\canfd\cy_canfd.c + + + $PROJ_DIR$\..\..\common\src\drivers\canfd\cy_canfd.h + + + + cpu + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\cpu\cy_cpu.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\cpu\cy_cpu.h + + + + crypto + + $PROJ_DIR$\..\..\common\src\drivers\crypto\cy_crypto.h + + + $PROJ_DIR$\..\..\common\src\drivers\crypto\cy_crypto_common.h + + + $PROJ_DIR$\..\..\common\src\drivers\crypto\cy_crypto_config.c + + + $PROJ_DIR$\..\..\common\src\drivers\crypto\cy_crypto_config.h + + + $PROJ_DIR$\..\..\common\src\drivers\crypto\cy_crypto_server.h + + + + dma + + $PROJ_DIR$\..\..\common\src\drivers\dma\cy_mdma.c + + + $PROJ_DIR$\..\..\common\src\drivers\dma\cy_mdma.h + + + $PROJ_DIR$\..\..\common\src\drivers\dma\cy_pdma.c + + + $PROJ_DIR$\..\..\common\src\drivers\dma\cy_pdma.h + + + + ethernet + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cdn_errno.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cdn_stdint.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cedi.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cps_v2.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cy_ethif.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\cy_ethif.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\edd.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\edd_int.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\edd_rx.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\edd_tx.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\emac_regs.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\emac_regs_macro.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\ethernet\log.h + + + + evtgen + + $PROJ_DIR$\..\..\common\src\drivers\evtgen\cy_evtgen.c + + + $PROJ_DIR$\..\..\common\src\drivers\evtgen\cy_evtgen.h + + + + flash + + $PROJ_DIR$\..\..\common\src\drivers\flash\cy_flash.c + + + $PROJ_DIR$\..\..\common\src\drivers\flash\cy_flash.h + + + + flexray + + api + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_control_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_init.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_interrupt_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_rx_handler.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_status_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_time_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_timer_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\api\cy_fr_api_tx_handler.c + + + + ccal + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_control_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_init.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_interrupt_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_rx_handler.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_status_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_time_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_timer_service.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\flexray\ccal\cy_fr_ccal_tx_handler.c + + + + + gpio + + $PROJ_DIR$\..\..\common\src\drivers\gpio\cy_gpio.c + + + $PROJ_DIR$\..\..\common\src\drivers\gpio\cy_gpio.h + + + + ipc + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_config.h + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_drv.c + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_drv.h + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_pipe.c + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_pipe.h + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_sema.c + + + $PROJ_DIR$\..\..\common\src\drivers\ipc\cy_ipc_sema.h + + + + lin + + $PROJ_DIR$\..\..\common\src\drivers\lin\cy_lin.c + + + $PROJ_DIR$\..\..\common\src\drivers\lin\cy_lin.h + + + + lvd + + $PROJ_DIR$\..\..\common\src\drivers\lvd\cy_lvd.c + + + $PROJ_DIR$\..\..\common\src\drivers\lvd\cy_lvd.h + + + + mcwdt + + $PROJ_DIR$\..\..\common\src\drivers\mcwdt\cy_mcwdt.c + + + $PROJ_DIR$\..\..\common\src\drivers\mcwdt\cy_mcwdt.h + + + + mpu + + $PROJ_DIR$\..\..\common\src\drivers\mpu\cy_mpu.c + + + $PROJ_DIR$\..\..\common\src\drivers\mpu\cy_mpu.h + + + + prot + + $PROJ_DIR$\..\..\common\src\drivers\prot\cy_prot.c + + + $PROJ_DIR$\..\..\common\src\drivers\prot\cy_prot.h + + + + scb + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_common.c + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_common.h + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_ezi2c.c + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_ezi2c.h + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_i2c.c + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_i2c.h + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_spi.c + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_spi.h + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_uart.c + + + $PROJ_DIR$\..\..\common\src\drivers\scb\cy_scb_uart.h + + + + sd_host + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sd_host\cy_sd_host.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sd_host\cy_sd_host.h + + + + smartio + + $PROJ_DIR$\..\..\common\src\drivers\smartio\cy_smartio.c + + + $PROJ_DIR$\..\..\common\src\drivers\smartio\cy_smartio.h + + + + smif + + $PROJ_DIR$\..\..\common\src\drivers\smif\common\cy_smif.c + + + $PROJ_DIR$\..\..\common\src\drivers\smif\common\cy_smif.h + + + $PROJ_DIR$\..\..\common\src\drivers\smif\common\cy_smif_memslot.c + + + $PROJ_DIR$\..\..\common\src\drivers\smif\common\cy_smif_memslot.h + + + $PROJ_DIR$\..\..\common\src\drivers\smif\ver2\cy_smif_ver_specific.c + + + $PROJ_DIR$\..\..\common\src\drivers\smif\ver2\cy_smif_ver_specific.h + + + + srom + + $PROJ_DIR$\..\..\common\src\drivers\srom\cy_srom.c + + + $PROJ_DIR$\..\..\common\src\drivers\srom\cy_srom.h + + + + sysclk + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sysclk\cy_sysclk.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sysclk\cy_sysclk.h + + + + sysflt + + $PROJ_DIR$\..\..\common\src\drivers\sysflt\cy_sysflt.c + + + $PROJ_DIR$\..\..\common\src\drivers\sysflt\cy_sysflt.h + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sysflt\cy_sysflt_config.h + + + + sysint + + $PROJ_DIR$\..\..\common\src\drivers\sysint\cy_sysint.c + + + $PROJ_DIR$\..\..\common\src\drivers\sysint\cy_sysint.h + + + + syslib + + $PROJ_DIR$\..\..\common\src\drivers\syslib\cy_syslib.c + + + $PROJ_DIR$\..\..\common\src\drivers\syslib\cy_syslib.h + + + $PROJ_DIR$\..\..\common\src\drivers\syslib\iar\cy_syslib_iar.s + + + + syspm + + $PROJ_DIR$\..\..\common\src\drivers\syspm\cy_syspm.c + + + $PROJ_DIR$\..\..\common\src\drivers\syspm\cy_syspm.h + + + + sysreghc + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sysreghc\cy_sysreghc.c + + + $PROJ_DIR$\..\..\tviibh8m\src\drivers\sysreghc\cy_sysreghc.h + + + + sysreset + + $PROJ_DIR$\..\..\common\src\drivers\sysreset\cy_sysreset.c + + + $PROJ_DIR$\..\..\common\src\drivers\sysreset\cy_sysreset.h + + + + sysrtc + + $PROJ_DIR$\..\..\common\src\drivers\sysrtc\cy_sysrtc.c + + + $PROJ_DIR$\..\..\common\src\drivers\sysrtc\cy_sysrtc.h + + + + systick + + $PROJ_DIR$\..\..\common\src\drivers\systick\cy_systick.c + + + $PROJ_DIR$\..\..\common\src\drivers\systick\cy_systick.h + + + + syswdt + + $PROJ_DIR$\..\..\common\src\drivers\syswdt\cy_syswdt.c + + + $PROJ_DIR$\..\..\common\src\drivers\syswdt\cy_syswdt.h + + + + tcpwm + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm.c + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm.h + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_counter.c + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_counter.h + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_pwm.c + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_pwm.h + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_quaddec.c + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_quaddec.h + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_sr.c + + + $PROJ_DIR$\..\..\common\src\drivers\tcpwm\cy_tcpwm_sr.h + + + + trigmux + + $PROJ_DIR$\..\..\common\src\drivers\trigmux\cy_trigmux.c + + + $PROJ_DIR$\..\..\common\src\drivers\trigmux\cy_trigmux.h + + + + + interrupts + + rev_b + + rev_c + rev_d + rtos + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_b\cy_interrupt_map.c + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_b\cy_interrupt_map_cm0plus.h + + + + rev_c + + rev_b + rev_d + rtos + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_c\cy_interrupt_map.c + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_c\cy_interrupt_map_cm0plus.h + + + + rev_d + + rev_b + rev_c + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_d\cy_interrupt_map.c + + + $PROJ_DIR$\..\..\tviibh8m\src\interrupts\rev_d\cy_interrupt_map_cm0plus.h + + + + + mw + + aic261 + + $PROJ_DIR$\..\..\common\src\mw\aic261\cy_aic261.c + + + $PROJ_DIR$\..\..\common\src\mw\aic261\cy_aic261.h + + + + button + + $PROJ_DIR$\..\..\common\src\mw\button\cy_button.c + + + $PROJ_DIR$\..\..\common\src\mw\button\cy_button.h + + + + eth_phy + + $PROJ_DIR$\..\..\tviibh8m\src\mw\eth_phy\cy_dp83867.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\eth_phy\cy_dp83867.h + + + + flash + + $PROJ_DIR$\..\..\common\src\mw\flash\cy_mw_flash.c + + + $PROJ_DIR$\..\..\common\src\mw\flash\cy_mw_flash.h + + + + power + + $PROJ_DIR$\..\..\tviibh8m\src\mw\power\cy_power.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\power\cy_power.h + + + + semihosting + + $PROJ_DIR$\..\..\common\src\mw\semihosting\cy_semihosting.c + + + $PROJ_DIR$\..\..\common\src\mw\semihosting\cy_semihosting.h + + + + smif_mem + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_device_common.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_device_common.h + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_hb_flash.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_hb_flash.h + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_s25fl.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_s25fl.h + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_s25fs.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_s25fs.h + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_semp.c + + + $PROJ_DIR$\..\..\tviibh8m\src\mw\smif_mem\cy_smif_semp.h + + + + sw_timer + + $PROJ_DIR$\..\..\common\src\mw\sw_timer\cy_sw_tmr.c + + + $PROJ_DIR$\..\..\common\src\mw\sw_timer\cy_sw_tmr.h + + + + + rtos + + rev_b + rev_c + rev_d + + + mem_mgmt + + $PROJ_DIR$\..\..\common\src\rtos\portable\MemMang\heap_4.c + + + + portable + + $PROJ_DIR$\..\..\common\src\rtos\portable\IAR\ARM_CM7\r0p1\port.c + + + $PROJ_DIR$\..\..\common\src\rtos\portable\IAR\ARM_CM7\r0p1\portasm.s + + + $PROJ_DIR$\..\..\common\src\rtos\portable\IAR\ARM_CM7\r0p1\portmacro.h + + + + $PROJ_DIR$\..\..\common\src\rtos\croutine.c + + + $PROJ_DIR$\..\..\common\src\rtos\event_groups.c + + + $PROJ_DIR$\..\..\common\src\rtos\list.c + + + $PROJ_DIR$\..\..\common\src\rtos\queue.c + + + $PROJ_DIR$\..\..\common\src\rtos\stream_buffer.c + + + $PROJ_DIR$\..\..\common\src\rtos\tasks.c + + + $PROJ_DIR$\..\..\common\src\rtos\timers.c + + + + startup + + $PROJ_DIR$\..\..\common\src\startup\startup.c + + + $PROJ_DIR$\..\..\common\src\startup\iar\startup_cm0plus.s + + + $PROJ_DIR$\..\..\common\src\startup\startup_customize.h + + + + system + + rev_b + + rev_c + rev_d + rtos + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_b\system_cyt4bf.h + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_b\system_tviibh8m_cm0plus.c + + + + rev_c + + rev_b + rev_d + rtos + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_c\system_cyt4bf.h + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_c\system_tviibh8m_cm0plus.c + + + + rev_d + + rev_b + rev_c + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_d\system_cyt4bf.h + + + $PROJ_DIR$\..\..\tviibh8m\src\system\rev_d\system_tviibh8m_cm0plus.c + + + + + $PROJ_DIR$\..\App\main_cm0plus.c + + + + Passenger_Seat_Timed_Controller_demo + + App + + $PROJ_DIR$\..\App\seat_timed_application.c + + + $PROJ_DIR$\..\App\seat_timed_application.h + + + $PROJ_DIR$\..\App\seat_motor_manager.c + + + $PROJ_DIR$\..\App\seat_motor_manager.h + + + $PROJ_DIR$\..\App\safety_manager.c + + + $PROJ_DIR$\..\App\safety_manager.h + + + + Bsp + + $PROJ_DIR$\..\Bsp\bsp_seat_move_can.c + + + $PROJ_DIR$\..\Bsp\bsp_seat_move_can.h + + + $PROJ_DIR$\..\Bsp\bsp_seat_backrest_heater.c + + + $PROJ_DIR$\..\Bsp\bsp_seat_backrest_heater.h + + + $PROJ_DIR$\..\Bsp\bsp_seat_current_adc.c + + + $PROJ_DIR$\..\Bsp\bsp_seat_current_adc.h + + + $PROJ_DIR$\..\Bsp\bsp_seat_fan.c + + + $PROJ_DIR$\..\Bsp\bsp_seat_fan.h + + + + Driver + + $PROJ_DIR$\..\Driver\motor_control.c + + + $PROJ_DIR$\..\Driver\motor_control.h + + + + Config + + $PROJ_DIR$\..\Config\seat_move_config.h + + + $PROJ_DIR$\..\Config\seat_backrest_heater_config.h + + + $PROJ_DIR$\..\Config\seat_fan_config.h + + + $PROJ_DIR$\..\Config\seat_current_adc_config.h + + + + Docs + + $PROJ_DIR$\..\Docs\README_Passenger_Seat_Timed_Controller.md + + + $PROJ_DIR$\..\Docs\BUILD_INFO_2026-09-16.md + + + + IAR + + $PROJ_DIR$\Passenger_Seat_Timed_Controller_demo.mac + + + + diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.mac b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.mac new file mode 100644 index 0000000..8be97b5 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_demo.mac @@ -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"; + } +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_program.mac b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_program.mac new file mode 100644 index 0000000..97c68a9 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/Passenger_Seat_Timed_Controller_program.mac @@ -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"; + } +} diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.hex b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.hex new file mode 100644 index 0000000..e13e7d6 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.hex @@ -0,0 +1,2690 @@ +:020000041000EA +:1000000000000228F59E0010ADA5001025A30010E9 +:1000100000000000000000000000000000000000E0 +:10002000000000000000000000000000E5A3001038 +:100030000000000000000000B1A50010B5A50010F0 +:1000400025990010499900106D99001091990010A0 +:10005000B5990010D9990010FD990010219A00104F +:10006000B9A50010BDA50010C1A50010C5A50010C0 +:10007000C9A50010CDA50010D1A50010D5A5001070 +:1000800000B5010009B2002905D40121C006C00E47 +:100090008140C648016000BD00B5010009B20029D9 +:1000A00005D40121C006C00E8140C148016000BDD9 +:1000B000F0B5020012B2002A18D41822BD4B040079 +:1000C00024B2A408A4001C59FF250600F60016401F +:1000D000B540AC438901C9B20500ED002A4091400A +:1000E000214300B28008800019501CE00F22B24C5E +:1000F000182503001BB21340083B9B089B00060019 +:1001000036B23240083A92089200A258FF26070001 +:10011000FF002F40BE40B2438901C9B2C000054074 +:10012000A9401143E150F0BD0269CA401000C00768 +:10013000C00F704701228A408260704701228A40C6 +:1001400042607047C22189004058C005C00D704709 +:10015000C121890041180A6880235B0213430B60A8 +:10016000C121890041180A68B54B13400B60C121B9 +:10017000890040587047822189004058C005C00D51 +:10018000704782218900415808000004C00F704761 +:1001900038B504002000FFF7EEFF05002000FFF750 +:1001A000F0FF2818002801D1012000E00020C0B293 +:1001B00032BD8121890041180A6880235B02134304 +:1001C0000B608121890041180A689D4B13400B6028 +:1001D00081218900405870470168802212060A4335 +:1001E0000260704780B5FFF7ADFF02BD80B5FFF735 +:1001F000AFFF01BD80B5FFF7CBFF02BD80B5FFF7B4 +:10020000D8FF01BD80B503F090F802BD80B503F0C2 +:100210009EF802BD00B5022801D301200FE00B00BB +:10022000DBB2042B01DB012009E0B34B800018187E +:10023000C9B2405C824201D3012000E0002000BD31 +:10024000F8B504001F00022C01D3012023E01500A3 +:100250000E002A003100C9B22000FFF7DBFF0128A1 +:1002600001D1012017E03000C0B2002816D1FF2FC5 +:1002700012D8CB4861034018A900401880214901D9 +:1002800040180168C74A0A403902FF231B020B408D +:10029000134303600020F2BD0120FCE7F6B2012EFB +:1002A00016D180204002874210D2BD486103401819 +:1002B000A9004018A021490140180168C54A0A4018 +:1002C0003902C54B0B4013430360E3E70120E2E72B +:1002D0000120E0E7F8B504001F00022C01D3012043 +:1002E00036E015000E002A003100C9B22000FFF7E9 +:1002F00091FF012801D101202AE006983100C9B2FE +:10030000022928D1802149028F4222D2202820D2DE +:10031000A34962038A18AB00D218C0235B01D2182C +:100320001668AC4B33403E02AB4F37401F4317605B +:1003300062038918AA008918C022520189180A6824 +:10034000F8239A43C000F823034013430B600020B6 +:10035000F2BD0120FCE7F6B2032E24D1802149042E +:100360008F421ED220281CD28D4962038A18AB000E +:10037000D218E0235B01D2181368DBB23E021E43A1 +:10038000166062038918AA008918E02252018918B0 +:100390000A68F8239A43C000F823034013430B6014 +:1003A000D5E70120D4E70120D2E7000000E100E01A +:1003B00080E200E000E400E01CED00E000B200128A +:1003C000C0B27047F8B505000F001600280000B253 +:1003D000FFF7F4FF04002000C0B2022801D301207F +:1003E0002CE032003900C9B22000C0B2FFF712FF82 +:1003F000012801D1012021E069482100C9B2490347 +:1004000041182A00D2B292008918C02212018A58DB +:10041000C9491140FFB23B02C02292001A400A4370 +:10042000FF218A43F1B21143E4B262038018EDB2B6 +:10043000AA008018C022120181500020F2BD0000E5 +:10044000FFFFFEFF70B50400022C01D3012028E05D +:1004500015000E002A003100C9B22000FFF7DAFEB5 +:10046000012801D101201CE04D4861034158802240 +:1004700012060A43C02189041143FF2212040A43D1 +:10048000AD4B1340F6B23202C0218900114019432E +:10049000FF229143EAB20A4361034250610340588C +:1004A000002070BDDCB506000F0010001900069C8E +:1004B0004901C20E1143400132003B0002F05BFFD4 +:1004C000060091204000FFF779FFF106C90E009168 +:1004D00071090B051B0D5B1E2200D2B20321C0B2B5 +:1004E000FFF7F8FED3BD80B51300A74A13600023C1 +:1004F000137203F006F801BDB8A4001000B5C0B235 +:10050000042801D2012000E00020C0B200BD00B5E7 +:10051000401A00B2012801DB012000E00020C0B237 +:1005200000BD00B5BE490978002901D000201FE0B8 +:1005300082214900415C002901D0002018E0FF2100 +:10054000891D415C002908D1FC214058002801D1B7 +:10055000012000E00020C0B20AE08021490041589B +:10056000FC228058814201D1012000E00020C0B26D +:1005700000BD00B5C0B264214843642103F000F817 +:1005800000BD10B504000800C0B2FFF7F2FF010083 +:10059000A448A2018018406903F0D0F910BD000002 +:1005A00000000440FF00FFFF10B40021B42281507E +:1005B0009D4A1368BC2403511268C023C2500268CC +:1005C000C423C2500A00F023C254F122815410BC4B +:1005D00070470000FF0000FF00FFFF00F1B582B090 +:1005E0001800904B029CA4011C19089B002B14D0EE +:1005F000099B002B11D00A9B002B0ED00092009A71 +:10060000002A0AD0002808D0E26A824205D30A00F4 +:10061000002A02D0616B002901D1002054E00021A2 +:10062000002303F08FF906000F00E06A4008002164 +:1006300080197941E26A002302F09DFE0500300036 +:1006400039007A4A002303F07DF906000F00E06AC2 +:100650000021009A002303F075F902000B000096B8 +:100660000197100019004008CE0730434908009E4A +:10067000019F8019794102F07EFE0600002D0BD00B +:10068000002E09D0BE4802998422520051434018DE +:10069000B8214058B04201D2002015E0606B3018FC +:1006A000401E616B02F06CFF05214843052801D212 +:1006B000052002E0602800D35F2008990D600999A9 +:1006C0000E600A9908700120FEBD80B55549800171 +:1006D0000818016AC069FFF727FD002801D0012032 +:1006E00000E00020C0B202BD10B44F490A68426267 +:1006F000026882624268C2620A68026342684263B8 +:100700000022026713007A24035313007C2403534E +:1007100013008C24035513008D2403550B68C36309 +:10072000436803640968416411009023C154912116 +:10073000425410BC70470000FFFCFFFF00B50200F0 +:100740000B0092B29BB29A4201D3401A00E0081A01 +:1007500080B200BD00B58A49842252005043081877 +:100760008E21415C00290BD08F21415C002907D0EC +:100770002D490968806B081A332801D2012000E056 +:100780000020C0B200BD00002C120028F8B503F014 +:10079000F4F9002801D1002041E0002425002EE0DA +:1007A00084214900764A6846038828004843101887 +:1007B0007426835368460388280048431018762619 +:1007C0008353230028004843101878268353164883 +:1007D0000368280048431018836323002800484317 +:1007E00010188726835501231E002800484310183F +:1007F0008E27C655694350188F2143546D1C042D14 +:1008000008D269462800C0B203F046FA0028C7D1D2 +:10081000002004E004480068934908600120F2BD0C +:100820008B1B00288C9A0010241B0028A086010036 +:10083000FEB58D488D4909680268891A052900D2DC +:100840009DE08A4909680160002452E0D2B2002A82 +:100850003ED02000704328187621405A072141439A +:10086000684600880818C1082000704328187622BE +:1008700081522DE00022684601882000704328182C +:100880007423C1527948016820007043281881639D +:1008900000212000704328188723C15401210B0038 +:1008A0002000704328188E27C35520007043281855 +:1008B0008F23C05C0028C9D168460288200070439D +:1008C00028187623C2522000704328188F228154A2 +:1008D0002000704328187621415A68460088FFF7A7 +:1008E0002DFF6643A91978228852019803F0D4FAA3 +:1008F000641C042C43D269462000C0B203F0CCF93A +:10090000002825D103F0C4FA84225200584B21005C +:10091000514359188725495DFF290AD021005143C9 +:10092000591887254D5D6D1C2100514359188726A4 +:100930008D552100514359188725495D032904D35A +:10094000002162439A188E23D15403F0A5FACFE711 +:1009500003F09EFA019084267600454D20007043F6 +:1009600028187E21405C002884D12000704328187C +:100970009421405C002800D07CE701227BE7F7BD92 +:100980000013002810B53A4A84235B004343D21871 +:100990008223D35C002B35D17E23D35C002B07D17F +:1009A0000B00DBB2052B2DD19423D35C002B29D077 +:1009B0007E23D35C002B04D07E23D35C7F2413558D +:1009C0000BE09523D35C002B04D08123D35C7F24E0 +:1009D000135502E000237F2413558A23D35C972408 +:1009E00013558223D15400218A23D1549423D15406 +:1009F0007E21515C002905D0D34980010818406947 +:100A000003F053FA10BD38B5002429E0CF4B13E0B2 +:100A100084214900164A2000484310188B23C05CEB +:100A20000028F3D020004843101880690028EDD03A +:100A30002000484310188369200048431018962569 +:100A4000405D00280BD0094800686143511849698E +:100A5000401A984203D305212000FFF793FF641C3E +:100A6000042CD5D331BD0000301B0028241B0028E6 +:100A700000130028F8B500273C0061E1B44AB248F1 +:100A8000A1014018016AB048A301C018C06902F072 +:100A900038FD84256D00AF4E20006843375020009C +:100AA0006843301847602000684330188760A648C4 +:100AB000A1014018816A200068433018C1602000FD +:100AC00068433018076120006843301847612000F0 +:100AD000684330188761200068433018C7612000E0 +:100AE000684330180762200068433018876320008D +:100AF00068433018C763200068433018076420003B +:100B00006843301847643900200068433018742265 +:100B10008152390020006843301876228152390012 +:100B20002000684330187822815239002000684341 +:100B300030187E22815439002000684330187F220B +:100B400081543900200068433018802281543900D4 +:100B50002000684330188122815439002000684306 +:100B600030188222815439002000684330188322D3 +:100B700081543900200068433018842281542000B9 +:100B8000FFF7A3FD0100200068433018852281543F +:100B90003900200068433018862281543900200033 +:100BA000684330188722815401212000684330189F +:100BB00088228154390020006843301889228154EA +:100BC00039002000684330188A22815439002000FF +:100BD000684330188B228154390020006843301854 +:100BE0008E22815439002000684330188F228154AE +:100BF00039002000684330189022815439002000C9 +:100C0000684330189122815439002000684330181D +:100C10009222815439002000684330189322815475 +:100C20003900200068433018942281543900200094 +:100C300068433018952281543900200068433018E9 +:100C4000962281543900200068433018972281543D +:100C500039002000684330189822815420006843EE +:100C600030189C214750200068433018A02147507D +:100C7000200068433018A4214750200068433018F2 +:100C8000A8214750200068433018AC21475020006D +:100C900068433018B0214750200068433018B42111 +:100CA00047502948A1014018016B200068433018C3 +:100CB000B82281503221200068433018F22281543A +:100CC000200068433018F322815420006843301814 +:100CD000F42281543900200068433018F6228152F2 +:100CE0003900200068433018F82281522000684300 +:100CF0003018FC21475020006843301880214900FB +:100D00004750390020006843301882225200815435 +:100D10003900200068433018FF22921D81543900A9 +:100D20002000684330188322520081542000684319 +:100D30003018FFF739FC65437019FFF7D5FC641CC8 +:100D4000042C00D29AE6F1BD8C9A0010B8880000FD +:100D5000941200280013002870B50600BF488421B3 +:100D60004900714344183000FFF7AFFC05008820AC +:100D7000205C002805D1852025540120882160545D +:100D80007EE028008521615CC0B2884268D060683E +:100D9000401C60607E20205C002806D19520205CED +:100DA000002802D07348006820622800C0B20028E2 +:100DB00054D02068401C20607E20205C0100C9B215 +:100DC000002905D19521615C002901D08120205C9A +:100DD0000100C9B2012918D18920205C002808D15E +:100DE000A0680021C943884203D0A068401CA060CD +:100DF00014E08920205C002810D0A068E1688842B7 +:100E00000CD2A068401CA06008E0C0B2022805D146 +:100E1000A068002802D0A068401EA0608220205C4C +:100E200000281BD18A20205C002817D07E20205C5F +:100E3000002813D07E20205C012803D1A0682169FE +:100E4000884207D27E20205C022807D12069A16851 +:100E5000884203D301213000FFF794FD85202554FB +:100E60009520205C00280BD042480068216A401A77 +:100E7000322805D3002001009522A154812160541D +:100E800070BD70B575498422520042438C187E2192 +:100E9000615C00296CD08D21615C00292CD1354E1C +:100EA00035689C4980010818806B0021FA22920065 +:100EB000002302F047FD02000B0097480021801834 +:100EC0005941FA22D200002302F055FA616A691AE8 +:100ED00081424DD301208D216054306820636068C9 +:100EE00060630020206701007A22A15201007C2269 +:100EF000A1528C2160543BE07C20205A0A2810D259 +:100F00007A20205A616880B28000201881647C2099 +:100F1000205A401C7C2160526068616B401A206737 +:100F20000FE07A20205A80B280002018806C7A214D +:100F3000615A626889B2890061188A646168081A16 +:100F400020677A20205A401C010089B20A2900D368 +:100F500000207A2160527C20205A0A2808D30548B4 +:100F60000068216B401AFA2802D301208C216054BA +:100F700070BD0000241B002870B50026340019E065 +:100F800030009021685430009121685411E0300005 +:100F9000912168548220285C00280AD1C148006849 +:100FA000E96B401A9749884203D305212000FFF7D7 +:100FB000E9FC641C042C00D387E028488421490004 +:100FC000614345182000FFF75CFF7E20285C002865 +:100FD00007D08220285C002803D18C20285C0028C0 +:100FE00000D1E6E783204000285C00280AD0F320E7 +:100FF000285C002806D130009021685430009121EF +:101000006854D6E78B20285C002814D0300090214B +:101010006854300091216854286F00280AD17E203E +:10102000285C022801D1022100E00321C9B220007E +:10103000FFF7A8FCBDE7286F0328A1D22000FFF727 +:1010400089FBC0B200280BD07820285A3149A20170 +:101050008918898F884203D3012002E000130028F9 +:1010600030009021695C00290BD101219022A95404 +:101070008C490968E9636968296431009122A9549F +:1010800008E069682A6C914204D06968296485493E +:101090000968E963C0B2002800D178E79120285C94 +:1010A000002806D10120912168547E4800686864B8 +:1010B00070E77C480068696C401A642800D269E7D0 +:1010C00004212000FFF75EFC64E770BD38B59C4C3E +:1010D000200003F009F800281BD0200003F009F8D5 +:1010E00070480168491C01600024250003E02800C5 +:1010F000FFF732FE6D1C042DF9D3FFF784FCA448E2 +:101100000168491C01600168192902D30460FFF7D6 +:1011100033FF31BD8C9A0010CF07000070B5282531 +:101120000026340009E0022201219A48A301C018D8 +:101130001823C05EFFF746F9641C042CF3D3022287 +:101140000121FF204230FFF73DF9924C1820205E2C +:10115000FFF734F92B005B1E02220121C0B2FFF71A +:101160006FF81820205EFFF729F902220121C0B292 +:10117000FFF768F9350011E08749A8012018406998 +:1011800002F056FAA8012018406902F0D1FB0021B4 +:10119000A8012018406902F0D3FB6D1C042DEBD38D +:1011A000012420007D4908703E48066078480660AA +:1011B000634E7B49300002F07FFE300002F099FF61 +:1011C000300002F08BFF774D280002F09BFF76493C +:1011D0000020285E02F0B5FF03210220285EFEF702 +:1011E00067FF0220285EFEF757FF0220285EFEF709 +:1011F00047FF300002F06CFF300002F050FEAC48B8 +:10120000047070BDC4090000F8B500220121A9488E +:10121000FFF769F901220221A748FFF764F90022CC +:101220000121A648FFF75FF9A54D01220121280001 +:10123000FFF759F900220021A248FFF754F9A24C08 +:10124000002204212000FFF74EF900220521200092 +:10125000FFF749F9002200212800FFF744F9FFF7C2 +:1012600009FC9A480090082000990860984E1324C1 +:101270003470984FC04A0021380002F042F9062528 +:10128000009805603470BC4A0121380002F039F939 +:10129000009805603470B84A0221380002F031F934 +:1012A000F1BD0000241B002878B58DB06846B34915 +:1012B000342202F059FF3948007800280BD0B0489A +:1012C00002F0F3FDAF4802F0F0FDAF4802F0EDFD93 +:1012D000AE4802F0EAFD7B4E6A460021300002F083 +:1012E00010F9794D6A460421280002F00AF96A468D +:1012F0000521280002F005F9714C6A460021200002 +:1013000002F0FFF800213000FEF718FF042128004A +:10131000FEF714FF05212800FEF710FF0021200032 +:10132000FEF70CFF012000906A460121200002F028 +:10133000E8F801212000FEF7FDFE0EB070BD0000B0 +:10134000000A58401CB591246400012203212000AA +:10135000FFF738F8012000908D4A00238D480021C6 +:10136000FFF7A0F82000FFF729F801220321C0B2FF +:10137000FFF768F8884C200003F058F800228749EE +:10138000200002F019FF2000FEF726FF13BD000029 +:10139000281B00288C9A0010809E0010891B0028B2 +:1013A000B89F0010C0A40010CD100010F2B50600C8 +:1013B00000273C000098002801D1002077E0380089 +:1013C0000099088002F064FDCD490978002903D016 +:1013D00002F062FD00206AE00121C94A117002F0AA +:1013E0005BFD6D4D2800FEF701FF2800FEF706FFAC +:1013F00002213148FEF7A2FE012003F09FF8B6B2A9 +:1014000031002800FEF702FF01283ED1BD4E00E06A +:10141000761E2800FEF7EEFE002801D1002EF7D13F +:101420002800FEF7E7FE00282FD0B64E00E0761E1B +:101430002800FEF7D7FE002801D1002EF7D12800A2 +:10144000FEF7D0FE002820D0012200992800FEF7E8 +:10145000D9FE012819D1C020000200990988014055 +:10146000814201D1012400E03C002000C0B20028EC +:101470000BD0A5480078002807D000980088FC21F0 +:1014800089010140002900D03C00012003F056F8FA +:1014900002210948FEF74EFE012003F04FF802F04A +:1014A000F7FC97490F7002F0F7FC2000C0B2F2BDC4 +:1014B000881B002880013140800F3140800431407A +:1014C0000004314080003140800231406412002825 +:1014D000681200280003314080B5C0B20202FC202F +:1014E00080011040C9B20843694680B2FFF75EFF31 +:1014F00002BD1CB50C00C0B20002FC2189010140F4 +:101500008020C0010843002C05D0694680B2FFF757 +:101510004DFF002801D1002003E0684600882070BC +:10152000012016BD38B505000C002100C9B2280005 +:10153000C0B2FFF7D1FF002801D100200FE06946BB +:101540002800C0B2FFF7D5FF002806D06846007813 +:10155000E4B2A04201D1012000E00020C0B232BDBF +:1015600080B5B44A00211160017C1172C1688068A5 +:1015700001F0C7FF01BD00006012002868A2001042 +:1015800000065840800658400007584080095840DF +:1015900000B4C4040024F40000006440ECA0001077 +:1015A000F8B50126300000246846047056480470DF +:1015B0000121A148FEF7BEFDC24F01213800FEF710 +:1015C000B9FD9E4D280002F0B9FF01219C48FEF7AD +:1015D000B1FD280002F0B2FF02210820FFF7A2FFB0 +:1015E00005002800C0B2002804D000210920FFF720 +:1015F00099FF05002800C0B2002804D000210A206D +:10160000FFF790FF05002800C0B2002804D00F218A +:101610000B20FFF787FF05002800C0B2002804D088 +:10162000A5210C20FFF77EFF05002800C0B200288E +:1016300004D000210D20FFF775FF05002800C0B27F +:10164000002804D000210E20FFF76CFF05002800C1 +:10165000C0B2002808D087210720FFF73DFF050012 +:10166000FA20800002F06AFF2800C0B2002804D0EF +:1016700086210720FFF756FF05002800C0B200288A +:101680000ED069460020FFF734FF002807D06846D7 +:101690000078BF210140802901D1350000E02500FC +:1016A0002800C0B2002810D0A1480470A1480470DE +:1016B000B948047014480670B8480068B849086012 +:1016C00001213800FEF73AFD07E0B34806700E48E6 +:1016D000047001213800FEF72DFD2800C0B2F2BDD4 +:1016E00010B50400534A002010601072E168A06831 +:1016F00001F007FFE168A068FEF720FD10BD0000C3 +:101700008D1B0028A0860100901B0028F8B501243D +:101710002000A149087000252800A949087001216E +:101720006848FEF707FD2E000FE0A6480078002865 +:1017300005D0A548B1014018406902F0B6FBA248A7 +:10174000B1014018FFF7CCFF761C042EEDD302F058 +:101750009FFB0090280044E02A0011E0B849842251 +:101760005200424389188A228A5C002A07D19722B4 +:101770008A5C002A03D198228A5C002AECD02200DD +:101780002B007E268B552B007F268B552B00802629 +:101790008B552B0082268B55072383268B552E00D5 +:1017A0008A27CE552E008B27CE552E009427CE5556 +:1017B0002E009527CE552E009627CE552E00972722 +:1017C000CE552E008927CE55D2B2002A08D08422C9 +:1017D0008B549C228A58A023CA5022009823CA54B2 +:1017E000401C0428BAD3009802F056FBF1BD80B526 +:1017F0007348007800280AD067480078002806D18E +:10180000674866490A680368D21A192A00D20FE0AD +:101810000968016069460020FFF76BFE002805D0CB +:1018200068460078BF210140802901D0FFF76EFF94 +:1018300001BD0000C81200288001314010270000BF +:101840008004314070B5604A80011018524A127805 +:10185000002A01D0002034E02422825C092A14D11D +:10186000334D2A782523C35C9A432523C45C0C405E +:1018700014432100C9B20920FFF754FE06003000CE +:10188000C0B2002815D02C7013E02A4D2A782523E9 +:10189000C35C9A432523C45C0C4014432100C9B2A5 +:1018A0000A20FFF73FFE06003000C0B2002800D03B +:1018B0002C703000C0B2002801D1FFF727FF3000A4 +:1018C000C0B270BD0004314038B504003E48A101EB +:1018D0004518686902F0E9FA2800FFF701FF0021C6 +:1018E0002000FFF7AFFF32BD70B505003648AA01F2 +:1018F0008618C9B2012902D12620345C01E02720D4 +:10190000345C0F20E840C00701D400200FE070696C +:1019100002F0CBFA3000FFF7E3FE2100C9B2280045 +:10192000FFF790FF002801D1002000E0012070BDEA +:10193000861B0028871B002870B504002248A101DF +:1019400045183F488421490061434018F421465C12 +:1019500083214900415C00290AD08B21415C002988 +:1019600006D1F221465C3100C9B2322900D23226BA +:10197000F3214654FEF718FE3100C9B22000FEF7ED +:1019800000FE0021686901F0DBFF686902F087FA58 +:101990002800FFF7E5FD70BD8A1B0028241B0028E6 +:1019A0002C1B002800B50A00D2B2002A07D08122E1 +:1019B0008154AD490968016201219522815400BD1D +:1019C000901B0028891B00288C9A001084228154C7 +:1019D0009C214158A02281500121982281547047B6 +:1019E00000B59C214158491C9C2281509C214158A2 +:1019F000002902D101219C22815000210A00842368 +:101A0000C254A02281509822815400BD00B58B2180 +:101A1000415C002904D08169002901D0816900E07E +:101A200092499622825C002A07D08F4A1268406948 +:101A3000101A884201D3012000E00020C0B200BD8E +:101A40000013002880B500217E2281547F2281541A +:101A50008022815494228154952281548122815480 +:101A6000962281548A228154972281540121832213 +:101A700081540121FFF7AAFF01BDF3B583B07C4873 +:101A8000039984225200514344180020694608708B +:101A900000250020694648700027002069468870AC +:101AA000002602F0F5F901909420205C00284DD02A +:101AB0008220205C002849D16E480078002845D15A +:101AC0002000FEF72EFD002840D06846007CC0B202 +:101AD000002809D064480068E169401A322835D3EB +:101AE0009520205C002831D12000FFF78FFF0028CF +:101AF00004D005210398FEF745FF27E08A20205CEB +:101B0000002808D0A0682169884204D12000FFF78E +:101B100099FF01271AE08A20205C002807D0A068DE +:101B20002169884201D2012003E0022001E08020E7 +:101B3000205C6946087068460078002803D00120C0 +:101B40006946487002E0002069464870019802F03A +:101B5000A3F93800C0B2002803D00398FFF7B4FE01 +:101B600080E068464078002801D100207AE068468D +:101B700001780398FFF7B8FE002801D1002071E03A +:101B800002F086F901909420205C002856D0822033 +:101B9000205C002852D13748007800284ED1200020 +:101BA000FEF7BFFC002849D02000FFF72FFF0028D8 +:101BB00004D005210398FEF7E5FE3FE08A20205C73 +:101BC000002810D0A0682169884204D12000FFF7C6 +:101BD00039FF012709E0A0682169884201D2012567 +:101BE00003E0022501E08020255C3800C0B2002817 +:101BF0001CD1280069460978C0B2884216D16846CF +:101C000000787E216054002080216054942160542B +:101C100095216054812160542000FEF765FD0398F2 +:101C2000FFF78AFE01266846867007E0FFB2002FA4 +:101C300003D180202554012600E00126019802F0FE +:101C40002BF96846807800280AD10A4800780028D5 +:101C500004D10398FFF738FE002801D1002001E0ED +:101C60003000C0B205B0F0BD241B0028B8880000C9 +:101C7000001300288A1B0028F6B583B00D001F0052 +:101C80000400E4B29C490978002916D0C0B2FEF7DE +:101C900035FC002811D09948007800280DD19848CB +:101CA0000078002809D16846057068460078012848 +:101CB00005D068460078022801D001201AE102F020 +:101CC000E7F80190842676008E4D20007043281896 +:101CD0008221405C002806D1200070432818FEF7BE +:101CE00020FC002804D1019802F0D6F8012001E17F +:101CF0002000704328187E21415C0CA80078002841 +:101D000013D10CA801700CA8007800280DD1200078 +:101D1000704328189522805C002806D0200070436C +:101D200028188122805C0CAA10700800C0B200281C +:101D300003D001206A46507002E000206A465070CD +:101D40000CA8007800280CD0C9B2002905D00CA836 +:101D5000007869460978884203D001206946887076 +:101D600002E000206946887068464078002805D067 +:101D7000C548A1014018406902F097F80A990297F6 +:101D80006846027C00272000704328188223C7542D +:101D90002000704328187F23C75420007043281860 +:101DA0008A23C254029B200070432818036120003C +:101DB000704328188B23C1542000704328189721A2 +:101DC0004754D2B2002A08D00BA80078002804D0CB +:101DD000200070432818FFF703FE0D98C0B20028BA +:101DE00006D02000704328189621405C002810D1AE +:101DF000A648016820007043281841610121200095 +:101E00007043281896228154200070432818862198 +:101E100047540BA80078002804D0200070432818ED +:101E2000832147546846807800281ED020007043E4 +:101E300028187E21475468460178200070432818EE +:101E40008022815491480168200070432818C161A4 +:101E50000121200070432818942281540CA8017895 +:101E60006643A819FFF79EFD20E020007043281864 +:101E70007E2147546846017820007043281880224C +:101E8000815482480168200070432818C1610121F3 +:101E90002000704328189422815420007043281891 +:101EA000952147546643A81981214754019801F0B0 +:101EB000F3FF6846407800280CD07348A101401811 +:101EC000FFF70EFC00212000FFF7BCFC002801D129 +:101ED00001200FE068468078002801D0002009E04A +:101EE00000212000FFF7C9FD002801D0380000E0E4 +:101EF0000120C0B205B0F0BD881B00288B1B002854 +:101F00008A1B00280013002880B50121FFF7B5FDCA +:101F100001BDF1B586B05E48069984225200514356 +:101F200044180027002501F0B3FF01908220205CB7 +:101F30006946087068460078002803D1019801F0CE +:101F4000ABFF9EE07F20205C694608748B20205CFC +:101F5000694648709720205C6946887000263000EA +:101F60008221605430007E21605430007F21605413 +:101F700030008021605430009421605430008A2168 +:101F80006054684600788321605430009721605483 +:101F90006846017C2000FFF705FD68468078002830 +:101FA00004D0684601782000FFF710FD68460078ED +:101FB00002280FD101252F0068464078002807D15C +:101FC0009220205C002803D08620205C002830D09E +:101FD00035002EE068460078032810D102276846B5 +:101FE0004078002809D19220205C002805D0862066 +:101FF000205C002801D101251BE0350019E068466E +:102000000078042815D16846007C012801D10227F8 +:1020100000E0012768464078002809D19320205C21 +:10202000002805D08620205C002801D1012500E091 +:1020300035002800C0B2002805D08620205C401C56 +:102040008621605401E096202654019801F024FF77 +:102050000698FFF739FC002813D0EDB2002D10D000 +:10206000012003906846007C02900020019000202F +:102070000090002300223900C9B20698C0B2FFF7D1 +:10208000FBFD07B0F0BD00008C9A0010241B002857 +:1020900000130028F1B584B0B1480499842252009D +:1020A0005143451801F0F4FE0090AE4804688320C7 +:1020B0004000285C002818D07E20285C002814D01E +:1020C0008B20285C002810D19420285C00280CD19B +:1020D0008220285C002808D1A3480078002804D179 +:1020E000C0202858201A322803D2009801F0D4FECC +:1020F00034E128680190F020285CF121695C052911 +:102100000AD2C42169580391BC216958F122AA5C02 +:10211000521CF123EA540CE00100C9B2890069188D +:10212000C822895803910100C9B289006918DC22CC +:102130008958019A0300DBB29B00EB18C8269A511C +:102140000200D2B29200AA18DC23D450401C020034 +:10215000D2B2052A00D30020F022A854C0202C506F +:10216000601A0290BC202858201A0021FA22D200BE +:10217000002301F0E7FBFA229200002301F0FBF8B4 +:102180000200A4202858904201D2A4202A58AC2052 +:102190002858A4216E580021002301F0D3FB02002F +:1021A0000B007008002180185941A422AA5800236E +:1021B00001F0E1F806000198C4216958471ABC20D3 +:1021C0002858201A322812D33000401CB8420ED2B0 +:1021D000F320285C002806D00020F3216854002159 +:1021E0000498FEF7CEF9009801F056FEB6E0BC2048 +:1021F0002858201AFA2802D30298FA2803D2009805 +:1022000001F04AFEAAE002980021FA22D20000233F +:1022100001F098FBFA229200002301F0ACF80400D0 +:10222000002C03D1009801F037FE97E00198039944 +:10223000401A0021AF4A002301F084FB02000B008A +:1022400060080021801859412200002301F093F812 +:102250000190B02028580290F3202C5C0198B42102 +:1022600068503000401CB84201D200245BE07F1C63 +:10227000B74210D22000C0B2322801D2322452E03C +:102280002000C0B25F284ED2641D2000C0B260287A +:1022900049D35F2447E0019800216422002301F024 +:1022A00051FB06000F00029800215A22002301F082 +:1022B00049FB8F4213D801D3864210D22000C0B20E +:1022C000322801D232242EE02000C0B25F282AD268 +:1022D000641D2000C0B2602825D35F2423E002984B +:1022E00000216E22002301F02DFB06000F00019853 +:1022F00000216422002301F025FB8F4213D801D373 +:10230000864210D22000C0B2332807D32000C0B2CA +:10231000372801D3FB3406E0322404E02000C0B2A9 +:10232000002800D000242000F321695CC0B288425C +:1023300011D0F320285C002806D12000C0B232283A +:1023400002D32800FEF7D0F9F3202C542100C9B2A3 +:102350000498FEF716F9009801F09EFD05B0F0BD57 +:1023600000130028241B00288A1B0028FEB5A5485E +:10237000007800285DD001F08BFDC3490978002961 +:1023800002D001F089FD54E001273900BE4A1170E6 +:10239000B8490A786B461A70002422000A7001F0CE +:1023A0007BFD6846007800282BD0250027E026001A +:1023B0001AE0684600783900A94008421ED001F0B2 +:1023C00067FD019084214900B04A28004843101855 +:1023D00082235B00C05C0028E9D069435018FEF7F7 +:1023E000A0F80028E3D13E00019801F055FDF6B2B7 +:1023F000002E03D02800C0B200F059F96D1C042D46 +:10240000D7D3FEF715FAFFF7F2F9250009E0280007 +:10241000FFF77FFD2800FFF777FD2800FFF73AFE62 +:102420006D1C042DF3D301F033FD97490C7001F0BE +:1024300033FDF7BD964800787047F8B50E000700E9 +:10244000FFB270490978002904D0C0B2FEF756F8EF +:10245000002801D1012046E001F01AFD04008B485C +:1024600084214900794345188A480078002804D01F +:10247000200001F011FD012035E0FF20801D285CC7 +:1024800000280CD0310089B2F820285AFEF73FF816 +:10249000002804D0200001F0FFFC012023E082206E +:1024A0004000285C00280FD0F620285A310089B25D +:1024B000884219D100208221490068546D48017872 +:1024C0000122BA4091430170F8202E52FC20285876 +:1024D0008021490068500120FF21891D6854200097 +:1024E00001F0DAFC0020F2BD200001F0D5FC012053 +:1024F000F9E70000A0860100F2B50400E4B241490A +:102500000978002904D0C0B2FDF7F8FF002801D1F6 +:10251000002025E001F0BCFC070084256D005B4E27 +:10252000200068433018FDF7FCFF002812D020007F +:1025300068433018FF21891D405C00280AD0684696 +:1025400000886C433119F822895A80B2814201D146 +:10255000012400E00024380001F09EFC2000C0B2FD +:10256000F2BDF3B581B000262DE000200EE000247E +:1025700082204000385C0028F7D068460189F620A8 +:10258000385AFDF7C4FF0028EFD028002043C0B21E +:10259000002817D168460089F6217852FC20385867 +:1025A000401CFC2178502800822149007854314891 +:1025B0000178B5400D430570AF48B101401840693E +:1025C00001F073FC761C042E1AD2012568460079AE +:1025D0002900B1400842F5D02C48842149007143BC +:1025E0004718FF20801D385C0028C0D0684601894C +:1025F000F820385AFDF78BFF0028B8D02C00B7E739 +:10260000F7BD0000881B0028F1B5002425002EE04E +:10261000842149001D4A6846038828004843101851 +:10262000F6268353280048431018FC23C3585B1C2C +:10263000280048431018FC26835101231E0028005F +:102640004843101882277F00C6552000694351185F +:10265000FF22921D885407480178AB400B4303705A +:102660008548A9014018406901F01FFC6D1C042D2C +:10267000CED3F1BD8E1B002880B50121054A117013 +:1026800080B2FFF7C1FF01BD8C1B0028001300289A +:102690008A1B00288B1B002810B5002404E02000B2 +:1026A000C0B200F004F8641C042CF8D310BDFEB5D1 +:1026B0000400E4B2B9490978002904D0C0B2FDF79A +:1026C0001DFF002801D10120D5E001F0E1FB0190C0 +:1026D00084256D00B24E2000684330187E21415C95 +:1026E0002000684330188322805C6A461070200006 +:1026F000684330188A22805C002806D12000684395 +:1027000030189722805C002803D001206A46507060 +:1027100002E000206A465070C9B2200068433018B9 +:10272000FFF740F9002739002000684330187E2267 +:10273000815439002000684330187F2281543900C9 +:1027400020006843301880228154390020006843FB +:10275000301882228154B3480078002806D007211F +:102760002000684330188322815430E06846407866 +:10277000002806D0062120006843301883228154A7 +:1027800025E068460078022813D0684600780328C0 +:102790000FD06846007804280BD0684600780528DA +:1027A00007D068460078072803D0684600780828D4 +:1027B00007D168460178200068433018832281548D +:1027C00005E0390020006843301883228154390025 +:1027D0002000684330188A22815439002000684361 +:1027E00030188B228154390020006843301894221D +:1027F00081543900200068433018962281543900F2 +:102800002000684330189722815468464078002899 +:1028100005D0062165437019FFF7D8F80FE039009D +:102820002000684330188422815420006843301807 +:10283000A0214750380065437119982288540198A7 +:1028400001F02AFB7748007800280BD00A4DA00140 +:102850002818406901F029FBA0012818FEF740FF65 +:10286000012008E02000FFF72FF8002801D03800F1 +:1028700000E00120C0B2FEBD8C9A0010FEB50D0034 +:102880000400E4B245490978002913D0C0B2FDF72D +:1028900035FE00280ED0644E307800280AD16846F4 +:1028A00005706846007832382E2803D25D4D2878AE +:1028B000002801D001206FE001F0EAFA01903078A1 +:1028C00000280CD12878002809D184256D00344EC9 +:1028D000200068433018FDF724FE002804D1019839 +:1028E00001F0DAFA012057E06846017820006843D9 +:1028F0003018F42281540027390020006843301832 +:10290000832252008154200068433018A42147508C +:10291000200068433018A821475020006843301831 +:10292000AC214750200068433018B021475068461A +:102930000178200068433018F222815468460178FB +:10294000200068433018F322815420006843301877 +:10295000FDF72AFE2000684330187E21405C0028E5 +:102960000ED02000684330188B21405C002807D12E +:10297000654370198221405C002801D10127FFE7DF +:10298000FFB2002F04D0684601782000FDF7F9FD62 +:10299000019801F081FA0020FEBD0000881B00288C +:1029A00000130028F8B500220392002202920122AF +:1029B00001920022009200230022C9B2C0B2FFF7A8 +:1029C0005BF905B000BD38B50400E4B231490978BF +:1029D000002904D0C0B2FDF791FD002801D10020EC +:1029E0001FE001F055FA842252002B4B2100514385 +:1029F00059187E25495D00290CD1210051435918F1 +:102A00009425495D002905D1544319199522895C03 +:102A1000002901D0012400E0002401F03DFA20004B +:102A2000C0B232BD8A1B00288B1B002870B50C0079 +:102A30000500EDB2002C08D016490978002904D011 +:102A4000C0B2FDF75BFD002801D1012020E001F0BC +:102A50001FFA84225200104B2900514359188968EB +:102A60002160290051435918C96861602900514308 +:102A70005918B8268959A160290051435958E16075 +:102A8000554359198922895C217401F005FA002007 +:102A900070BD0000881B002800130028F8B50D0049 +:102AA0000400E4B275490978002918D0C0B2FDF7D6 +:102AB00025FD002813D07248007800280FD1BF48A8 +:102AC000007800280BD100950098002807D02848EE +:102AD000A1014018006B40000099884201D20120FA +:102AE00036E001F0D5F9070084256D00B54E2000D1 +:102AF000684330187E21405C00281BD120006843C9 +:102B000030189421405C002814D12000684330180C +:102B10009521405C00280DD12000684330188221A7 +:102B2000405C002806D1200068433018FDF7F9FC0E +:102B3000002804D1380001F0AFF9012008E0009826 +:102B40006C433119B8228850380001F0A5F90020F3 +:102B5000F2BD00B59C4800680021FA22D200002393 +:102B600000F0F0FEFA229200002300F004FC00BD09 +:102B70008C9A0010F8B505000C002E00F6B2002C5F +:102B800009D03E480078002805D02800C0B2FDF7E3 +:102B9000B5FC002801D101206CE001F079F90700B3 +:102BA0008948016884225200864B3000504318182F +:102BB000806B091A25703000504318188E25405D2F +:102BC000002803D0332901D2012000E000206070EA +:102BD0003000504318188F25405DA07030005043DE +:102BE00018187425405BE080300050431818762593 +:102BF000405B20813000504318187825405B60818D +:102C00006F48B5014019808FA0816078002808D0F6 +:102C1000A078002805D06089A589A84201D30120A9 +:102C200000E00020E0703000504318188C25405D13 +:102C3000002807D0300050431818006F032801D235 +:102C4000012000E000202071564398198B22805CFF +:102C5000607108000021FA22D200002300F072FE09 +:102C6000FA229200002300F086FB2061380001F078 +:102C700013F90020F2BDA84908707047881B00288E +:102C80008B1B0028FEB505000C002E00F6B2002CB0 +:102C900009D0A2480078002805D02800C0B2FDF76E +:102CA0002DFC002801D1012086E001F0F1F801900F +:102CB00084214900434A3000484310188768300097 +:102CC00048431018C068009030004843101889230A +:102CD000C05C60703000484310187E23C05CA07058 +:102CE000714350188321405CE070019801F0D4F8E2 +:102CF0000098002801D101205EE001970098019919 +:102D0000884201D2009801902D48B1014018C26A52 +:102D1000257067600098A06022610198002100235F +:102D200000F010FE02000B000098400800218018FF +:102D30005941009A002300F01EFBE0600198002139 +:102D4000774A002300F0FEFD02000B0000984008C7 +:102D5000002180185941009A002300F00CFB6062AA +:102D60000026A661E6612800C0B2002805D1E0680F +:102D70006061E068A06126621DE0EDB2012D17D10F +:102D8000E068606101980021664A002300F0DAFDE6 +:102D900002000B0000984008002180185941009A59 +:102DA000002300F0E8FAA061E068E061266202E03A +:102DB0006661E06820620020FEBD00008A1B0028DA +:102DC0008C9A001000130028241B0028F8B588B046 +:102DD0000D0016000400E4B25049097800290CD017 +:102DE000C0B2FDF78BFB002807D077480078002899 +:102DF00003D176480078002801D001208AE00696A9 +:102E00000795FFF7B3FA01F043F8842676006D4D7D +:102E1000210071436918CF6801F03EF803A80290C1 +:102E200004A8019005A80090069B079A390020008D +:102E3000FDF7D4FB002801D101206BE001F028F858 +:102E400007006148007800280AD16048007800280F +:102E500006D1200070432818FDF763FB002804D139 +:102E6000380001F019F8012054E007992000704360 +:102E70002818A42281500699200070432818A822FF +:102E800081500599200070432818AC228150049984 +:102E9000200070432818B02281506846017B200032 +:102EA00070432818F22281546846017B2000704349 +:102EB0002818F3228154012120007043281883220E +:102EC00052008154200070432818FDF76DFB20004C +:102ED000704328187E21405C00280ED020007043EB +:102EE00028188B21405C002807D16643A81982214D +:102EF000405C002801D1012000E00020C0B2002881 +:102F000004D06846017B2000FDF73BFB380000F051 +:102F1000C3FF002009B0F0BD8F1B0028881B0028CC +:102F200010270000EC13000070B505000C002E0007 +:102F3000F6B2002C09D026480078002805D02800D9 +:102F4000C0B2FDF7DBFA002801D1012038E000F023 +:102F50009FFF257084225200B84B3100514359180D +:102F600083256D00495D6170310051435918A425D6 +:102F700049596160310051435918A8254959A16048 +:102F8000310051435918AC254959E1603100514392 +:102F90005918B02549592161310051435918B425B8 +:102FA00049596161310051435918F225495D217633 +:102FB00056439919F322895C617600F06DFF002079 +:102FC00070BD0000001300288B1B00288A1B0028FE +:102FD000881B0028F1B586B00C006846007E019081 +:102FE0000026300030006946087430006946887059 +:102FF00030006946487092480078002809D0684639 +:10300000007EFDF77BFA002803D08E4D287800283B +:1030100001D0012010E10394FFF7A8F98A48019933 +:1030200089014018C76A0398874201D2012003E151 +:1030300000F02EFF029081480199842252005143F2 +:1030400044188920205C002812D0E06800280FD0A6 +:103050008B20205C00280BD18220205C002807D127 +:103060002000FDF75EFA002802D02878002804D05E +:10307000029800F011FF0120DEE003980021E268D1 +:10308000002300F05FFC02000B007808002180188C +:1030900059413A00002300F06EF907007E20205CC1 +:1030A000694608702000FEF79BFC276101258A20F5 +:1030B000255430008B21605430009721605430003B +:1030C000832160549420205C002819D0A068874296 +:1030D00001D1300005E0A068B84201D2280000E02C +:1030E0000220802160549620205C00287DD1574822 +:1030F000006860619620255430008621605474E099 +:1031000068460078002839D068460078012802D146 +:10311000A068B84206D368460078022804D1A068A7 +:10312000874201D2280000E030009621615C00292E +:1031300007D14649096861619621655431008622AC +:10314000A154C0B2002850D13F48019989014018CC +:10315000406900F0AAFE30007E21605430008021DA +:1031600060543A480068E06194202554684601782C +:103170002000FEF717FC6846857036E09520205C3D +:1031800000281DD0A068874201D1300005E0A0686A +:10319000B84201D2280000E0022080216054206A59 +:1031A000E061942025549620205C00281DD12748FA +:1031B000006860619620255430008621605414E038 +:1031C000A068874206D13000862160542000FEF7B7 +:1031D00039FC0AE0A068B84202D26846057402E0F1 +:1031E00002206946087468464570029800F054FE53 +:1031F00068468078002809D00198FEF765FB002812 +:1032000001D0300000E02800C0B215E068464078E8 +:10321000002810D000200390002002900020019090 +:10322000002000903B0001226846017C6846007E39 +:10323000FEF722FD00E0002007B0F0BD00130028DB +:10324000881B00288A1B00288C9A0010241B002849 +:1032500038B500200E490870FDF7D6FFFEF772F86A +:10326000FEF79EF9050000F088FC0400FDF756FF0C +:10327000EDB2002D06D0E4B2002C03D0FDF786FAA3 +:10328000002803D1FEF742FA012000E0002032BD01 +:103290008B1B0028D021890040587047C22189002B +:1032A0004058C005C00D7047902292008150704771 +:1032B000822189004058C005C00D704700B50068E4 +:1032C000800B8007800F002801D1802000E0402083 +:1032D00000BD00B5C02189004058C006C00E0828B6 +:1032E00001D2012000E00020C0B200BDF8B504000A +:1032F0000D0016002000FFF7ECFF002809D02F007A +:103300000025B54210D22000FFF7C4FF78556D1C90 +:10331000F7E72F00002505E02000FFF7BBFF69005D +:1033200078526D1CB542F7D3F1BDF8B507000D001A +:1033300016003800FFF7B2FF0400A64200D23400A6 +:10334000290022003800FFF7D1FF2000F2BDF8B5B8 +:1033500005000C0000262800FFF7B0FF070028003A +:10336000FFF7A6FF874204D021002800FFF79CFF4B +:1033700001263000F2BD12422DD0994231D970B5EC +:1033800000251B4209D00C00002112E0110000F0C2 +:10339000F7F80A000021002370BD0942F6D000248E +:1033A000012649186441944200D3A41A7641F8D307 +:1033B0003100012601E0764109D2001864416D41D7 +:1033C000A41A9D41F7D2A4185D41B619F5D3300077 +:1033D00022002B0070BD1B42CFD108B501F0E8F8E8 +:1033E00008BD01D19042CAD202000B00002100208A +:1033F0007047000070B55F4BC018C00900015E4BFC +:10340000C31804290CD21F251C68CE002800B04028 +:103410008443D2B21540C800854025431D600CE0AE +:10342000FC311F255C68CE002800B0408443D2B236 +:103430001540C800854025435D6070BD00B5002A79 +:1034400003D101228A40426002E001228A40826068 +:1034500000BD30B403008C000F25596C2800A0403B +:1034600081431540A5400D435D6430BC704730B4C6 +:10347000030001259C6C28008840844315408D4042 +:1034800025439D6430BC704730B403000125DC6CDB +:1034900028008840844315408D402543DD6430BCBE +:1034A000704730B403004C0010340325D96C280059 +:1034B000A04081431540A5400D43DD6430BC7047FA +:1034C000426901228A4042614069704730B403007A +:1034D00001259C6928008840844315408D40254380 +:1034E0009D6130BC704730B403004C000325196C5B +:1034F0002800A04081431540A5400D431D6430BC09 +:103500007047F8B504000025002C30D01600002EBE +:103510002DD00F00326839002000FFF78FFF327A7C +:1035200039002000FFF766FF726939002000FFF7BD +:103530009EFFB26939002000FFF7A6FFF26939004B +:103540002000FFF7AEFF726839002000FFF781FF0F +:10355000F26839002000FFF7C6FF39002000FFF7AE +:10356000AFFF326939002000FFF7B0FF00E0034DE4 +:103570002800F2BD0000CFBF0000304001005A001B +:103580000022030A8B420BD203098B4219D2430853 +:103590008B422ED2411A00D201465241104670474A +:1035A000FF2209023FD012068B4205D312120902F4 +:1035B0008B4201D31212090203098B4219D300E096 +:1035C000090AC3098B4201D3CB01C01A52418309B6 +:1035D0008B4201D38B01C01A524143098B4201D364 +:1035E0004B01C01A524103098B4201D30B01C01A8F +:1035F0005241C3088B4201D3CB00C01A5241830809 +:103600008B4201D38B00C01A524143088B4201D335 +:103610004B00C01A5241884200D3401A5241CFD2C7 +:1036200001461046704708B500F0C4FF08BD000011 +:10363000F8B501231A00002800D151E1002900D17A +:103640004EE1A84A12680C7AA74D1540A4048022C6 +:10365000D20214402C430322CE681640A34D2540CD +:103660003604C024A40234402C430D786E07760F34 +:103670009F4D25403606E024E40434402C438E78E8 +:103680009C4D2540B4078026F60534402C43282560 +:103690004D5D0126B4431D40254334240C5D0226B4 +:1036A000B5436400022634402C430D7F0426B44306 +:1036B000AD000426354025434C7F0826B543E40081 +:1036C000082634402C4340254E5D1E408A4D25403F +:1036D000B4028026F60034402C4340254D5DEDB207 +:1036E0006D081E002E40F6B2844D254074028026DF +:1036F000B60034402C4370254E5D814D2540340288 +:103700008026760034402C4371254E5D16407D4D59 +:1037100025403603C024A40134402C4304600C7AB5 +:10372000002C02D10023836006E00C7A012C02D128 +:103730004B69836000E083600B6A03614B6A4361FD +:10374000CB6A83610B6BC3614B6903628B69436214 +:103750006D4B1C684423CB5CDBB2FF25AC43234399 +:103760004C240C5DE4B2694D1D402404FF231B046E +:1037700023402B435C240C5DE4B21D022D0A23067A +:103780002B436C240C5DE4B2614D1D402402FF23E9 +:103790001B0223402B4343645E4B1B6864240C5D77 +:1037A000E4B25B4D1D402402FF231B0223402B4348 +:1037B00054240C5DE4B2FF25AB431C438464564B98 +:1037C0001C684123CB5C13400325AC432343602496 +:1037D0000C5D1440514E1E40A302C0252D011D401A +:1037E00035434823CE5C16403023DB4330241D4054 +:1037F000370127402F4350254D5D1540484E3E4030 +:103800002F02C025AD003D40354358268E5D164041 +:10381000C027BD43B701C0263E402E4368254D5DFD +:1038200015400C27BE43AF000C253D403543C56411 +:103830003C4D2D6807267227CF5D3740009707273C +:10384000BD432F00009D3D437327CF5D3E40702751 +:10385000BD43370170263E402E43466535254D5DFC +:103860008567314D2D680E7C16400327BD432E43DE +:10387000CD7C1540C027BE43AF01C0253D40354338 +:103880004E7C16400C27BD43B7000C263E402E430D +:103890008D7C2A403340120114401C430465234AA6 +:1038A0001268CB8894B21A0422438B889BB21B0AFD +:1038B0009BB2174C14401B02FF2212021A402243F3 +:1038C0000B78052B05D10979C9B2FF239A4311431F +:1038D00004E04978FF239A43C9B2114301630022EF +:1038E0001000F2BD8CA50010FFFFFBFFFFFFFCFFE7 +:1038F000FFFFFFF8FFFFFFBFFFFBFFFFFFFDFFFF25 +:10390000FFFEFFFFFFCFFFFF90A50010FFFF00FFAE +:10391000FF00FFFF94A5001098A50010FFF3FFFF24 +:10392000FFFCFFFF9CA50010A0A50010A4A500109F +:103930000168802212060A430260704701617047E5 +:103940008160704730B4434351435D18010C0C0053 +:10395000130C5C432D1980B2434392B251430024AF +:10396000C918644124040B04090C09195043C018F8 +:10397000694130BC7047000000B5022801D3012026 +:103980000FE00B00DBB2042B01DB012009E0BE4B92 +:1039900080001818C9B2405C824201D3012000E0C7 +:1039A000002000BDF8B504001F00022C01D3012047 +:1039B00023E015000E002A003100C9B22000FFF7F5 +:1039C000DBFF012801D1012017E03000C0B2002840 +:1039D00016D1FF2F12D8AD4861034018A900401836 +:1039E0008021490140180168A94A0A403902FF2391 +:1039F0001B020B40134303600020F2BD0120FCE7D3 +:103A0000F6B2012E16D180204002874210D29F4884 +:103A100061034018A9004018A0214901401801681D +:103A20009C4A0A403902C94B0B4013430360E3E749 +:103A30000120E2E70120E0E700B20012C0B27047C7 +:103A4000F8B505000F001600280000B2FFF7F4FFDC +:103A500004002000C0B2022801D301202CE0320073 +:103A60003900C9B22000C0B2FFF786FF012801D19A +:103A7000012021E085482100C9B2490341182A00EC +:103A8000D2B292008918C02212018A58B04911405E +:103A9000FFB23B02C02292001A400A43FF218A4330 +:103AA000F1B21143E4B262038018EDB2AA008018AB +:103AB000C022120181500020F2BD70B50400022C1A +:103AC00001D3012028E015000E002A003100C9B200 +:103AD0002000FFF751FF012801D101201CE06B48B5 +:103AE00061034158802212060A43C0218904114310 +:103AF000FF2212040A43964B1340F6B23202C02151 +:103B0000890011401943FF229143EAB20A4361033D +:103B1000425061034058002070BD70B50400022C73 +:103B200001D3012028E015000E002A003100C9B29F +:103B30002000FFF721FF012801D101201CE053489C +:103B4000610341588022D2050A43C02189041143F0 +:103B5000FF2212040A437E4B1340F6B23202C02108 +:103B6000890011401943FF229143EAB20A436103DD +:103B7000425061034058002070BDF8B598B00BA8C2 +:103B8000342100F029FD9A4803C801AA03C2083A6B +:103B900003A8A549202200F0E7FAAA4800780028E7 +:103BA00001D001206DE000252C0007E02800A64987 +:103BB000620088522800A5490855641C062C14D2BE +:103BC000A348142161434118097A0BAA11720BAA68 +:103BD000142161434118496814236343C058FFF717 +:103BE00090FC0028E2D000204BE0684600F0B2F8DC +:103BF000002801D1002044E0009800219622920084 +:103C00000023FFF79FFE934A002312184B41100038 +:103C10001900914A0023FFF7AEFB002800D10120D4 +:103C200003AFB8818D4E300000F054FD01A9300083 +:103C300000F0ECFC002801D0002022E02C0006E07F +:103C4000142060432818C06800F042FE641C062C53 +:103C500013D27F4D142060432818007C787103A98B +:103C6000142060432818C06800F038FD0028E7D011 +:103C7000300000F02FFD002003E0012071490870A2 +:103C8000012019B0F0BD0000B8A4001000000440ED +:103C9000FF00FFFFFF0000FFF8B584B00E00C0B2C8 +:103CA0000290002528006946088004212A0003AC00 +:103CB000200000F081FC2C0062480078002806D02B +:103CC000674F3878002802D10298062801D30020D7 +:103CD00039E0012038705E4802991422514340189F +:103CE000C0680190019800F0A1F8019800F0FCFD77 +:103CF000019800F07FF800281FD003AA69460198B8 +:103D000000F0F8FD002818D103A8C078002814D0CE +:103D1000684600880005000D6946088068460088EE +:103D20004949029A52008852012448480299445451 +:103D3000002E02D0684600883080019800F076F8A6 +:103D40003D702000C0B205B0F0BD000000FFFF00D4 +:103D5000FFFCFFFFF8B5070000200090FF262C367F +:103D60003000FFF769FE0500002F04D0684601F01F +:103D7000D1FA002801D0002038E0009839494018D5 +:103D80003949FFF7FDFB0400002C02D0374884427C +:103D900001D300202AE0042201212800FFF7BDFE04 +:103DA000002801D0002021E0042201213000FFF78B +:103DB00047FE002801D0002018E0631E04220121E4 +:103DC0002800FFF7EFFD002801D000200EE00422BC +:103DD00001212800FFF771FE002801D0002005E036 +:103DE00000982100FFF7CCFB38600120F2BD0000F5 +:103DF000A0A400107CB505001D4C062100226E46D3 +:103E0000300000F0D9FB6946280000F05FFE6846EC +:103E10000078002803D1641E002CF4D101E00120B9 +:103E200000E0002076BD0000E4A20010E0B51149DA +:103E30000CC9083969460CC10839694600F0F0FD23 +:103E400007BD0000801B0028D01A0028E81A0028AF +:103E5000089E0010FFC99A3B00CA9A3B0010904090 +:103E6000811B00285F8DCB00608DCB00010001001D +:103E7000A0860100A8A400100000000000000000BF +:103E8000801C800803D0401C801EFCD100BF7047FE +:103E9000EFF3108072B6704780F31088704740309F +:103EA000016810220A43026070474030016808220E +:103EB0000A43026070470000F8B5012422004C2339 +:103EC000CB5C052B03D24D23CB5C052B01D310001B +:103ED000E2E0002800D1DEE0002900D1DBE06F4AFB +:103EE00012684B7923406E4D15409B048022D2020C +:103EF00013402B4303228D7915406A4E1E402B043C +:103F0000C025AD021D40354307230E7A1E40664F83 +:103F10002F403606E025ED0435403D43CE79634F12 +:103F20002F40B5078026F60535403D430E7D01271D +:103F3000BD4326402E4324254D5D0227BE436D0020 +:103F400002273D40354305600D7906003036009666 +:103F5000009E3668FF27BE43EDB23543009E3560B4 +:103F60004D79012D0BD18C79002C02D10024846075 +:103F700006E08C79012C03D10C68846000E0846039 +:103F8000CC6804610C694461CC6984610C6AC461C9 +:103F90000C680462464C25680C7EE4B2FF26B543EB +:103FA0002C4330254D5DEEB2424D25403604FF24B2 +:103FB000240434402C4340254D5DEDB22402240AF4 +:103FC0002E06264348240C5DE7B2FF2424023A4D16 +:103FD0002E403F02274037434764384E3668382723 +:103FE000CF5DFFB20097FF27BE43009F37432826CF +:103FF0008E5DF6B23D40360234402C438464304C32 +:1040000025688C7D14400326B5432C4325254E5D41 +:1040100016402C4D2540B602C024240134402C43C8 +:104020002C254D5D15403026B4432E0130253540FA +:10403000254334240C5D1440234E2E402502C02419 +:10404000A4002C4034433C254D5D1540C026B443AC +:10405000AE01C0253540254344240C5D22400C248C +:10406000A54394000C2222402A43C264174A1468D4 +:104070004C228A5C1A400725AC4322434D240C5D38 +:1040800023407024A2431C01702323401343436543 +:10409000497D816700221000F2BD000078A5001064 +:1040A000FFFFFBFFFFFFFCFFFFFFFFF8FFFFFFBF6E +:1040B0007CA50010FFFF00FFFF00FFFF80A50010A0 +:1040C00084A50010FFF3FFFFFFFCFFFF88A5001091 +:1040D0000168802212060A43026070477830016846 +:1040E00001220A4302607047C16F0800C007C00F79 +:1040F0007047010070310A68012313430B60006FA1 +:104100007047000030B5010000221000134B1B68FF +:10411000002914D08C786407640F0725AB431C4337 +:104120000B79002B01D00122FFE763005B08D20767 +:104130001A430B4B0024095F89005A5000E00648DF +:1041400030BD00B5074A1368002B03D0126800B2D7 +:104150008000115000BD00000100560074A5001041 +:10416000008020402412002870B4103A03D378C98C +:10417000103A78C0FBD2530701D318C918C001D533 +:1041800008C908C0D20704D30B880380891C801C8F +:10419000002A01D50B78037070BC70478B0701D1E2 +:1041A0008307E1D0124205D00B78491C0370401CF4 +:1041B000521EF9D170470000F7B5AD4BAD4A1568F6 +:1041C000AD4A1668AD4A12680192AD4A1268009273 +:1041D000002422002200002800D11EE10A00002A4B +:1041E00000D11AE15168002901D1936805E05168B6 +:1041F000022901D1230000E0022311695769B94265 +:1042000001D3116900E05169092909D29D4929406A +:104210002125555D002D15D080256D000D4313E03F +:10422000112905D2974F2F408021C9013943EFE76B +:10423000212905D2934F2F40802109023943E7E716 +:104240008B48EBE0904D0D40D168491E0907090FDE +:104250000F27BD4329438D4D0D408021490429433B +:104260000160117F002901D0210000E0012101251A +:104270000127BE43294031430E00516809092940F6 +:104280000227BE434900022739403143D67E10271A +:10429000B943360110273E400E43517E2027BE43CE +:1042A00049012027394031431668012E00D02500EE +:1042B0004E007608E90731430425A9435D00042632 +:1042C00035400D4308218D43D90008231940294367 +:1042D000537F6D4D0D40190280235B001940294327 +:1042E000957F80239B006A4E0E406902194031433E +:1042F000D67F684D0D40B1028026F600314029433B +:104300002025565D644D2940F60280273F013E403E +:104310000E4311798F07BF0F5C4931403F06C0261D +:10432000B6043E400E430662167E009F5649394051 +:10433000360280277F003E400E43977E544931402D +:104340007E021E400E431169491EC906C90E1F2771 +:10435000BE433143C026B60081512421515CC9B20D +:104360000091C121890041180E68FF27BE43009FBC +:1043700037430F60167E019F43493940360280273C +:104380007F003E400E435169491EC906C90E1F27D2 +:10439000BE4331438026B60081512C21515CC9B205 +:1043A00000918121890041180E68FF27BE43009FBC +:1043B00037430F60116B1B4E8151916A184E81512A +:1043C0003821515CF026360186190096009E376828 +:1043D0002F4E3E4049020B40334300990B60392178 +:1043E000515CF4221201821813681D40C902802317 +:1043F0001B011940294311600B49445002980028C1 +:104400000AD002980460029884610298C4600298FD +:10441000C46126480299086223001800FEBD00000E +:10442000C80F0000880F0000C80E00001B49016083 +:10443000C02189040162FF220832C0218900425054 +:104440000021C1239B00C15080239B00C2508122C8 +:1044500092008150124A8150154A8150114A815070 +:10446000114A8150134A8150134A8150704700000D +:104470000140AA0064A5001068A500106CA50010FA +:1044800070A50010FF3FFFFFFFFEFFFFFFFFFFFCD7 +:10449000FFFDFFFFFFFBFFFFFFF7FFFF0F00000324 +:1044A000C80E0000C80F0000880F0000EFCDAB0061 +:1044B000880E0000080F0000480F0000F8B5040047 +:1044C0000D00334F387800280ED02D4E18222D497C +:1044D000300000F069FF0020F075CF2040002A492D +:1044E0000860002000F0C0FF2000290001223A707F +:1044F00080225204002399420AD301D8904207D364 +:10450000244A04000D0024A003C80019694102E0F8 +:104510000200002000211D4B00241C60204D2A6059 +:10452000214A146005221A601A68D203FCD51C6067 +:104530000029DDD10028DBD13C70F1BD70B505004C +:10454000184E306800280ED10D4C18220D4920005D +:1045500000F02AFF0020E075E52040000A490860CD +:10456000002000F081FF2C0030681049FFF708F8A8 +:10457000002122000023FFF7E5F9FFF79FFF70BD40 +:104580005C1A0028A0A000101C1B002810E000E00E +:10459000851B0028FFFFFF00010000FFFFFFFFFF5A +:1045A00014E000E0681B002818E000E040420F0023 +:1045B00070470000704700001206130A1A43130CDC +:1045C0001A4300BF094205D0830707D00270401C80 +:1045D000491EF9D1704700000022FFE7130010398F +:1045E00006D330B41400150010393CC0FCD230BCE6 +:1045F000490700D30CC000D504C0890002D3028053 +:10460000801C002900D5027070470000F8B5020038 +:1046100000231800A94C2468002959D000920A7878 +:104620001207120F009D10352E680F27BE4332432C +:104630002A604A78FF25AC43D2B222438C78002C02 +:1046400001D0012400E01C009D4D15402402802271 +:10465000520014402C430122CE781640994D25403B +:1046600074028026B60034402C430D79002D01D011 +:10467000160000E01E00944D2540B4028026F6008E +:1046800034402C434D79002D01D0160000E01E006F +:104690008E4D254074078026B60534402C438D7915 +:1046A000002D01D0160000E01E00A14D2540B407EA +:1046B0008026F60534402C43C979002901D0130027 +:1046C000FFE762005208D9071143009A116000E029 +:1046D0009848F2BD0021016001617047F0B585B0D6 +:1046E0000C000025019594490E68944909680491CD +:1046F00093490968039193490F68934909680291A6 +:10470000002C00D1D4E00090009800F0E7F8029966 +:104710008020C000084380218900014380204000A0 +:1047200008430222024301210A4304201043009A55 +:10473000106120784007400F07229643304362788B +:104740005207520F702398431201702616400643F9 +:104750000322A07810407D4B33400602C020800029 +:1047600030401843E378002B01D00B0000E02B0011 +:10477000774E0640D80280231B011840304323792E +:10478000002B01D00B0000E02B0046007608D80774 +:104790003043009B186060798006800E049B3F26A2 +:1047A000B3431843A3791340C026B0439B01C026EE +:1047B0001E400643E07943075B0F664830401E0207 +:1047C000E023DB0033400343207A002801D00D00B2 +:1047D000FFE75F4E1E40E80280231B011840304374 +:1047E000657A1540874B03402D03C0208001284087 +:1047F0001843A57A1540844B0340AD03C020000246 +:1048000028401843A3891D052D0D804B03402D041E +:104810007F4828401843A37B0B4045006D08D8070C +:104820002843009B5860E07B4007400F039B07250F +:10483000AB431843237C0B404025A8439B014025F4 +:104840002B400343607C014080208343C8018021CA +:1048500008401843A17C6F4B03400902FF2000026F +:1048600008401843E17CC906C90E6B4B034009049C +:10487000F820400308401843217D0A406749014061 +:104880009205C02000041040084300998860E08A27 +:10489000390C090480B20843218B82B2080410430A +:1048A0000099C86021001A31009800F055F801E025 +:1048B00020480190019805B0F0BD0000D4A400107C +:1048C000FFFEFFFFFFFDFFFFFFFBFFFFFFFFFFDF1F +:1048D0003830016801220A430260704738300168AD +:1048E00001229143016070473C30016801220A4374 +:1048F0000260704710B503000020002916D0002A7E +:1049000014D05B6A1C000C80190F0C00E407E40F44 +:104910001470990F0C00E407E40F9470590F0C0009 +:10492000E407E40F5470D90FD17000E0014810BDC6 +:10493000FFFFFFBF0100DA00D8A40010DCA40010C4 +:10494000E0A40010E4A40010E8A40010FFFCFFFFA6 +:10495000FFF7FFFFFFF8FFFFF0B50A000023190083 +:104960006F4C2468002A49D05579002D01D00126CA +:1049700000E01E006B4D2540B4028026F600344056 +:104980002C431579002D01D0012600E01E00664D54 +:10499000254074028026B60034402C43D578002D83 +:1049A00001D0012600E01E00604D25403402802623 +:1049B000760034402C435578002D01D0012500E0CD +:1049C0001D000226B4436D000226354025431478AD +:1049D000002C01D0012400E01C0001260127BD436A +:1049E00034402C439278002A00D11E0004229443C4 +:1049F000B20004231A402243826100E047490800C4 +:104A0000F0BD0000FFCFFFFFFF3FFFFFFFFF00F003 +:104A10000000FF0FFF00FFFFFFFFE0FFFFFF3FFF72 +:104A2000F0B50A0000231900414C2468002A49D03F +:104A30005579002D01D0012600E01E00394D25409A +:104A4000B4028026F60034402C431579002D01D0A5 +:104A5000012600E01E00344D254074028026B60079 +:104A600034402C43D578002D01D0012600E01E00F3 +:104A70002E4D254034028026760034402C43557854 +:104A8000002D01D0012500E01D000226B4436D0079 +:104A90000226354025431478002C01D0012400E083 +:104AA0001C0001260127BD4334402C439278002A84 +:104AB00000D11E0004229443B20004231A40224372 +:104AC000026100E015490800F0BD000010B50200C9 +:104AD000002000291FD01269930A1C00E407E40F8C +:104AE0004C71530A1C00E407E40F0C71130A1C00FC +:104AF000E407E40FCC7053081C00E407E40F4C708B +:104B000013001C00E407E40F0C7092081300DB078D +:104B1000DB0F8B7000E0014810BD00000100DA00DF +:104B2000ECA40010FFFBFFFFFFFDFFFFFFFEFFFFF8 +:104B3000F0A4001000B5012027490A68520802407D +:104B4000002A01D0022001E00968084000BD274882 +:104B500001680800C007C00F704700B5C21E022AD6 +:104B600001D3012002E0C01E0860002000BD1CB57A +:104B7000FF246946FFF7F1FFC0B2002802D02000F1 +:104B8000C0B207E03C480099890041580800C007BE +:104B9000C00FC0B216BD00B5002801D0032801D354 +:104BA000012002E0401E0860002000BD1CB5FF246B +:104BB0006946FFF7F0FFC0B2002802D02000C0B263 +:104BC00008E03448009909014018C1680800C0078E +:104BD000C00FC0B216BD0000241526402E48006844 +:104BE00081080800C007C00F70470000401526402C +:104BF00038B504002000C0B208280DD3274D182274 +:104C00002749280000F0D0FB0020E87525482649F8 +:104C10000860002000F028FC24482100C9B2890067 +:104C200041580A00D205D20F002A05D0E4B2A100F3 +:104C300040580007000F00E0FF2032BD38B50500E6 +:104C40002800C0B208280ED3144C182214492000A2 +:104C500000F0AAFB0020E0758920000112490860DD +:104C6000002000F001FC1148EDB2A90040580009F5 +:104C70008007800F32BD0000401626400F480068B4 +:104C8000000AC0B270470E480068000AC0B2704700 +:104C90000000000000192640100027405C1A002880 +:104CA00008A000105B0800001C1B002840122640D2 +:104CB00004480068000AC0B270470000101020408D +:104CC0000C1020400810204038B50400072C0ED3EB +:104CD000124D18221249280000F066FB0020E875EA +:104CE000CE2040000F490860002000F0BDFB0A48BC +:104CF000A10040584007400F010009B2042907D124 +:104D00000448A1004058C106C90E80204000084355 +:104D100000B232BD00012640001226405C1A002875 +:104D200054A000101C1B00281E4909688A03920B1E +:104D30000260090E0A00D207D20FC2711749096832 +:104D40000A00D204D20C82800A00120CD2B2028174 +:104D5000090CC9B24181264909680F220B00134092 +:104D600003730B091A4042730A0AD204D20CC2819F +:104D7000090F8907890F0174094909680A0A52074E +:104D8000520F82710A00120CD205D20D4282890F95 +:104D90000A00D207D20F0275704700003415264072 +:104DA0003C152640301526401CB50C006946FFF71F +:104DB000D4FE0100C9B2002901D0C0B217E00D48ED +:104DC0000099890040584106490E21601F21020ABE +:104DD0000A406260020C1140A160010F8907890F2F +:104DE0006173C00E0100C907C90F2173002016BDF1 +:104DF00038152640001626401CB50C006946FFF702 +:104E0000CAFE0100C9B2002901D0C0B233E01A487D +:104E1000009909014158CAB222601F220B0A1340AF +:104E200063600B0C1A40A260090F8907890F217378 +:104E30000099090141184A68D10F61730721130FC6 +:104E40000B40002B01D0012300E00023A3731202CA +:104E5000120A2261009A120180188068C20F22751E +:104E6000020E1300DB07DB0F63750200A275000C56 +:104E70000140E175002016BD0019264080B50100F3 +:104E800086481822FFF770F9012085490870002034 +:104E900002BD000000B583490968C90F002907D089 +:104EA0007F490978002907D100210160032000BD56 +:104EB000002101600320FAE77849096801600020B9 +:104EC000F5E7000010B50400FFF734FE002803D119 +:104ED0000020206003200BE071480078002804D0F7 +:104EE0006E4880682060002002E0002020600320DF +:104EF00010BD00B56A490978002904D0674949689E +:104F00000160002002E000210160032000BD0000DC +:104F100000B565490968C90F002907D060490978BB +:104F2000002907D100210160032000BD002101609C +:104F30000320FAE75949C96801600020F5E700003D +:104F400000B55A490968C90F002907D054490978A2 +:104F5000002907D100210160032000BD002101606C +:104F60000320FAE74D49096901600020F5E70000D8 +:104F700010B50400FFF732FE002803D100202060A6 +:104F800003200BE046480078002804D043484069DD +:104F90002060002002E000202060032010BD0000FF +:104FA00010B50C00C0B2FFF78FFE00280CD0401ED9 +:104FB0000ED0401E10D087214900401A10D0401E4C +:104FC00012D0C01E14D017E02000FFF763FF16E0D8 +:104FD0002000FFF78EFF12E02000FFF773FF0EE0C6 +:104FE0002000FFF795FF0AE02000FFF7C1FF06E071 +:104FF0002000FFF7A5FF02E000202060032010BD85 +:1050000080B501000020FFF7CBFF02BD80B5032271 +:10501000D2B2824203D200200860012003E0401C8B +:10502000C0B2FFF7BDFF02BD30B587B004006846CF +:10503000FFF7E6FF0100C9B2002903D0002121607B +:10504000C0B228E001A8FFF76FFE01AD287C02285E +:1050500003D10098206000201DE07B480068C00F4D +:10506000002808D0FFF773FDC0B2002807D1002048 +:10507000206003200FE00098206000200BE00098E3 +:1050800001994843A988FEF77BFAE979491CFEF7A4 +:1050900077FA2060002007B030BD0000441A0028D5 +:1050A000841B002818152640081526400C1526409C +:1050B000F0B58BB004000D0069462000C0B2FFF7C8 +:1050C000A5FF0100C9B2002903D000212960C0B2A8 +:1050D000B7E02000C0B2022848D307A92000C0B220 +:1050E000401CFFF761FEC0B200280DD0824E18228E +:1050F0008249300000F058F90020F075804881495D +:105100000860002000F0B0F97F482100C9B2890092 +:10511000401808380068C00F002807D007A8407B57 +:10512000022807D10098286000208AE000982860B3 +:10513000002086E0E4B2641C2000FFF718FD0100A7 +:10514000C9B2002902D0C0B2FF2803D100202860D4 +:10515000032076E00098079948430899FEF710FA73 +:105160000999FEF70DFA286000206AE001A92000E5 +:10517000C0B2401CFFF740FEC0B200280DD05E4E0A +:1051800018225E49300000F00FF90020F0755F48EA +:105190005C490860002000F067F95D482100C9B251 +:1051A00009014058C00F002807D001AE307B02280B +:1051B00007D100982860002043E000982860002074 +:1051C0003FE0E4B2641C2000FFF7F0FC0100C9B22C +:1051D000002902D0C0B2FF2803D10020286003209C +:1051E0002FE0707B012821D100980021019E00272B +:1051F0003F06320A17433606059A002392197B416F +:10520000FEF7A0FB06000F0002980021039A00237E +:10521000FEF798FB02000B0030003900FEF7ABF8F8 +:10522000000E0A021043090E286009E00098019957 +:1052300048430299FEF7A4F90399FEF7A1F9286003 +:1052400000200BB0F0BD00003015264010B5032340 +:10525000421E0400E4B2002C03D10800FFF7E4FE74 +:105260000CE01400DBB2E4B2A34204D31000C0B2DD +:10527000FFF71EFF02E0C0B2FFF792FE10BD38B587 +:1052800004000D002000C0B2FFF7B2FC0100C9B25B +:10529000FF2904D12800FFF7FDFDC0B21BE06946DD +:1052A000C0B2FFF7D3FF0100C9B2002903D000212B +:1052B0002960C0B20FE02000C0B2FFF7BFFCC0B24F +:1052C000002805D002280BD006D303280CD00FE00D +:1052D00000982860002032BD009840082860F9E757 +:1052E000009880082860F5E70098C0082860F1E77A +:1052F000002028600320EEE75C1A002854A000106C +:10530000630900001C1B0028001626407F090000CE +:10531000001926401CB5040069460020FFF7AFFFC6 +:105320000100C9B2002903D000212160C0B209E008 +:10533000FFF7A9FC01000098C9B2491CFEF720F94B +:105340002060002016BD1CB5040069460020FFF750 +:1053500096FF0100C9B2002903D000212160C0B22C +:1053600008E0FFF78BFC01000098491CFEF708F9E4 +:105370002060002016BD1CB504006846FFF7E3FF5F +:105380000100C9B2002901D0C0B208E0FFF790FCCB +:1053900001000098491CFEF7F3F82060002016BDBC +:1053A00080B5FFF7E8FF02BD71B58B0709D0521E2B +:1053B00028D30B780370401C1B421FD0491C8B075D +:1053C000F5D1121F3ED3274CE50140C9331BB3432F +:1053D0002B4209D1830716D140C0121F32D340C9D6 +:1053E000331BB3432B42F7D0121DFF231E4205D0BF +:1053F0000670401C360A521E1E42F9D111000022CE +:10540000FFF7E0F871BDDB4216D0401E4670360A49 +:105410004680360C0671001D121F06D340C9331B8F +:10542000B3432B42F2D0401CDEE7401C0AE040C9E7 +:10543000331BB3432B42D7D10680360C4680001D68 +:10544000121FF4D2121D0BD00B780370521E07D01E +:10545000401C491C1B42F7D10370401C521EFBD15B +:1054600071BD00000101010100B501BEFEE780B57C +:105470000C4A0D490B68934210D00A6002684A60DA +:1054800042688A608268CA60C2680A6102694A61C9 +:1054900042698A618269CA61C069086200F008F8DD +:1054A00001BD0000F3F2F1F0201A002870477047A8 +:1054B00000B5FEE700B5401A002801D4012000E045 +:1054C0000020C0B200BD00B5401A00B2012801DBC7 +:1054D000012000E00020C0B200BD00B500281BD0B4 +:1054E000002919D002780B789A4215D142784B786E +:1054F0009A4211D182788B789A420DD182888B881A +:105500009A4209D1C288CB889A4205D1008909897B +:10551000884201D1012000E00020C0B200BD00B5EA +:105520000100C9B2042903D28B49C0B2085C00E073 +:105530000420C0B200BD10B50C000100C9B204299E +:1055400001D2002C01D1002006E0C0B2FFF7E7FF36 +:105550002100C0B201F044FD10BD10B50400002CC4 +:1055600002D06078042801D3002011E02089401D7A +:105570000A21FEF705F8322800D23220602800D335 +:105580005F206178032902D1332800D33220C0B2D2 +:1055900010BD00B5714B1870714B1970002A05D001 +:1055A0001178704B197091886F4A1180C0B2002831 +:1055B00003D001206D49087002E000206B4908709B +:1055C00001206B49087000BDF8B50500002D05D01D +:1055D0000C00002C02D01600002E01D100203BE070 +:1055E0000A21002227003800FEF7E6FF68682070D5 +:1055F0006868000A03210A00024062700200D2B209 +:1056000092080A40A2706A68120CA280AA68E2801E +:10561000AA68120C22812A68C0239B009A4202D1F8 +:105620002A7B082A03D001203070002014E0F022E9 +:105630000240102A03D00220307000200CE02078B5 +:10564000C01E05D0401F012802D9001F012801D823 +:10565000012001E031700020F2BD00B5002801D02A +:10566000002901D1002055E04278042A03D3042008 +:10567000087000204EE00278092A06D14278002AFC +:1056800003D004200870002044E00278032A1FD1D0 +:105690008278012A06D08278022A03D00520087079 +:1056A000002037E0C28896235B009A420CD3B44AAC +:1056B00043789B00D258C3889A4205D30089B14AE7 +:1056C0008018B14A904222D306200870002021E0C1 +:1056D0008278002A03D00520087000201AE00278A2 +:1056E0000D2A0AD1C288A94B9A4202D1008900280A +:1056F0000DD00620087000200CE0C288002A02D1DC +:105700000089002803D006200870002002E0002055 +:105710000870012000BD80B58EB00100C9B2042917 +:1057200015D29B490200D2B20C235A438918897AB8 +:1057300000290AD16946C0B2FFF7FDFE002806D055 +:1057400068463221405C002801D0012000E00020A2 +:10575000C0B20FB000BD0000A8A50010961B002825 +:10576000971B0028981B00287C1B00289C1B0028E6 +:10577000FC12002838B50024250007E02000C0B244 +:10578000FFF7C9FF002800D06D1C641C2000C0B2C8 +:105790000428F3D32800C0B232BD38B500252C0050 +:1057A0000AE02800C0B2FFF7B6FF002803D02000AF +:1057B0000124AC4004436D1C2800C0B20428F0D37F +:1057C0002000C0B232BD38B58EB000252C001BE0E1 +:1057D00069462800C0B2FFF7AEFE002813D0684625 +:1057E000417804290BD04178062908D0417807294F +:1057F00005D04178082902D04078092803D120003B +:105800000124AC4004436D1C2800C0B20428DFD33F +:105810002000C0B20FB030BD38B58EB000252C00CE +:1058200010E069462800C0B2FFF785FE002808D0C6 +:1058300068463421405C002803D020000124AC409D +:1058400004436D1C2800C0B20428EAD32000C0B273 +:105850000FB030BDF8B504002000C0B2042837D224 +:105860000F002000C0B2FFF75AFE060001253900E4 +:1058700089B22800B040C0B2FCF773FEFCF776FD99 +:10588000390089B23000C0B2FCF7D7FDBFB2390091 +:105890003000C0B201F0BAF800203D492200D2B277 +:1058A0000C235A43891888723A490A782800A04084 +:1058B00082430A70C04801782A00A2400A4302705D +:1058C000BE4801782A00A2400A430270BC48057015 +:1058D000F1BDF8B50700380080B2FCF795FEFCF783 +:1058E00045FD0026340018E02000C0B2FFF717FE87 +:1058F0000500390089B22800C0B2FCF79EFD3900CE +:1059000089B22800C0B201F081F821482100C9B253 +:105910000C22514340188672641C2000C0B2042837 +:10592000E2D31C4806700F20A3490870A3490870F1 +:105930000120A3490870F1BD38B501004C78144D21 +:105940002000C0B20C22504328180A22FEF726FC81 +:10595000012001002200D2B20C235A43AA189172EE +:105960000C490A780300A34013430B7092490A784C +:105970000300A34013430B709149087031BD000030 +:1059800074A40010D4FEFFFFBD0200003CC3000061 +:10599000F0190028991B00287FB5012421006A46D0 +:1059A000117088490A68521C0A606A4601A9FFF70B +:1059B0000BFE002805D0694601A8FFF74EFE00281F +:1059C0000AD181480168491C016001AA6846017832 +:1059D0000220FFF7DEFDDFE001AD2878092836D090 +:1059E000B748017800291ED0A988B64A128891428A +:1059F00019D1B548007800280BD0B44901A8FFF7A9 +:105A00006CFD002805D001AA00210320FFF7C1FD8D +:105A100008E06D480168491C016001AA08210220C4 +:105A2000FFF7B7FDB8E00078002810D0A54801883E +:105A3000A888FFF748FD002809D163480168491C80 +:105A4000016001AA08210220FFF7A3FDA4E02000C5 +:105A50009B49087020009C490870A88899490880D3 +:105A60009A4801A94CC94CC00C390C38287809282F +:105A70000CD1A888FFF72DFF01AA00210420FFF711 +:105A800088FD93480168491C016085E02878082852 +:105A90000DD1A9886878FFF7DDFE01AA0021012059 +:105AA000FFF777FD8A480168491C016074E0287897 +:105AB0000C2813D1687884403F4801782143017055 +:105AC0003E4801780C43047001AA00210120FFF731 +:105AD00060FD7F480168491C01605DE028780D2861 +:105AE0003BD16878FFF717FE002809D0B24801685B +:105AF000491C016001AA09210220FFF74AFD4BE081 +:105B000001F05AFC00280BD1FCF794FC002807D1C7 +:105B10006878FFF704FDC0B201F036FA002805D11D +:105B200001AA0B210220FFF734FD35E0224801785D +:105B30006A78230093400B430370204801786A7809 +:105B400094400C43047001AA00210120FFF721FDBD +:105B50005F480168491C01601EE001F02DFC00282F +:105B600005D001AA0B210220FFF713FD14E068788D +:105B7000FFF7D1FD002810D0A9886878FFF76AFEEA +:105B800001A8FFF7D9FE01AA00210120FFF701FDBE +:105B90004F480168491C01607FBDFFF7EBFD0428F9 +:105BA000EED385480168491C016001AA0A21022040 +:105BB000FFF7EFFCF0E70000FD120028FE120028BE +:105BC000FC120028341B0028381B002838B50400BC +:105BD000002C02D02078032801D000200FE0200004 +:105BE000FFF7BBFC05006078FFF799FCA1880091E6 +:105BF0002B00DBB2E288A178C0B201F061F932BDBE +:105C0000F8B584B0FCF7A5FF009001F0D5FB0028A3 +:105C100006D16A4801680098FFF74CFC002800D1C3 +:105C20004BE0002000E0401C0100C9B2042944D22E +:105C30000326634F39780D183540624C2900C9B2EC +:105C40000C2251436118897A0029ECD001A829005F +:105C5000C9B20C22514361180A22FEF79FFA0020B4 +:105C60002900C9B20C225143611888720124564997 +:105C70000A782000A84082430A702800401C064091 +:105C80003E70009864304D49086001A8FFF79EFF00 +:105C9000002804D101AA0E210220FFF77AFCA648B1 +:105CA00001782200AA400A430270A4480178220029 +:105CB000AA400A430270A248047005B0F0BD00007B +:105CC0009A1B00287E1B00289B1B0028DC1A00283A +:105CD000401B002800B502780B789A4227D1427801 +:105CE0004B789A4223D182788B789A421FD1C2781E +:105CF000CB789A421BD142684B689A4217D1302226 +:105D0000825C3023CB5C9A4211D13222825C3223F6 +:105D1000CB5C9A420BD13122825C3123CB5C9A421C +:105D200005D13422805C3422895C884201D0012074 +:105D300000E00020C0B200BD38B58EB0002435E0D0 +:105D400069462000C0B2FFF7F6FB00282DD07D4D3C +:105D50002000C0B2285C00280AD07B482100C9B2CC +:105D60003822514341186846FFF7B4FF00281CD081 +:105D700075482100C9B2382251434018694638227B +:105D8000FEF7F2F9012001002200D2B2A9546A49BB +:105D90000A780300A34013430B7068490A78030094 +:105DA000A34013430B7066490870641C2000C0B206 +:105DB0000428C5D30FB030BD3C1B0028541B00285D +:105DC0009E1B0028F0190028991B002800B50078B8 +:105DD000002808D0012808D0032808D0042804D0BF +:105DE000092806D007E0002006E0012004E0032097 +:105DF00002E0062000E0072000BD00B54078002842 +:105E000010D0012810D0022810D0032810D0042868 +:105E100012D005280ED0062810D0072810D0092847 +:105E200010D011E0002010E001200EE002200CE074 +:105E300003200AE0042008E0052006E0062004E034 +:105E4000072002E00B2000E0092000BD80B501F032 +:105E5000B3FA002808D001F0B9FA002801D10320D4 +:105E600000E00420C0B210E0FFF7ADFC002803D131 +:105E700001F0ACFA002801D0042006E0FFF78DFC09 +:105E8000002801D0022000E0012002BDF8B5FFF794 +:105E900084FC6946C870FFF796FC0400FFF7BCFC61 +:105EA000694688702000C0B2002803D101F08EFA44 +:105EB000002801D0012500E00025884807780F263A +:105EC0003740FFF7C3FF00013843694648708448F4 +:105ED0000078694608708348077801F06DFA002859 +:105EE00001D0102100E000216846C0783040014315 +:105EF000E80108432640694689780A0132437A4915 +:105F00000978F0230B43694649786C4624782402CB +:105F10000C437649098809042143FFB2C0B200024C +:105F20003843D2B214040443DBB21A062243704849 +:105F300001F05EFCF2BD0000FD120028FE120028F8 +:105F4000FC1200284C1B002810190028F1B594B051 +:105F5000684814A909780C2251434018019006A9F9 +:105F600014A80078FFF7E7FA002801D10020B3E079 +:105F700006A8FFF72BFF070006A8FFF73EFF040067 +:105F800006ADE878694608703220285C002802D007 +:105F900030202E5C00E0002607986946488055486E +:105FA00014A909780C2251434018807A00280DD09A +:105FB000012427000198807869460870019880883C +:105FC000694648800198FFF7C8FA06003220285C2D +:105FD000002808D1474814A909780C2251434018D9 +:105FE000807A002802D00420059001E0002005906E +:105FF0003420285C002802D00820049001E0002012 +:1060000004903420285C002805D00998002802D18B +:106010001020039001E0002003903420285C002829 +:1060200009D00A98002806D009980A99884202D314 +:106030002020029001E0002002906878042802D11C +:106040004020019001E0002001906878072803D0EB +:10605000FCF7F0F9002801D0802000E0002069461C +:1060600009788907890F059A0A4304991143039A0D +:106070000A4302991143019A0A4310433421695C8F +:10608000002907D00D997C4A914201D37B4A02E056 +:106090000D9A00E07A4A14A90978FFB23B071B0F5A +:1060A000E4B22401F02525401D432B020B43C0B26E +:1060B0000004184369464988090601436846408838 +:1060C00080B2000A80B292B213020343F6B23206E3 +:1060D0001A436C4801F08CFB15B0F0BD961B0028EC +:1060E000971B0028981B0028A51B00287C1B002854 +:1060F00001030000F019002878B585B004002000E5 +:10610000C0B2042809D22000C0B2FFF708FA6946DD +:10611000C0B2FCF72FFD002801D0002031E0684616 +:106120004178002901D0012100E000218278002A75 +:1061300001D0022200E00022C378002B01D004230A +:1061400000E000230579002D01D0082600E000269C +:106150004579002D01D0102500E000250A431343A6 +:106160001E433543E4B2EDB22A022243C18809043A +:1061700011430389808902041A43C120800001F081 +:1061800037FB06B070BDF1B590B001A910A800783A +:10619000FFF7D1F9002801D1002063E08020400200 +:1061A0000499814203D336496A46118002E004997A +:1061B0006A4611800599814201D3314F00E0059F65 +:1061C000079819303221FDF7DBF90400FF2C00D9C4 +:1061D000FF2401AD6878042801D1012600E00026E3 +:1061E0006878072803D0FCF725F9002801D00220A1 +:1061F00000E000206978082901D1042200E0002293 +:106200006978092901D1082100E0002130430243C7 +:1062100011430F20AA780240090111433122AA5CE0 +:106220001040974A10AB1B78D25C1201024310A8B1 +:106230000078C9B20B020343684600880104194381 +:10624000BFB2E4B220043843D2B212060243B148CE +:1062500001F0CEFA002801D1002003E010A8007858 +:10626000FFF74AFF11B0F0BD80B58649864AAA48BB +:1062700001F0BEFA02BD0000112700001027000047 +:10628000FFFF000002030000F8B5FCF762FC040009 +:106290007E4D29682000FFF70DF9002805D0012068 +:1062A0007B4908702000643028607A4D29682000FE +:1062B000FFF700F9002806D0012098490870FA205D +:1062C00040002018286096480078002814D195488E +:1062D00001682000FFF7EEF800280DD0924A93489D +:1062E00000781070012092490B78127805009540D3 +:1062F0001D430D708A4908708E480078002814D11B +:10630000A74801682000FFF7D5F800280DD0A54A5E +:10631000A54800781070A54901200B781278050077 +:1063200095401D430D7083490870594D2878002809 +:106330001ED0FFF7ABFD002815D0002028709C4927 +:106340000A78521C1207120F0A709A490A78002A1A +:1063500004D0994A1070994A1070087098480168E2 +:10636000491C016003E097480168491C016085E011 +:106370006A4D287800280FD0FFF776FF002806D056 +:10638000002028708E480168491C016003E08D4898 +:106390000168491C016071E0002500E06D1C2800C7 +:1063A000C0B204282CD20127AF40614E3078384269 +:1063B000F4D02800C0B2FFF7C9FD00281BD0307808 +:1063C000B84330705648017800290FD056490978F3 +:1063D000EDB28D420AD155490A78521C9207920FAC +:1063E0000A700021017019344E4804607448016835 +:1063F000491C016003E073480168491C01603DE0ED +:10640000002500E06D1C2800C0B2042836D2012708 +:10641000AF40664E30783842F4D02800C0B2FFF763 +:10642000B2FE002825D03078B843307014482900D7 +:10643000C9B2415C491C0907090F2A00D2B2815434 +:106440003C48017800290FD056490978EDB28D42B9 +:106450000AD155490A78521C9207920F0A700021FE +:10646000017019344E48046055480168491C0160A8 +:1064700003E054480168491C0160FFE7F1BD0000DA +:10648000501B0028505354310F110401581B002891 +:10649000FC120028641B002870B530210024220063 +:1064A000494D2800FEF788F8E0212200474D2800DA +:1064B000FEF782F8042529002200454E3000FEF741 +:1064C0007BF82200434C29002000FEF775F800F00D +:1064D00093FA01F005F9002804D1062000F03CFFF2 +:1064E000002016E0FCF735FB3B4908603B4908609B +:1064F0000C4908602A490860394908600120394977 +:1065000008700F210A4A1170284A11700349087057 +:10651000012070BD0303000007030000FF120028E4 +:10652000A31B00285C1B0028A11B00289F1B002820 +:10653000FD120028A41B00281FB52B480068002866 +:1065400002D0022000F008FF00F01EFF00F034FF30 +:1065500000280AD0254C2078002812D12448008831 +:10656000FFF7B7F9012020700BE000201F490870E9 +:1065700007E00098C0218900884202D16846FFF7F1 +:106580000BFA684601F0F4F80028F2D1FFF738FB67 +:1065900000F01EFBFFF7D0FBFFF776FE1FBD0000EB +:1065A000601B0028A21B0028A01B0028FE12002848 +:1065B000A51B00289C1B0028961B0028971B002861 +:1065C000441B0028481B0028F01900281019002837 +:1065D0004C1B0028501B0028541B0028581B002867 +:1065E000641B0028FC120028141B00289D1B002897 +:1065F0007C1B002800B5C0B2042801D2012000E0B5 +:106600000020C0B200BD00B50100C9B2002901D10F +:106610006F4813E00100C9B2012902D1AF20400147 +:106620000CE00100C9B2022901D16A4806E0C0B2FB +:10663000032802D1E120400100E0002000BD00B5A8 +:10664000401A002801D4012000E00020C0B200BDA3 +:1066500000B5884201D3401A00E0081A00BD10B509 +:1066600000280FD00C00002C0CD06178002909D034 +:106670002169002906D0A168002903D02169826818 +:10668000914201D2002019E080680021A268002315 +:10669000FDF758F902000B0020694008002180181E +:1066A000594122690023FCF766FE6168FFF7D0FFBD +:1066B000052801D2012000E00020C0B210BD00B5C5 +:1066C000C0B232382E2801D2012000E00020C0B232 +:1066D00000BD00B50178022905D00178012904D157 +:1066E0004078022801D1012000E00020C0B200BDA6 +:1066F00000B50178032905D00178012904D140783B +:10670000032801D1012000E00020C0B200BD38B54F +:106710000C00BD490200D2B23C235A438D18C0B2CE +:10672000FBF7C5FF0A202870AC70002038216854A0 +:1067300031BDB549C0B23C2250430818092101704F +:10674000022181700021382281547047AE49C0B2C5 +:106750003C22504308180021017003228270382225 +:106760008154704770B505000C002800C0B2FFF7D7 +:1067700041FF002807D0FBF75DFE002803D18020F1 +:106780004002844201D3002020E02800C0B2FBF781 +:106790008EFF002807D1210089B22800C0B2FBF784 +:1067A0004CFE002801D0002010E09748EDB23C21BB +:1067B0004D4345193C2100222E003000FDF7FCFE20 +:1067C0006C600120A87038216854012070BD000061 +:1067D000FC530000901A0000F2B59EB007001600AE +:1067E0001C00002001903800C0B2FFF703FF002812 +:1067F00019D029A80078022803D029A800780328F6 +:1068000011D13800C0B2FFF7FEFE1E9988420AD3AC +:1068100005A93800C0B2FCF735FA002803D105A855 +:106820004078002801D10020E4E0774D3800C0B264 +:106830003C2148432818049004983621405C69465E +:1068400048706846067027A80078002807D1684677 +:106850000078FFF734FF002801D10020CAE0089932 +:106860001E98FFF7F5FE02900394269E289C27A809 +:10687000007800281CD00398002802D00298002835 +:1068800016D10298002811D1E4B2002C08D13100B1 +:106890003800C0B2FFF766FF002801D10020A9E050 +:1068A0003800C0B2FFF745FF0120A3E00020A1E0BF +:1068B0000FA804993C22FDF757FC2000C0B2002825 +:1068C00008D131003800C0B2FFF74CFF002801D1D9 +:1068D00000208FE03800C0B23C2148432D18E4B2BC +:1068E000002C0CD028000FA93C22FDF73DFC012014 +:1068F0000100A97001003822A95469464870249CFF +:1069000027A8007800282FD02598002801D00190D2 +:1069100018E0002C01D0019414E002980021FA2222 +:1069200092000023FDF70EF802000B0003980021EF +:10693000801859410022401E9141039A0023FCF720 +:106940001AFD01900198002807D0029A0199380099 +:10695000C0B2FCF73BFA002815D008213800C0B2BD +:10696000FFF7D5FE002045E0684601783800C0B248 +:10697000FBF784FF002806D008213800C0B2FFF7DB +:10698000C6FE002036E029A800786870002C01D0EF +:10699000012001E029A8007828701E98A860EC600A +:1069A000019828610020A861039828626846007851 +:1069B0003521685427A80078372168546846407804 +:1069C000362168541E980899884201D3012000E0BE +:1069D0000220342168541E993800C0B2FCF7FAFA3C +:1069E000002806D008213800C0B2FFF790FE002032 +:1069F00000E001201FB0F0BD10B5F0210022724C64 +:106A00002000FDF7D9FD10BD20180028F8B50500BD +:106A10000C002800C0B2FFF7EDFD002813D02800BD +:106A2000C0B2FBF744FE6848EDB23C214D43451926 +:106A30003C21002632002F003800FDF7BDFD6C60C0 +:106A40002E700320A870F1BD38B50400002505E0C4 +:106A500021002800C0B2FFF7D9FF6D1C042DF7D329 +:106A600031BD78B585B0040057482300DBB23C2522 +:106A70006B43C5182878082835D1696889B2200089 +:106A8000C0B2FBF739FD002804D12000C0B2FFF7E7 +:106A90005DFE96E02000C0B2FBF795FF002800D015 +:106AA0008FE0286B002806D0296B2000C0B2FBF7CE +:106AB000F5FF002812D0696889B22000C0B2FBF748 +:106AC0001BFD002804D12000C0B2FFF73FFE04E008 +:106AD00009212000C0B2FFF71AFE72E02000C0B208 +:106AE000FFF727FE6DE0C87804280BD0C87802288D +:106AF00008D0C878032805D0C878052802D0C878FF +:106B0000012805D109212000C0B2FFF700FE58E09E +:106B10002878012855D01600287806281BD1E86A65 +:106B2000FA21490041183000FFF789FD002811D0F3 +:106B300069462000C0B2FBF779FF002805D0092183 +:106B40002000C0B2FFF7E3FD3BE00398A862EE62CD +:106B50000720287035E0E86AFA21890041183000E2 +:106B6000FFF76DFD00282CD069462000C0B2FBF76E +:106B70005DFF002805D009212000C0B2FFF7C7FD46 +:106B80001FE0E86A361A0398A96A401A00210F4AE2 +:106B90000023FCF7D7FE32000023FCF7ECFB0600D5 +:106BA000002E05D02000C0B2FBF781FD002805D0E3 +:106BB00009212000C0B2FFF7AAFD02E02E630820E1 +:106BC000287006B070BD000020180028A0860100C3 +:106BD000F8B592B0FBF7CAFBFBF7BBFF06900020AD +:106BE00007900CE006212800C0B2FFF790FD03E0FB +:106BF0002800C0B2FFF79DFD0798401C079007983A +:106C0000042800D358E1079DAC4807993C22514322 +:106C100044183820205C002806D008A92800C0B2FB +:106C2000FCF730F8002800D0E6E7FBF703FC00286B +:106C300003D108AEF078072805D107212800C0B29B +:106C4000FFF765FDD8E7616889B22800C0B2FBF79D +:106C500053FC002804D12800C0B2FFF777FDCBE732 +:106C6000F078062807D12078082804D02800C0B280 +:106C7000FFF76CFDC0E72078012816D1B078002816 +:106C800013D0607820702078062802D10698E06240 +:106C90000BE0E068002806D00698E16840186169BA +:106CA0004018A06101E00020A061207806280BD0E8 +:106CB0002078072808D02078082805D020780128D7 +:106CC00009D16078062806D1069A08A92800C0B222 +:106CD000FFF7C7FE90E7207804282FD1E1690698D6 +:106CE000FFF7ADFC002828D06078059001200490C3 +:106CF0003720205C03906068029020690190E06872 +:106D00000090236A3520225CA1682800C0B2FFF7FA +:106D100063FD002811D1616889B22800C0B2FBF779 +:106D2000EBFB002804D12800C0B2FFF70FFD04E000 +:106D300004212800C0B2FFF7EAFC5DE7F0780228E2 +:106D400002D0F078032801D1012700E00027F07875 +:106D5000042803D03800C0B2002852D02000FFF72A +:106D6000B8FC00280AD008A92000FFF778FC00280A +:106D700004D02800C0B2FFF7DCFC3DE72000FFF79D +:106D8000A8FC002823D03620205C00281FD1280032 +:106D9000C0B2FBF78CFC00280CD0FBF74BFB0028A3 +:106DA00001D0072100E00821C9B22800C0B2FFF7D6 +:106DB000AEFC21E7042020700020A0610698FA2193 +:106DC000C9004018E06101203621605418E0380005 +:106DD000C0B2002809D02000FFF78AFC002804D0A8 +:106DE0002800C0B2FFF7A5FC0AE0FFB2002F01D0D7 +:106DF000052100E00421C9B22800C0B2FFF787FCDA +:106E0000FAE6F078012804D12800C0B2FFF791FC1F +:106E1000F2E6F078052815D12000FFF75AFC00288B +:106E20000AD008A92000FFF71AFC002804D0280087 +:106E3000C0B2FFF77EFCDFE606212800C0B2FFF7F4 +:106E400066FCD9E6A069002800D1D5E6A1690698BC +:106E5000FFF7F5FB002800D1CEE62800C0B2FBF713 +:106E600026FC00280CD0FBF7E5FA002801D007210A +:106E700000E00821C9B22800C0B2FFF748FCBBE619 +:106E80002078032800D0ADE669462800C0B2FBF7A1 +:106E9000CDFD002805D008212800C0B2FFF737FC3F +:106EA000AAE60398616A884200D0A1E6042128007E +:106EB000C0B2FFF72CFC9FE613B0F0BD20180028ED +:106EC000F6B585B004000E000698002815D01F0006 +:106ED0003800C0B2FFF7F3FB00280ED03000C0B27C +:106EE000012803D03000C0B2022806D10C9920003E +:106EF000C0B2FFF737FC002801D1002040E0390084 +:106F0000C9B22000C0B2FBF7B9FC002806D00821A6 +:106F10002000C0B2FFF7FBFB002031E01948210040 +:106F2000C9B23C225143451869462000C0B2FBF764 +:106F30007DFD002806D008212000C0B2FFF7E7FB46 +:106F400000201DE0039868623100C9B22000C0B281 +:106F5000FBF728FD002806D008212000C0B2FFF76B +:106F6000D6FB00200CE0012028700320687034203C +:106F70002E540698E8600020A86135202F54012087 +:106F800007B0F0BD2018002870B504002000C0B282 +:106F9000FFF730FB002809D02000C0B2FBF713FD3B +:106FA000002803D1FBF746FA002801D0002013E0A7 +:106FB0000A48E4B23C214C4304193820205C0028E4 +:106FC00001D0002008E065683C2100222600300046 +:106FD000FDF7F2FA6560012070BD0000201800285E +:106FE00038B596B005000C00002C1AD02800C0B2AD +:106FF000FFF700FB002814D069462800C0B2FBF759 +:1070000041FE00280DD10FA92800C0B2FBF78CFF6C +:10701000002806D10AA92800C0B2FBF707FD002806 +:1070200001D000203CE01F48EDB23C214D43401907 +:1070300001782170817861706946CA78A2708A7877 +:10704000E27042686260039AA260019AE260029A6A +:1070500022610C9A6261149AA261099AE261059A0E +:107060002262069A6262079AA262089AE2620FAAF4 +:10707000527E3023E2543622825C3123E25438229D +:10708000825C3223E2543622805C002801D0012049 +:1070900000E000203322A0544878342160540120BD +:1070A00017B030BD20180028AA490A680860704748 +:1070B000B3490A6810239A43C0B200011023184054 +:1070C000104308607047AE490A68AE4B1340C0B227 +:1070D0000002802252001040184308607047A849FF +:1070E0000A68B44B1340C0B2000380225201104022 +:1070F000184308607047A2490A6852005208C0B29B +:10710000C007104308607047B2480068704700200D +:107110007047B1480068704710B50400AE4804607D +:10712000BFF34F8FFFF7F5FFA04201D1012000E030 +:107130000020C0B210BD38B5C824C0B2002801D0AC +:10714000012505E0002503E00120FDF7F7F9641EA5 +:10715000A24901200A680240AA4203D0002C01D0B3 +:10716000020000E00022D2B2002AEDD109680140FD +:10717000A94200D00020C0B232BD0020984908606A +:107180009849086098490860BFF34F8F7047000026 +:10719000FEB50400FF20E230019001252F0000F031 +:1071A0002AFD0020FFF7C7FF69464870FFF7AFFFD1 +:1071B00069460870FFF7ADFF0600894800688949F5 +:1071C0000968894A1268894B98420DD141400023D1 +:1071D000DB434B409A4207D101992140002901D15C +:1071E000E10601D5290000E000210022804B1A7041 +:1071F0000022804B1A7000227F4B1A70019A2240A5 +:10720000002A02D17D4A117002E02A007B4B1A70DD +:107210007B4A117061000CD50020FFF77DFF07004D +:10722000FFF777FF0600FFF7A8FF002074490870FA +:107230000BE073490978002902D0FFF79EFF04E0B4 +:107240006A49884201D1FFF798FFFFB2002F09D0A9 +:107250006C48864206D06C48864207D16946097858 +:10726000002903D105206249087016E063490978B6 +:10727000002907D062490978002903D104205C491C +:1072800008700AE069460978002906D0FFF744FF34 +:10729000002802D105205649087068464078002829 +:1072A00002D15348057048E000F05CFC00F084FC1B +:1072B000C82424012000FFF7F7FE0120FFF7F8FEA5 +:1072C0000020FFF700FF0020FFF709FF0120FFF774 +:1072D00012FF00F066FC1F480068A0420BD128484E +:1072E000016809092940002905D00068010B080040 +:1072F000C007C00F00E02800C0B2002802D03C4800 +:1073000005701AE000F06BFC0120FFF714FF002865 +:1073100002D13748057010E000F088FCFA208000A8 +:10732000FDF70CF9FFF7F0FE002802D13048057098 +:1073300003E03048057000F001F8F7BD80B52D4836 +:107340000078002805D02C480078002801D100F0F2 +:107350006DFC01BD08C0264080B50100C9B20029FE +:107360000CD023490A78002A08D108700020FBF7C6 +:1073700083F9FBF791F90020FFF766FB01BD0000E0 +:1073800010C02640FFFEFFFF80B5FFF7C0FEFFF7ED +:10739000C0FE1C49884202D10520FFF7DDFFFBF744 +:1073A00049F8002802D00120FFF7D6FFFFF7C6FFFB +:1073B00001BD0000FFEFFFFF00B50D480078002879 +:1073C00001D0012000E00020C0B200BD08480078D4 +:1073D0007047000014C026400C10274000C0264013 +:1073E000001027400410274008102740364C494819 +:1073F000911B0028931B0028941B0028921B002837 +:10740000951B002845564F4D41564F4D00B5010084 +:1074100009B2002905D40121C006C00E8140AE4842 +:10742000016000BD00B5010009B2002905D40121A9 +:10743000C006C00E8140CC48016000BDF0B502001E +:1074400012B2002A18D41822A44B040024B2A408B3 +:10745000A4001C59FF250600F6001640B540AC43B9 +:107460008901C9B20500ED002A409140214300B2D4 +:107470008008800019501CE00F22BC4C1825030026 +:107480001BB21340083B9B089B00060036B23240FB +:10749000083A92089200A258FF260700FF002F40EA +:1074A000BE40B2438901C9B2C0000540A9401143A2 +:1074B000E150F0BD00B5002A03D101228A404260AC +:1074C00002E001228A40826000BD0268CA401000CA +:1074D000C007C00F704700B5022801D301200FE09C +:1074E0000B00DBB2042B01DB012009E0BC4B800068 +:1074F0001818C9B2405C824201D3012000E000208C +:1075000000BDF8B504001F00022C01D3012023E0C8 +:1075100015000E002A003100C9B22000FFF7DBFF82 +:10752000012801D1012017E03000C0B2002816D197 +:10753000FF2F12D8AB4861034018A90040188021E2 +:10754000490140180168A84A0A403902FF231B027A +:107550000B40134303600020F2BD0120FCE7F6B2AC +:10756000012E16D180204002874210D29D4861032F +:107570004018A9004018A0214901401801689B4A01 +:107580000A403902C44B0B4013430360E3E7012078 +:10759000E2E70120E0E700B20012C0B27047F8B5A0 +:1075A00005000F001600280000B2FFF7F4FF0400EA +:1075B0002000C0B2022801D301202CE032003900A3 +:1075C000C9B22000C0B2FFF786FF012801D1012017 +:1075D00021E084482100C9B2490341182A00D2B2EF +:1075E00092008918C02212018A58AC491140FFB29A +:1075F0003B02C02292001A400A43FF218A43F1B2A3 +:107600001143E4B262038018EDB2AA008018C022D0 +:10761000120181500020F2BD70B50400022C01D38C +:10762000012028E015000E002A003100C9B2200018 +:10763000FFF751FF012801D101201CE069486103D7 +:107640004158802212060A43C02189041143FF22B7 +:1076500012040A43914B1340F6B23202C021890052 +:1076600011401943FF229143EAB20A436103425099 +:1076700061034058002070BD70B50400022C01D396 +:10768000012028E015000E002A003100C9B22000B8 +:10769000FFF721FF012801D101201CE051486103BF +:1076A00041588022D2050A43C02189041143FF2298 +:1076B00012040A43794B1340F6B23202C02189000A +:1076C00011401943FF229143EAB20A436103425039 +:1076D00061034058002070BD00E100E000E400E0DC +:1076E00038B500200100974A11700100964A1170C8 +:1076F0009649086000F0D4F8002803D000F02CF977 +:10770000002804D19248B849086000202BE0914C31 +:107710002000FCF7F7FC002804D08D48B24908602F +:10772000002020E0B1490020205EFCF70AFD002186 +:107730000220205EFFF782FE0220205EFFF772FE2D +:10774000AB4D280001F0ECF8AA49280000F05EFDDE +:10775000A5490860002801D0002004E00220205E36 +:10776000FFF754FE012032BD80E200E01CED00E096 +:1077700038B50400002C01D100202FE0FCF788FB75 +:1077800071490A7813006F4D2D78DBB2AB4203D1FB +:10779000FCF782FB002021E0C44B1500EDB22D0167 +:1077A0005D5925601500EDB22D015D196D686560AC +:1077B0001500EDB22D015D19AD68A5601500EDB2A3 +:1077C0002D015B191B7B2373521C1300DBB2102BA2 +:1077D00000D300220A70FCF75FFB012032BD0000DD +:1077E000B8A4001000000440FF00FFFFFF0000FFEE +:1077F000F1B594B00D0016005021002422006F4610 +:107800003800FCF7D9FE149880210901884203D280 +:107810004E480068002804D04D48A949086000205F +:1078200038E068462100017021000172149901912D +:1078300008214C2281540395049600E0641C20002A +:10784000C0B2082821D26A4D2100C9B2280001F037 +:10785000BFF90100C9B20129F0D0C0B20328EDD0B0 +:107860001498984908606A462100C9B2280001F0BE +:10787000DBF893490860002805D193480168491C4A +:107880000160012006E0002004E090480168491CE6 +:107890000160002015B0F0BD00FFFF00FFFCFFFFFE +:1078A000F8B5002500956846FDF734FD002801D0A5 +:1078B000002046E0864C0098A04205D300982100A5 +:1078C000FBF75EFE002901D000203AE0009821007D +:1078D000FBF756FE0400002C03D0FF20801C8442DE +:1078E00001D300202DE0FF2608363000FFF753FEBD +:1078F0000700002200213800FFF7BEFE002801D05B +:1079000000201EE0002200213000FFF748FE002882 +:1079100001D0002015E023005B1E0022002138006A +:10792000FFF7EFFD002801D000200AE0002200212F +:107930003800FFF771FE002801D1012000E0280087 +:10794000C0B2F2BD821B0028831B0028141B002834 +:10795000010062000008002818B5A7B01AA85D4908 +:107960003422FCF701FC0DA85B493422FCF7FCFB38 +:1079700068465A493422FCF7F7FB594C1AAA0721EA +:107980002000FBF7BEFD002801D0002024E00DAA56 +:1079900006212000FBF7B5FD002801D000201BE0E8 +:1079A0006A4601212000FBF7ACFD002801D0002031 +:1079B00012E0012201212000FFF77CFDFA20800067 +:1079C000FCF7BCFD01212000FFF77FFD012801D15C +:1079D000012000E00020C0B228B010BD80B50448EE +:1079E00000F0D4FA01BD0000F41A0028DD7900107F +:1079F000000454400808002870B51400002C05D07D +:107A0000207A002802D12078002800D04DE03548A7 +:107A10000168491C01606068334908606068C021E2 +:107A20008900884214D14C20205C082810D1E068DD +:107A3000C0B209280CD1E068000AC0B2102807D1F2 +:107A40002069002804D1E068000C80B2FAF7DCFD60 +:107A5000264801780A00521C1300DBB2102B00D319 +:107A600000221300224D2D78DBB2AB4204D1214815 +:107A70000168491C016018E00C4B65680E00F6B205 +:107A800036019D514C25655D0E00F6B236019E19FA +:107A90003573E5680E00F6B236019E1975602469EB +:107AA000C9B2090159188C60027070BD20170028F6 +:107AB00000B5C0B2002802D101200F49086000BD06 +:107AC000101B00280C1B0028001B0028041B00288A +:107AD000005A6202CCA1001000A2001034A20010D3 +:107AE00080013140F81A0028081B0028821B00285A +:107AF000831B0028FC1A0028141B002843490A682D +:107B00000860704742490A680860704741490A683E +:107B10000860704740490A6801239A43C0B20300D5 +:107B2000DB07DB0F13430B6070473B490A681023E8 +:107B30009A43C0B2000110231840104308607047F8 +:107B400035490A68354B1340C0B20002802252000A +:107B500010401843086070473148016801220A4309 +:107B60000260704710B5FCF793F9040000F024F8A8 +:107B70000020FFF7C3FFFA20C001FFF7C3FF00207A +:107B8000FFF7C4FF0020FFF7C5FF0120FFF7CDFF7F +:107B90000020FFF7D5FF00F004F82000FCF77CF987 +:107BA00010BD80B5FCF774F91E490A68032313431E +:107BB0000B60FCF771F901BD80B5FCF769F9194953 +:107BC0000A6803239A43012313430B600A680323C3 +:107BD0009A43022313430B60FCF75EF901BD80B5A5 +:107BE000FFF7EAFF10480168802212060A4302608C +:107BF000FFF7D7FF01BD80B5FFF7DEFF0A48016838 +:107C0000490049080160FFF7CCFF01BD04C02640D0 +:107C100008C026400CC0264010C02640FFFEFFFFD3 +:107C200044C0264040C0264000C0264080B5FFF733 +:107C3000C3FFFFF791FFFFF7B4FF01BD00B50028B8 +:107C400001D0002901D1A04808E0A04A904206D105 +:107C500000200880802080014860002000BD9C4AF0 +:107C6000904205D18020800102000A804860F4E73C +:107C7000BA4A904206D18020C0010880802080014D +:107C80004860EAE7B64A904206D1C020C0010880A9 +:107C9000802080014860E0E7B24A904206D180200F +:107CA00000020880802080014860D6E7AE4A9042FA +:107CB00005D100200880802080014860CDE7AB4AD4 +:107CC000904205D18020800102000A804860C4E70C +:107CD000A74A904206D18020C00108808020800100 +:107CE0004860BAE7A34A904206D1C020C00108808C +:107CF000802080014860B0E79F4A904206D18020F2 +:107D000000020880802080014860A6E76E48A5E751 +:107D100000B5002100E0491C0A290AD2974A18231D +:107D20004B43D3589842F6D1182041435018001DB8 +:107D300000E0002000BD10B50A00D2B2402A01DBED +:107D4000002017E0020C120480235B02D218AC233F +:107D5000C3589B089B049B0C9B00D218C9B2884B4C +:107D6000BC240059000A4007400F185C801C4143A6 +:107D70008800101810BD10B50A00D2B2202A01DB0D +:107D8000002016E0020C120480235B02D218C023EC +:107D9000C3589B089B049B0C9B00D218C9B2784B1C +:107DA000C82400594007400F185C801C41438800DC +:107DB000101810BD10B50B00DBB2022B01DB002048 +:107DC00028E0030C1B04802464021B190C00E4B29D +:107DD000002C05D1A0240459A408A404A40C04E098 +:107DE000B0240459A408A404A40CA4001B19C9B20B +:107DF000002904D1BC2140584007400F04E0BC21B9 +:107E0000405800094007400F5D49085C801C424310 +:107E10009000181810BD00B55A49884201D30521B9 +:107E200000E00021400A4006400E0818C0B200BD24 +:107E300038B504000020002C44D02000FFF7EBFFF1 +:107E40000121226D520F0A40002A05D00A004E4B34 +:107E50000500EDB26D005A53226D120F0A40002A40 +:107E600005D00222484B0500EDB26D005A53226D39 +:107E7000D20E0A40002A05D00322434B0500EDB282 +:107E80006D005A53226D920E0A40002A05D004223A +:107E90003D4B0500EDB26D005A53226D520E0A4063 +:107EA000002A05D00522384B0500EDB26D005A536B +:107EB000226D520D1140002904D00621324AC0B271 +:107EC0004000115231BD000001006200000052402C +:107ED0000002524070B500221300130000283CD06D +:107EE00003689B0F1C00E407E40F0C720B7A002B55 +:107EF00005D10368DB00DB089B144B6003E00368DB +:107F0000DB00DB084B6043685B0D1C00E407E40FFB +:107F10000C7043681B0C1B071B0F4C240B554C2388 +:107F2000CB5C082B01DA130002E04C23CB5C083B4E +:107F30000AE01400A4B2A4000419A4681500ADB2AC +:107F4000AD004D19EC60521C14000D4D1E00B6B270 +:107F5000AD5DA4B2ADB2AC42EBD370BD00045240F3 +:107F600000065240000852400000544000025440B5 +:107F700000045440000654400008544058080028AB +:107F8000B0A4001001085240741A0028F1B5A8B03E +:107F90000020009000202898FFF7BAFE0700002F6D +:107FA00029D0289E306D97490140002902D0289899 +:107FB000FFF73EFF0125306DC00C284000281CD182 +:107FC000306D2840002868D1306D00092840002815 +:107FD00000D0BAE0306D400A0540002D0BD03000D3 +:107FE00050300168802292000A43026038680028FD +:107FF00001D03868804729B0F0BD300050300168AA +:10800000802212030A43026098203058002816D0BC +:10801000002400E0641C2000C0B220282CDA982044 +:108020003058E04028400028F4D02100C9B22898F8 +:10803000FFF781FE0090A540982035501CE09C2061 +:108040003058002818D0002400E0641C2000C0B282 +:10805000202811DA9C203058E04028400028F4D035 +:1080600020342100C9B22898FFF765FE0090200057 +:10807000E03085409C20355000980028BBD013A9E3 +:108080000098FFF727FF78680028B4D013AA2100D2 +:10809000C9B200207B689847ADE7300050300168D6 +:1080A0002943016017E0289AD4235B00D45801AA21 +:1080B0000300DBB29B00D450401C02000B00D2B284 +:1080C000DBB29A42EFD3B868002804D001AAC9B243 +:1080D0000020BB689847A42030584006400E012875 +:1080E00089DB2898C021490040582840002822D127 +:1080F000A420305800903F240098020A224000211A +:108100002898FFF757FE0028E5D013A9FFF7E2FEF5 +:108110003000A83001683F229143009A120A1440AF +:108120000C43046078680028D5D013AA00210120F0 +:108130007B689847CFE734482899BC22895849077B +:10814000490F415C891C0020B7E7300050300168BE +:1081500010220A43026017E0289ADC235B00D458FF +:1081600001AA0300DBB29B00D450401C02000B00AC +:10817000D2B2DBB29A42EFD3B868002804D001AA89 +:10818000C9B20120BB689847B42030584006400E61 +:10819000012800DA2FE72898C021490040584008FC +:1081A0002840002822D1B420305800903F24009865 +:1081B000020A224001212898FFF7FCFD0028E3D0A5 +:1081C00013A9FFF787FE3000B83001683F229143C2 +:1081D000009A120A14400C43046078680028D3D037 +:1081E00013AA012101207B689847CDE7064828990A +:1081F000BC22895809094907490F415C891C0020A4 +:10820000B4E70000EEE0F73FB0A40010F1B594B081 +:10821000D34800681290D34800681190D24807688C +:10822000D2480668D24800681090D24800680F9083 +:10823000D14800680490D14800680E90D04800688A +:108240000190D04800680D90CF4800680C90CF484E +:1082500000680B90CE4800680A90CE4800680990EC +:10826000CD4800680890CD4800680390CC4800686D +:108270000290DC4800680790DB4800680090149882 +:10828000002810D00C00002C0DD02820205C0028E5 +:1082900002D0E06A002806D03020205C002804D0FC +:1082A000606B002801D1D14824E31498FFF730FD1A +:1082B000002801D1CD481DE321680160616841605B +:1082C000A1688160E168C16021690161149D01209C +:1082D000A861A869C007FCD50320A86105A9149866 +:1082E000FFF7ACFC002801D0C04803E32820215C44 +:1082F000129ABF4810400A04FF21090411400143AB +:10830000BC4808406946898A89B2890889B28900CF +:108310000143842029503020205C4006400EFE217D +:10832000C903119BE44A1A40000408401043842208 +:10833000AA5892082823E35CD2189204920CAD4B01 +:1083400003409200DD48104018438822A850A06BDB +:10835000C000C0087A0F520710439022A85007228D +:108360004020205C1040099FD54B3B400702E02095 +:10837000C000384018434123E35C13407027B843E2 +:108380001F0170233B4003434220205C104007271D +:10839000BB431843BC23E8504420235C1800C007AB +:1083A000C00F73005B08C6071E434520205C4006D3 +:1083B000400EC44B1E400706FE20C0053840304327 +:1083C0004626A65D7606760EBB4F07403004084071 +:1083D00038438826AE59B6083027E75D7F00F61986 +:1083E000B604B60C834F0740B600B448304038435B +:1083F000A026A8514820265C3000C007C00F109E60 +:1084000076007608C00730434926A65D7606760ECC +:1084100003403606FE20C005304018434A23E35C83 +:108420005B065B0EA44E0640180401403143A64F84 +:10843000A020285880084623E35C4226A65DBE5D46 +:10844000B61C7343C01883049B0C6A4808409B0009 +:108450009A4919400143B02029504720205C002848 +:1084600001D1002100E00121009B0120834308008D +:10847000C007C00F18434B21615C002901D10021C6 +:1084800000E00121022398434900022319400143DF +:108490001498C0235B00C150B020285880084A219E +:1084A000615C4123E35CFB5C9B1C594340188304E3 +:1084B0009B0C82484F490F9E0E409B000340334364 +:1084C000AC26AB514323E35C1A40089B0726B34319 +:1084D0001A43C823EA507D4A049E16407C4B1E4036 +:1084E0000496AC26AE59B60800964C26A65DB44656 +:1084F0004026A65DBE5DB61C67467743009EF61912 +:10850000B604B60C049F0F40B60006403E43F02769 +:10851000EE510E9F6F4E3E4032404D26A65DB60690 +:10852000B60E13403604FC22920332401A43F02365 +:10853000EB589B0814339B049B0C11409A0010408D +:108540000843C0216850802252021498000C000495 +:108550006946898A4018801806998B08002125E011 +:10856000F4A40010F8A40010FCA4001000A5001052 +:1085700004A5001008A500100CA5001010A50010FF +:1085800014A5001018A500101CA5001020A50010AF +:1085900024A5001028A500102CA5001030A500105F +:1085A00034A5001000260660001D491C9942F9D32D +:1085B00001990120084302210143CF480840217D51 +:1085C000012901D1012100E00021CC4B03404802E8 +:1085D0008021890008401843217D01290CD1012107 +:1085E0000BE000BF38A500103CA5001001006200A0 +:1085F000FFFF00FF0300FFFF0021C14B0340080203 +:108600008021490008401843802188434021884345 +:108610002021884310218843042188430190019838 +:10862000A861E08AC005C00D0D99B64B0B4001044E +:10863000B54808401843217EB44B03400902FF208F +:108640000002084018437F23617E19407F26B04313 +:108650000143A07E1840C901C90940060843E861EA +:10866000207D012853D11F20A18B01400C9FA84ED3 +:108670003E400F04F821490339403143A67F304082 +:10868000A44E0E400102F8204001084030430F2163 +:10869000E67F0E40F027B8433701F0263E40064300 +:1086A0002020205C01400F20864331432220205CA3 +:1086B000012811D1012610E0FFFF80FFFCFF000020 +:1086C000FFF8FFFFFFFFFF80B0A40010FFFFFFC017 +:1086D000FFFFC0FFFFFFFFBF00268F480840F105E6 +:1086E0008026360431400143E9602320215C194093 +:1086F0000B9E8A4830400E02FE21C90131400143E1 +:108700002420205C03407F2081430B43AB64032083 +:108710003C21635C03400A993026B1431E0130239B +:1087200033400B433D21615C08400C218B438100A9 +:108730000C20084018433E21615C012901D1012130 +:1087400000E000210223984349000223194001431D +:108750003F20205C012801D1012000E000200126FB +:108760000123994330400843802168501498000C3D +:1087700000048421695889088904890C89004018FB +:108780008018002105E0E36A8F00DB598F00C35198 +:10879000491C2823E35C9942F5D31498000C00048B +:1087A0008821695889088904890C89004018821831 +:1087B000002308E0606BD900401803C8DF00D71918 +:1087C00003C7083F5B1C3020205C8342F2D38448FF +:1087D00003990140834A0A4083491140834A0A4071 +:1087E00083491140834A0A404B491140824A0A405A +:1087F00082491140824A0A40802109031143814A7B +:108800000A4081491140814A0A40814911403A4A4F +:108810000A4080491140804A0A4080491140804AFC +:108820000A4036491140364B0B4080218B43402192 +:10883000C9430B402022D243134010271F43082373 +:108840009F430423DB431F400224E4432740374374 +:108850000397039F6F65029F384002900298614F13 +:10886000074061483840614F074061483840614FD8 +:10887000074029483840604F074060483840604F03 +:108880000740684838405F4F07405F4838405F4FB7 +:1088900007405F483840184F07405E4838405E4FF9 +:1088A00007405E4838405E4F074014483840144F38 +:1088B00007408020874339400A4010208243082027 +:1088C00082431340234001208343AB650798304324 +:1088D0000440EC651498FFF79EFA4000B04900226E +:1088E0000A52019801218843A861A86930400028F4 +:1088F000FBD1002015B0F0BDFFBFFFFFFFFDFFFF64 +:10890000FFFEFFFFFFFF00FE0000FF01FF00FFFF73 +:10891000FFFFE0FFFFE0FFFFFFFF7FFFFF80FFFFA4 +:10892000FEB50700002F01D19E4857E03800FFF741 +:10893000EFF9002801D19B4850E000240460446016 +:10894000C46004613D000120A861A96901400029BB +:10895000FBD00326AE6169463800FFF76FF90028A7 +:1089600001D090483AE084202C5088202C508E482A +:1089700090216850BC202C50A0202C50B0202C50AE +:10898000C02040003C50AC202C50C8202C50F0207F +:108990002C50C0202C50380C00046946098840181F +:1089A00080214902401801998A08210002E00460F0 +:1089B000001D491C9142FAD3AE617C48E8617C48B5 +:1089C000E860AC6480202C506C65AC65EC653800C8 +:1089D000FFF721FA400072490C520020FEBD000052 +:1089E000FFFFFFDFFFFFFFEFFFFFFFF7FFFFFFFBD3 +:1089F000FFFFFFFDFFFFFFFEFFFFBFFFFFFFDFFFEA +:108A0000FFFFEFFFFFFFFBFFFFFFFDFFFFFFFEFF8D +:108A1000FF7FFFFFFFDFFFFFFFEFFFFFFFF7FFFF1E +:108A2000FFFBFFFFFFFFF7FFF8B50600002E02D0A7 +:108A30001400002C01D15B48B0E068460170684624 +:108A40000078202801DB5748A8E03000FFF760F9E4 +:108A5000002801D15348A1E037000125B86928401A +:108A6000002808D1CC20395868460078C140080059 +:108A7000C007C00F00E02800C0B2002801D049485C +:108A80008CE0684601783000FFF775F9002801D1C5 +:108A9000444883E001000A68464B13400B60217A8A +:108AA000012901D12B0000E0002302001668424991 +:108AB00031409B078026F60533400B431360016865 +:108AC000890F2940002902D161688A0400E06268A8 +:108AD00001000B685B0F5B07D200D2081A430A60E3 +:108AE000011D0A68354B13400B60011D0A68120214 +:108AF000120A0A604C21615C0B071B0F011D0E68F6 +:108B00002F4A32401B04F02636031E4016430E60E7 +:108B10002178012901D12B0000E00023021D1668F5 +:108B2000284931405B058026B60333400B43136070 +:108B30002178012901D12B0000E00023021D1668D5 +:108B4000214931401B058026760333400B431360D7 +:108B50004C21615C082901DA002102E04C21615CB2 +:108B600008391A4A89B2515C00220AE013009BB20C +:108B70009B00E318DB681600B6B2B6008619B36036 +:108B8000521C13000E00F6B29BB2B6B2B342EDD344 +:108B9000684600788540D0203D500020F2BD00009E +:108BA000741A002801006200FFFFFF1F030A00067D +:108BB000330A0000FFFFFFDFFFFFFFBFFFFF7FFF64 +:108BC000FFFFF0FFFFFFDFFFFFFFEFFFB0A400108C +:108BD00010B50200002A03D00800C0B2202801DB33 +:108BE000002021E000200123CC241459CC401C405B +:108BF000002C09D0D4201058C8401840002801D1BA +:108C0000180010E003200EE0D8241459CC401C407A +:108C1000002C01D0022006E0DC241259CA40134087 +:108C2000002B00D00420C0B210BD0000F8B584B005 +:108C3000FEF7E1FFBA490127086803229043384351 +:108C40000860B8480A6802400A60B7490A68032306 +:108C50009A433A430A600A6802400A60B3490A68C4 +:108C600003239A433A430A600A6802400A60B04903 +:108C70000A6803239A433A430A600A68104008606E +:108C8000AC4801680F22914339430160AA4801684A +:108C9000072291430160A9480168072291430422F9 +:108CA0000A4302600168F0229143C0220A43026035 +:108CB0000168C02292000A4302600168C0221201CA +:108CC0000A43026001689E4A0A408021490111431B +:108CD0000160E1480168802212060A430260DF4811 +:108CE000016839400029FAD0016849083940002953 +:108CF000FAD0DB480168DB4A0A400260DA480168C2 +:108D0000D84A0A408021490011430160D7480168D0 +:108D1000D44A0A408021490011430160D4480168C7 +:108D2000D04A0A400260D3480168072291430222D8 +:108D30000A430260D0480168490049080160CF48F1 +:108D40000168890A890229220A4302600168CC4A23 +:108D50000A4002600168802252040A4302600168EE +:108D6000C84A0A4002600168802212060A43026073 +:108D7000C5480168C54A0A408021C9001143016005 +:108D80000168FF229143AF220A43026080218902D9 +:108D90000268BF4B13400B430360802292040368B8 +:108DA000BC4C1C4014430460BB4B04681C40046072 +:108DB000046880252D0625430560B848046807250A +:108DC000AC43022525430560B5480468240E2406FB +:108DD000B44D254305600468B34D254080246405E7 +:108DE0002C430460046880252D0625430560AF48A8 +:108DF0000468640064080460AD4C8025AD002668FA +:108E0000A2483040284320602068FF26B0436226F5 +:108E10000643266026689E4830408026F6020643B8 +:108E2000266026689B483040024322602068184034 +:108E30002060206880221206024322609D4802685A +:108E40000724A2430224144304609B4802687F2441 +:108E5000A24328241443046002688C4C14402C4321 +:108E6000046002688A4C1440214301600168894A09 +:108E70000A400260914902680A40026002681A4092 +:108E80000260026880242406144304608C4802684F +:108E90000724A243022414430460B04802687F24DC +:108EA000A2431E24144304600268784C14402543F6 +:108EB00005600268764C1440C02292022243026090 +:108EC0000268744C1440046002681140016001683B +:108ED0000B4003600168802212060A430260A0482A +:108EE000006838400028FAD09E48006838400028C2 +:108EF000FAD09D48006838400028FAD09B480068A6 +:108F000038400028FAD09A4801680722914301604E +:108F1000684698491022FBF727F9002635E000BF84 +:108F2000C4132040FFFCFFFF0013204080132040AB +:108F3000A013204000002440001526404415264080 +:108F4000FF8FFFFF32200021FBF7B8FA300040B25C +:108F500080002818016830229143220052B2120189 +:108F6000302313400B430360641E200040B2694667 +:108F7000320052B25200895C00B209B28842E1DA92 +:108F800032200021FBF79AFA761C300040B2082804 +:108F90003FD2300040B204280DD1EFF310840020FE +:108FA00080F31088FA239B0000220121102000F09A +:108FB0000BFA84F31088704D300040B280002818FE +:108FC00001680F2291436A4633005BB25B00D218FE +:108FD00052781207120F0A430260300040B280003C +:108FE0002818016830220A430260300040B2800035 +:108FF00028180168802252000A430260300040B203 +:10900000800028180168802212060A4302600224A8 +:10901000ABE75A48134A016811400160F821C9437F +:1090200003680B400360564803681A4002600268F8 +:1090300011400160FEF7C0FD52480168802212060F +:109040000A43026001680F430760FEF7AAFD00F0C3 +:1090500039F805B0F0BD00BF1C1526402415264088 +:1090600010102040FF00FFFF081020400C1020408F +:1090700020032040041226400419264008192640E7 +:10908000FFFFF8FFFFFFFFEF00192640FFE0FFFFA3 +:10909000FFFFE0FFFFFFFFF9FFFFFFCF08122640B1 +:1090A00014192640F1D24D00FFFFFF8F18192640FA +:1090B000101926400C12264000162640FFFFFFF72D +:1090C0001012264038B586B068462F491822FBF7A3 +:1090D0004BF86846FBF7D2FEC0B200280ED02B4CEE +:1090E00018222B492000FCF75FF90020E075FF20D3 +:1090F000A830284908600020FCF7B6F9264D280062 +:10910000FCF74EF9C0B200280ED0204C182220499E +:109110002000FCF749F90020E075FF20AA301D4926 +:1091200008600020FCF7A0F91C4C2868206020682B +:109130001B4940181B49FAF723FA1B4908601B4DCD +:1091400020681B494018FA218900FAF719FA2860AB +:109150002868C0031749086007B030BD04162640D0 +:109160000C1926401C19264040162640441626405D +:109170001412264084A4001040122640080020400B +:1091800008042040081526401CA400105C1A002882 +:10919000609F00101C1B0028681B00286C1B002807 +:1091A0003F420F0040420F00741B0028701B002834 +:1091B000E7030000781B0028F3B595B017000021E5 +:1091C0000D000800684601801C00A4B2A40820001D +:1091D00080B280218900884203D30125684604803B +:1091E0007E4C1C9E00F011F9002800D099E06846E2 +:1091F000877016A8077801A81021FBF7EDF90198F0 +:10920000022188433100C9B24900022211400143C2 +:109210000191019804218843FFB2B9000422114052 +:109220000143019101980821884369468978C90062 +:1092300008221140014301910198102188430190B7 +:109240000198202188430190019840218843019092 +:109250000198802101430191614F0198384015A97F +:1092600009780902F82252010A400243019201984A +:109270005C490140019101980002000AA8214905BA +:109280000143019111A81021FBF7A6F91198800A5A +:109290008002A4B2A105890D0143119152480068D2 +:1092A000002806D101980740D020400138430190A2 +:1092B00007E0002805D0019807408020400138438E +:1092C0000190002211A901A800F013F9EDB2002DC0 +:1092D00003D06846008800F08DF809A81021FBF73C +:1092E0007BF9099802218843F6B2710002221140ED +:1092F0000143099109983D490140099109983C4968 +:109300000140802040020843099009980002000AA9 +:109310008821490501430991002109A800F092F82C +:1093200050E000F07DF80128FBD000F073F8012830 +:10933000F7D10DA81021FBF74FF90DA80078FD21FA +:1093400001400DA801700DA800780C2101430DA863 +:1093500001700D980002000AB021490501430D91EA +:1093600000210DA800F06EF800F054F8002829D173 +:10937000EDB2002D03D06846008800F03BF805A848 +:109380001021FBF729F9059802218843F6B27100F4 +:1093900002221140014305910598144901400591AD +:1093A0000598134901408020400208430590059824 +:1093B0000002000A8821490501430591002105A802 +:1093C00000F040F817B0F0BD1CB5012400949BB22A +:1093D000D2B2C9B2C0B2FFF7EFFE13BDFF01000069 +:1093E000FFE0FFFFFF1FFFFF30070017FF00FFFF39 +:1093F000FFFF00FF0D490A680D4B134080B20005C6 +:109400000C4A02401A430A60704708480068C00FBF +:10941000C0B27047084801680800C007C00F704715 +:1094200005480068C00FC0B27047000028102640F1 +:10943000FFFF0FC00000F03F2C102640C069C00F96 +:10944000C0B27047F8B502000E00002A01D1052015 +:109450004EE00F2069490B681B0A0340002B01D125 +:10946000002007E00968090A08400E2801D1012000 +:1094700000E00220624940010F18624C624D280052 +:1094800011001022FAF78AFE28680121884328601B +:1094900010212800FCF70BF85C4B2A000121380052 +:1094A00000F0CFF8002804D0012021E0641E002C39 +:1094B00015D03800FFF7C2FF0028F7D1002E08D0E2 +:1094C00010212800FBF7F2FF300029001022FAF7E4 +:1094D00065FE00F079F8012803D1032008E002209E +:1094E00006E000F071F8002801D1002000E004201F +:1094F000F2BDFEB503001600002B03D001910198C8 +:10950000002801D105205DE00F203C490A68120ABD +:109510000240002A01D1002007E00968090A08403A +:109520000E2801D1012000E0022035494001081831 +:109530000090374C344D280019001022FAF72EFE07 +:10954000344F380001991022FAF728FE28680121CB +:10955000884328603868012188433860102128003A +:10956000FBF7A5FF10213800FBF7A1FF3B002A0005 +:109570000121009800F065F8002804D0012021E0C6 +:10958000641E002C15D00098FFF758FF0028F7D173 +:10959000002E08D010212800FBF788FF300029009A +:1095A0001022FAF7FBFD00F00FF8012803D1032089 +:1095B00008E0022006E000F007F8002801D10020B2 +:1095C00000E00420FEBD000010B50F4C102120006B +:1095D000FBF76CFF2068000F0007A02109068842F6 +:1095E00001D1002009E02068000F0007F0210906E2 +:1095F000884201D1012000E0022010BD000020407F +:1096000000002240FFFFFF0FA01A0028181B0028AF +:10961000FFFFFF00C01A0028174A1268120C12043C +:1096200089B2114381607047C16070470161704722 +:1096300000B50068C00F002801D1104800E00020EC +:1096400000BDF8B507000C0015001E003800FFF73C +:10965000EFFF00280DD129003800FFF7E5FF3100AA +:109660003800FFF7E3FF21003800FFF7D5FF0020A7 +:1096700000E00248F2BD000060A5001001008A0071 +:1096800000B5010009B2002905D40121C006C00EB1 +:1096900081408F48016000BDF0B5020012B2002A7F +:1096A00018D418228B4B040024B2A408A4001C591F +:1096B000FF250600F6001640B540AC438901C9B24B +:1096C0000500ED002A409140214300B2800880004F +:1096D00019501CE00F22804C182503001BB21340C8 +:1096E000083B9B089B00060036B23240083A9208BD +:1096F0009200A258FF260700FF002F40BE40B24351 +:109700008901C9B2C0000540A9401143E150F0BD34 +:1097100080B500F012F87148714902680A40026091 +:10972000704802680A4002606F4802681140016098 +:1097300000F092F800F0A9F801BDFEB568466B494B +:109740000CC90CC008390838002630000AE06A4607 +:109750000300DBB2DB00D2185268521C1140002912 +:109760000ED1401C0100C9B200290AD107216A4666 +:109770000300DBB2DB00D2580A40002AE7D0FEE744 +:10978000FEE75B4C072021680140002900D0FEE77E +:10979000584F3968491C0840002800D0FEE73500C2 +:1097A00024E068462900C9B2C900401841686846EB +:1097B0002A00D2B2D200805800F044F815E0300000 +:1097C0000028EED02168491E68462A00D2B2D20095 +:1097D000805800F037F868462900C9B2C90040181F +:1097E00041683868401C00F02DF86D1C2800C0B29C +:1097F000002826D168462900C9B2C900405800286F +:1098000014D0206869462A00D2B2D2008958884212 +:109810000AD368462900C9B2C900401840682168C7 +:10982000884201D30120CBE73000C9E76846290010 +:10983000C9B2C9004018406821688842BFD30120DE +:10984000BEE7F7BD80B50022002302E00CC0083857 +:1098500008308842FAD301BD30B52748800827492F +:10986000274A002308E01C00E4B2A40014591D009C +:10987000EDB2AD004C515B1C1C000500E4B2EDB232 +:10988000AC42F0D31F48016030BD10B51023112148 +:109890001A4800221C00E4B2A4001459DBB29B0059 +:1098A000C4500B00DBB29B00D258C9B289004250B1 +:1098B00001210020FFF7F0FE00210120FFF7ECFE60 +:1098C0000020FFF7DDFE0120FFF7DAFE10BD0000EB +:1098D00000E100E000E400E01CED00E000132040A7 +:1098E000FFFFF7FF80132040A0132040C8A4001002 +:1098F000589F00105C9F001080000000001C002892 +:109900000000001008ED00E000B5010009B20029D8 +:1099100005D40121C006C00E81404A48016000BD47 +:1099200000B5FEE780B54848006848480068C10FA8 +:10993000002905D046498005800D800008588047E1 +:109940000020FFF7E1FF01BD80B5424800684248B2 +:109950000068C10F002905D03D498005800D8000B9 +:10996000085880470120FFF7CFFF01BD80B53B4875 +:1099700000683B480068C10F002905D034498005C4 +:10998000800D8000085880470220FFF7BDFF01BD11 +:1099900080B53448006834480068C10F002905D0FC +:1099A0002B498005800D8000085880470320FFF771 +:1099B000ABFF01BD80B52D4800682D480068C10F80 +:1099C000002905D022498005800D80000858804775 +:1099D0000420FFF799FF01BD80B52648006826489E +:1099E0000068C10F002905D019498005800D80004D +:1099F000085880470520FFF787FF01BD80B51F4845 +:109A000000681F480068C10F002905D01049800573 +:109A1000800D8000085880470620FFF775FF01BDC4 +:109A200080B51848006818480068C10F002905D0A3 +:109A300007498005800D8000085880470720FFF700 +:109A400063FF01BD80E200E040A50010001120404E +:109A50004809002844A500100411204048A5001022 +:109A6000081120404CA500100C11204050A50010FA +:109A70001011204054A500101411204058A50010CA +:109A8000181120405CA500101C11204004A30010F8 +:109A900034A40010800231400400000008000000DF +:109AA000800658403A0100000009314002000000E1 +:109AB0000A0F0D07E0010000FC5300009821000090 +:109AC000F4010000E8030000F401000090A300107E +:109AD00044A400108002314005000000080000008E +:109AE000000758403B010000000531400100000024 +:109AF0000AF0D07076010000E0150000D4170000D5 +:109B00005E010000E8030000F4010000ACA30010B7 +:109B100054A40010000431400000000008000000C0 +:109B2000800958404001000000073140000000005B +:109B3000090F0D0795030000901A0000282300006C +:109B4000F4010000E8030000F4010000C8A30010C5 +:109B500064A40010800031400000000008000000F4 +:109B600000065840390100000006314005000000A1 +:109B700009F0D07087020000201C0000982100002E +:109B8000F4010000D0070000F401000080B5F7F7F1 +:109B90008BFB00F07DF800F0B2F8FDF7CFFBFA2068 +:109BA0008000FAF7CBFCF2E710B500F071F800F096 +:109BB000A6F8FFF73BF8F7F777FB00F069F8002805 +:109BC00003D000F09CF8002801D1FFF7DFFF1C4C08 +:109BD00000F0C6F8206000F0C6F82068FDF7D8FA5B +:109BE000FDF7EAFB002801D0FFF7D0FF62B6154C65 +:109BF000F9F72EFB20602068002804D00120FDF733 +:109C0000ABFBFFF7C3FF0120F9F735F8FCF744FC85 +:109C1000002801D1FFF7BAFFFCF78EFCFDF7CCFB63 +:109C2000002805D0F7F740FB00F032F800F067F8A5 +:109C3000FDF784FBFA208000FAF780FCECE70000D7 +:109C4000201B00282812002800B5104A8018C009DF +:109C50000001214A8018042905D20068C900C840C3 +:109C6000C006C00E05E0FC314068C900C840C0060F +:109C7000C00EC0B200BD00B5002A03D101228A4047 +:109C8000426002E001228A40826000BD0000CFBF36 +:109C900078B58DB0684611493422FAF765FA104E4E +:109CA0006A4607213000F9F72CFC04000022072146 +:109CB0003000FFF7E0FF00250A480560002C07D1BF +:109CC00007213000FFF7C0FF002801D1012000E08C +:109CD0002800C0B20EB070BD0000304064A100107A +:109CE00000083140F01A002800B5002A03D10122F3 +:109CF0008A40426002E001228A40826000BDF0B5E5 +:109D00008DB0684614493422FAF72EFA134F6A468A +:109D100001213800F9F7F5FB060000242000694610 +:109D200008726A4602213800F9F7EBFB05000022B1 +:109D300001213800FFF7D8FF002202213800FFF789 +:109D4000D3FF3543002D01D1012000E02000C0B237 +:109D50000DB0F0BD0000000098A10010800531405A +:109D60000648006870470020C04304490860044961 +:109D70000860002003490860704700000018264072 +:109D80000418264008102640F1B4016841184368C1 +:109D900058080A1800988068DB0701D54B46C018A0 +:109DA00091422DD00D78491C032403262E4002D168 +:109DB0000E78F61C491C2B090F2B07D10B780F339B +:109DC000491C03E00F78491C0770401C761EF9D12E +:109DD000002BE5D00E78491CAD082C40032C01D196 +:109DE0000C78491C24023419654244190025AE1E22 +:109DF0009E42D5D02678641C0670401C6D1CF6E788 +:109E000000980C30F2BC704700053140060000009D +:109E10000000000080189040020000000006314061 +:109E20000100000000000000401990400500000003 +:109E3000000531400700000000000000C0189040FD +:109E40000300000000053140050000000000000094 +:109E50004018904001000000000631400200000060 +:109E6000000000008019904006000000000631400C +:109E70000600000000000000801A90400A00000068 +:109E800004000000000000000000000000000000CE +:109E90000100030363000000000000000000000058 +:109EA0003200000000000000000000000000000080 +:109EB00000000000000000000000000000000000A2 +:109EC000020300000000000003000000000000008A +:109ED000030000000000000003000000000000007C +:109EE000030000000000000003000000010000006B +:109EF0000102000072B6104810490860BFF34F8F8E +:109F00000120C0040E490A6802430A600D490A682C +:109F100002430A600C490A6802430A6000200021DB +:109F20000D4A03C26A45FCD1084FB847084FB847ED +:109F3000084FB847FEE700000000001008ED00E001 +:109F40000013204080132040A013204011970010E0 +:109F50009DA2001001A4001000FF0128FFFF0128AE +:109F6000443A5C50726F6A6563745C3531315C4EA3 +:109F7000375765625C6669726D776172655C7432D1 +:109F8000675C747669696268386D5C7372635C7370 +:109F9000797374656D5C7265765F645C73797374F4 +:109FA000656D5F747669696268386D5F636D307086 +:109FB0006C75732E630000009F0F0000000000000E +:109FC000000000000000000000000000000003008E +:109FD000000000000000000000000000000300007E +:109FE000000000000300000000000000030000006B +:109FF000000000000300000000000000030000005B +:10A000000100000000000000443A5C50726F6A6575 +:10A0100063745C3531315C4E375765625C666972DA +:10A020006D776172655C7432675C747669696268C9 +:10A03000386D5C7372635C647269766572735C73AD +:10A040007973636C6B5C63795F737973636C6B2E8C +:10A0500068000000443A5C50726F6A6563745C3556 +:10A0600031315C4E375765625C6669726D7761723B +:10A07000655C7432675C747669696268386D5C73BC +:10A0800072635C647269766572735C737973636C16 +:10A090006B5C63795F737973636C6B2E6300000094 +:10A0A000443A5C50726F6A6563745C3531315C4E62 +:10A0B000375765625C6669726D776172655C743290 +:10A0C000675C636F6D6D6F6E5C7372635C64726905 +:10A0D000766572735C7379737469636B5C63795FC3 +:10A0E0007379737469636B2E6300000001000000D4 +:10A0F0000000000000000000100000001000000040 +:10A10000100000000100000100000000000000003D +:10A11000000000000000000000000000000000003F +:10A12000000000000000000070B40123002413E0D0 +:10A130000168001D194202D04D466918491E0C6085 +:10A14000091D121F042AFAD20D00960701D50C80B2 +:10A15000AD1C1A4000D02C700268001D002AE7D107 +:10A1600070BC704700000000060000000000000006 +:10A1700000000000000000000000000000000000DF +:10A1800000000000000000000000000000000000CF +:10A1900000000000000000000000000006000000B9 +:10A1A00000000000000000000000000000000000AF +:10A1B000000000000000000000000000000000009F +:10A1C000000000000000000000000000000000008F +:10A1D0000800000015000000000000000000000062 +:10A1E000000000000000000000000000000000006F +:10A1F000000000000000000000000000000000005F +:10A20000010000000E00000015000000000000002A +:10A21000000000000000000000000000000000003E +:10A22000000000000000000000000000000000002E +:10A230000000000001000000060000000000000017 +:10A24000000000000000000000000000000000000E +:10A2500000000000000000000000000000000000FE +:10A2600000000000000000000000000006000000E8 +:10A2700000000000000000000000000000000000DE +:10A2800000000000000000000000000000000000CE +:10A2900000000000000000000000000010B50749A9 +:10A2A00079441831064C7C44163404E0081D0A68D1 +:10A2B000891888470100A142F8D110BD08000000AC +:10A2C0002400000065FEFFFFA8080000001300281E +:10A2D00000000000B5FAFFFFFE0200002A010000A6 +:10A2E0000008002800000301000000000100000039 +:10A2F000000000000000000001000000FF0F01004E +:10A30000000000004D4F544F525F494E325F4842AB +:10A31000375F4842385F484F52495A4F4E54414C7C +:10A320000000000004207146084202D0EFF30980CB +:10A3300001E0EFF3088000B50149884700BD000047 +:10A340006F54001000F00EF8002801D0FFF7A6FFB0 +:10A3500000BF00BF002000BF00BFFFF725FC00F0DA +:10A3600003F800000020704780B500F001F801BD3F +:10A370000746384600F002F8FBE7000080B500BF52 +:10A3800000BF024A11001820ABBEFBE72600020006 +:10A390004D4F544F525F494E335F4842355F4842FC +:10A3A000365F534541545F50414E00004D4F544F6E +:10A3B000525F494E345F4842335F4842345F424106 +:10A3C000434B5245535400004D4F544F525F494E3A +:10A3D000315F4842315F4842325F4C4547524553F6 +:10A3E0005400000004207146084201D0EFF30980B8 +:10A3F000EFF3088000B50149884700BD95A400101F +:10A400000548014603B4684680F3098800BF00BFD1 +:10A4100000BF00BFFFF796FFA5EDF5FE00127A0022 +:10A42000000000000024F400008000000080000014 +:10A4300000800000484F52495A4F4E54414C5F48EB +:10A44000414C4C00534541545F50414E5F48414C94 +:10A450004C0000004241434B524553545F48414C2D +:10A460004C0000004C4547524553545F48414C4C0A +:10A470000000000030750000204E00003075000024 +:10A48000A8610000000300010004010302020002B1 +:10A49000000300058169891E0978C9B2016070470F +:10A4A00000000000000101010101010000010000A5 +:10A4B0000203040506080C10040300011314001520 +:10A4C000C70103000100000000080028FFFF012869 +:10A4D0000003000B0000000000000000000000006E +:10A4E000000000000000000000000000000000006C +:10A4F000000000000000000000000000000000005C +:10A50000000000000000000000000000000000004B +:10A51000000000000000000000000000000000003B +:10A52000000000000000000000000000000000002B +:10A53000000000000000000000000000000000001B +:10A54000000000000000000000000000000000000B +:10A5500000000000000000000000000000000000FB +:10A5600000000000000000000000000000000000EB +:10A5700000000000000000000000000000000000DB +:10A5800000000000000000000000000000000000CB +:10A5900000000000000000000000000000000000BB +:10A5A000000000000000000000010203FEE70000C0 +:10A5B000FEE70000FEE70000FEE70000FEE7000007 +:10A5C000FEE70000FEE70000FEE70000FEE70000F7 +:10A5D000FEE70000FEE740045500020001000150C4 +:10A5E00003F9790010000123B17A0CA004090004DA +:10A5F00001010001210E83D0A4286004FFFFFF1F8A +:10A60000020225920807F0010352400001F20602FF +:10A6100018F2060418F2060618F2060818F30600E7 +:10A620005478F10618F2060418F2060618F2050826 +:10A6300018F002FF2199001004F1FF04F1FF04F16A +:10A64000FF04F1FF04F1FF04F1FF04F1FF04F147FF +:10A65000046C0248090028060AF321060001F123D0 +:10A6600034F25208680002010F0F010000000000E0 +:10A6700000000000000000000000000000000000DA +:10A6800000000000000000000000000000000000CA +:10A6900000000000000000000000000000000000BA +:10A6A00000000000000000000000000000000000AA +:10A6B000000000000000000000000000000000009A +:10A6C000000000000000000000000000000000008A +:10A6D000000000000000000000000000000000007A +:10A6E000000000000000000000000000000000006A +:10A6F000000000000000000000000000000000005A +:10A700000000000000000000000000000000000049 +:10A710000000000000000000000000000000000039 +:10A720000000000000000000000000000000000029 +:10A730000000000000000000000000000000000019 +:10A740000000000000000000000000000000000009 +:10A7500000000000000000000000000000000000F9 +:10A7600000000000000000000000000000000000E9 +:10A7700000000000000000000000000000000000D9 +:10A7800000000000000000000000000000000000C9 +:10A7900000000000000000000000000000000000B9 +:10A7A00000000000000000000000000000000000A9 +:10A7B0000000000000000000000000000000000099 +:10A7C0000000000000000000000000000000000089 +:10A7D0000000000000000000000000000000000079 +:08A7E000000000000000000071 +:040000051000A40142 +:00000001FF diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.out b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.out new file mode 100644 index 0000000..6a381a7 Binary files /dev/null and b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.out differ diff --git a/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Tests/audit_timed_only.py b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Tests/audit_timed_only.py new file mode 100644 index 0000000..b304854 --- /dev/null +++ b/firmware/t2g/Passenger_Seat_Timed_Controller_demo/Tests/audit_timed_only.py @@ -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", + "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.*?)\};", + 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) diff --git a/firmware/t2g/README.md b/firmware/t2g/README.md index 8b41c88..63a8505 100755 --- a/firmware/t2g/README.md +++ b/firmware/t2g/README.md @@ -2,7 +2,14 @@ `Seat_Controller_demo/` is the repository-owned IAR project used by the real seat-bench executor. It contains the 2026-07-30 heater, ventilation, four-axis -motion, SMV8 position memory, Hall feedback and ADC current-diagnostic code. +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: @@ -10,7 +17,8 @@ The matching Cypress 7.9.0 build dependencies are vendored beside it: firmware/t2g/ ├── common/ ├── tviibh8m/ -└── Seat_Controller_demo/ +├── Seat_Controller_demo/ +└── Passenger_Seat_Timed_Controller_demo/ ``` This preserves the IAR project's `../../common` and `../../tviibh8m` paths and diff --git a/firmware/t2g/Seat_Controller_demo/App/main_cm0plus.c b/firmware/t2g/Seat_Controller_demo/App/main_cm0plus.c index fea17aa..eb0f81e 100755 --- a/firmware/t2g/Seat_Controller_demo/App/main_cm0plus.c +++ b/firmware/t2g/Seat_Controller_demo/App/main_cm0plus.c @@ -13,6 +13,7 @@ #include "bsp_seat_backrest_heater.h" #include "bsp_seat_fan.h" #include "seat_controller_thermal.h" +#include "seat_feature_profile.h" volatile uint32_t g_seatMoveResetReason = 0u; volatile uint32_t g_seatMoveInitResult = MOTOR_API_FAILURE; @@ -59,11 +60,13 @@ int main(void) } Motor_SetWatchdogServiceEnabled(true); +#if SEAT_FEATURE_THERMAL if(!SeatControllerThermal_Init()) { SafetyManager_LatchFault(SAFETY_FAULT_DRIVER); SeatMove_HoldSafeOff(); } +#endif if(!SeatMoveApplication_Init()) { SeatMove_HoldSafeOff(); @@ -72,7 +75,9 @@ int main(void) for(;;) { SeatMoveApplication_Service(); +#if SEAT_FEATURE_THERMAL SeatControllerThermal_Service1ms(); +#endif if(SafetyManager_IsInhibited()) { Motor_ForceSafeOff(); diff --git a/firmware/t2g/Seat_Controller_demo/App/seat_controller_thermal.c b/firmware/t2g/Seat_Controller_demo/App/seat_controller_thermal.c index 55f150f..57ee54b 100755 --- a/firmware/t2g/Seat_Controller_demo/App/seat_controller_thermal.c +++ b/firmware/t2g/Seat_Controller_demo/App/seat_controller_thermal.c @@ -7,6 +7,7 @@ #include "seat_backrest_heater.h" #include "seat_backrest_heater_config.h" #include "seat_fan_config.h" +#include "seat_feature_profile.h" #define SEAT_CONTROLLER_THERMAL_CONTROL_ID (0x29Eu) #define SEAT_CONTROLLER_THERMAL_CONFIG_ID (0x2AAu) @@ -247,6 +248,7 @@ static void SeatControllerThermal_HandleControl29e( fanGear = (uint8_t)((frame->data0 >> 16u) & 0x0Fu); heaterGear = (uint8_t)((frame->data0 >> 20u) & 0x0Fu); +#if SEAT_FEATURE_VENTILATION if(fanOn == 2u) { SeatControllerThermal_StopFan(1u); @@ -266,7 +268,14 @@ static void SeatControllerThermal_HandleControl29e( g_seatControllerThermalInvalidCount++; SeatControllerThermal_StopFan(0u); } +#else + (void)fanOn; + (void)fanMode; + (void)fanGear; + SeatControllerThermal_StopFan(1u); +#endif +#if SEAT_FEATURE_HEATING if(heaterOn == 2u) { SeatControllerThermal_StopHeater(1u); @@ -286,6 +295,11 @@ static void SeatControllerThermal_HandleControl29e( g_seatControllerThermalInvalidCount++; SeatControllerThermal_StopHeater(0u); } +#else + (void)heaterOn; + (void)heaterGear; + SeatControllerThermal_StopHeater(1u); +#endif } static void SeatControllerThermal_HandleConfig2aa( @@ -308,16 +322,23 @@ static void SeatControllerThermal_HandleConfig2aa( g_seatControllerThermalInvalidCount++; return; } +#if SEAT_FEATURE_VENTILATION s_fanConfiguredMaxGear = (fanConfig == 1u) ? 1u : ((fanConfig == 2u) ? 2u : 3u); +#endif +#if SEAT_FEATURE_HEATING s_heaterConfiguredMaxLevel = (heaterConfig == 1u) ? 1u : ((heaterConfig == 2u) ? 2u : 3u); +#endif +#if SEAT_FEATURE_VENTILATION if(s_fanRunning && !SeatControllerThermal_RequestFan(s_fanLastVehicleGear)) { g_seatControllerThermalOutputFaultStage = 4u; SeatControllerThermal_LatchOutputFault(); } +#endif +#if SEAT_FEATURE_HEATING if(g_seatBackrestHeaterAppliedLevel != SEAT_BACKREST_HEATER_LEVEL_OFF) { @@ -327,11 +348,13 @@ static void SeatControllerThermal_HandleConfig2aa( SeatControllerThermal_LatchOutputFault(); } } +#endif } static void SeatControllerThermal_HandleMode3ce( const bsp_seat_move_can_frame_t *frame) { +#if SEAT_FEATURE_HEATING uint8_t mode; if(frame->dlc != SEAT_FAN_CAN_DLC) { @@ -347,6 +370,10 @@ static void SeatControllerThermal_HandleMode3ce( g_seatControllerThermalInvalidCount++; SeatControllerThermal_StopHeater(0u); } +#else + (void)frame; + SeatControllerThermal_StopHeater(1u); +#endif } static void SeatControllerThermal_HandleSwitch42a( @@ -363,6 +390,7 @@ static void SeatControllerThermal_HandleSwitch42a( } fanSwitch = (uint8_t)(frame->data0 & 0x03u); heaterSwitch = (uint8_t)((frame->data0 >> 2u) & 0x03u); +#if SEAT_FEATURE_VENTILATION if(fanSwitch == 1u) { if(!SeatControllerThermal_RequestFan(s_fanLastVehicleGear)) @@ -375,6 +403,11 @@ static void SeatControllerThermal_HandleSwitch42a( { SeatControllerThermal_StopFan(1u); } +#else + (void)fanSwitch; + SeatControllerThermal_StopFan(1u); +#endif +#if SEAT_FEATURE_HEATING if(heaterSwitch == 1u) { if(!SeatControllerThermal_RequestHeater(s_heaterLastVehicleGear)) @@ -387,10 +420,15 @@ static void SeatControllerThermal_HandleSwitch42a( { SeatControllerThermal_StopHeater(1u); } +#else + (void)heaterSwitch; + SeatControllerThermal_StopHeater(1u); +#endif } static bool SeatControllerThermal_RequestFan(uint8_t vehicleGear) { +#if SEAT_FEATURE_VENTILATION uint8_t physicalGear = (vehicleGear >= 3u) ? 3u : vehicleGear; if((vehicleGear == 0u) || (vehicleGear > 5u)) { @@ -418,10 +456,15 @@ static bool SeatControllerThermal_RequestFan(uint8_t vehicleGear) return BspSeatFan_SetDutyPercent(s_fanTargetDutyPercent); } return true; +#else + (void)vehicleGear; + return false; +#endif } static bool SeatControllerThermal_RequestHeater(uint8_t vehicleGear) { +#if SEAT_FEATURE_HEATING uint8_t physicalLevel = (vehicleGear >= 3u) ? 3u : vehicleGear; if((vehicleGear == 0u) || (vehicleGear > 5u) || !s_heaterModeValid) { @@ -440,6 +483,10 @@ static bool SeatControllerThermal_RequestHeater(uint8_t vehicleGear) g_seatControllerHeaterFeedbackRaw = physicalLevel + 1u; s_feedbackPending = true; return true; +#else + (void)vehicleGear; + return false; +#endif } static void SeatControllerThermal_StopFan(uint8_t feedbackRaw) diff --git a/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c b/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c index 29a010d..628caff 100755 --- a/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c +++ b/firmware/t2g/Seat_Controller_demo/App/seat_move_application.c @@ -8,6 +8,7 @@ #include "safety_manager.h" #include "seat_controller_thermal.h" #include "seat_dynamic_mode.h" +#include "seat_feature_profile.h" #include "seat_motor_manager.h" #include "seat_move_config.h" #include "seat_move_store.h" @@ -336,7 +337,9 @@ void SeatMoveApplication_Service(void) } else { +#if SEAT_FEATURE_THERMAL SeatControllerThermal_HandleFrame(&frame); +#endif } } SeatMove_ServiceDispatcher(); @@ -363,7 +366,9 @@ void Motor_CalibrationServiceHook(void) } else { +#if SEAT_FEATURE_THERMAL SeatControllerThermal_HandleFrame(&frame); +#endif } } 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_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_STOP_MOTOR: case SEAT_MOVE_OPCODE_STOP_ALL: + break; case SEAT_MOVE_OPCODE_CALIBRATE_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_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; default: *reject = SEAT_MOVE_REJECT_BAD_OPCODE; diff --git a/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h b/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h new file mode 100644 index 0000000..5cd0fa1 --- /dev/null +++ b/firmware/t2g/Seat_Controller_demo/Config/seat_feature_profile.h @@ -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 */ diff --git a/firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp b/firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp index d4ecfd8..dbb3ebb 100755 --- a/firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp +++ b/firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.ewp @@ -2359,6 +2359,9 @@ $PROJ_DIR$\..\Config\seat_move_config.h + + $PROJ_DIR$\..\Config\seat_feature_profile.h + $PROJ_DIR$\..\Config\seat_backrest_heater_config.h diff --git a/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.hex b/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.hex index bdad282..215b93c 100755 --- a/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.hex +++ b/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.hex @@ -1,12 +1,12 @@ :020000041000EA -:100000000000022801EC00103DF4001055F1001032 +:1000000000000228F5EB0010E1F30010F9F00010F9 :1000100000000000000000000000000000000000E0 -:1000200000000000000000000000000015F20010B9 -:10003000000000000000000041F4001045F4001032 +:10002000000000000000000000000000B9F1001016 +:100030000000000000000000E5F30010E9F30010EC :10004000CDE50010F1E5001015E6001039E60010CE :100050005DE6001081E60010A5E60010C9E600107C -:1000600049F400104DF4001051F4001055F4001044 -:1000700059F400105DF4001061F4001065F40010F4 +:10006000EDF30010F1F30010F5F30010F9F30010B8 +:10007000FDF3001001F4001005F4001009F4001065 :1000800000B5010009B2002905D40121C006C00E47 :100090008140C648016000BD00B5010009B20029D9 :1000A00005D40121C006C00E8140C148016000BDD9 @@ -78,7 +78,7 @@ :1004C000060091204000FFF779FFF106C90E009168 :1004D00071090B051B0D5B1E2200D2B20321C0B2B5 :1004E000FFF7F8FED3BD80B51300A74A13600023C1 -:1004F000137203F048FE01BD24F3001000B5C0B232 +:1004F000137203F048FE01BDC8F2001000B5C0B28F :10050000042801D2012000E00020C0B200BD00B5E7 :10051000401A00B2012801DB012000E00020C0B237 :1005200000BD00B5BE490978002901D000201FE0B8 @@ -313,8 +313,8 @@ :101370002000FFF723F801220321C0B2FFF762F833 :10138000884C200003F0A2FE00228749200003F0D1 :1013900063FD2000FEF720FF13BD0000FC1B0028AA -:1013A00034E700108CEB0010C21C00283CEE00104B -:1013B0002CF30010CD100010F2B5060000273C0001 +:1013A00034E7001080EB0010C21C0028B8EC0010DD +:1013B000D0F20010CD100010F2B5060000273C005E :1013C0000098002801D1002077E0380000990880BB :1013D00003F0AEFBCD490978002903D003F0ACFB44 :1013E00000206AE00121C94A117003F0A5FB6D4D90 @@ -343,9 +343,9 @@ :10155000FFF7D5FF002806D068460078E4B2A04225 :1015600001D1012000E00020C0B232BD80B5B44AF4 :1015700000211160017C1172C168806802F003FED5 -:1015800001BD00006012002808F00010000658405D +:1015800001BD000060120028ACEF001000065840BA :1015900080065840000758408009584000B4C404F1 -:1015A0000024F400000064408CEE0010F8B5012621 +:1015A0000024F4000000644030EE0010F8B501267D :1015B0003000002468460470564804700121A14898 :1015C000FEF7B8FDC24F01213800FEF7B3FD9E4D76 :1015D000280003F003FE01219C48FEF7ABFD280024 @@ -1109,12 +1109,12 @@ :104530009BB21B0A9BB2174C14401B02FF221202B3 :104540001A4022430B78052B05D10979C9B2FF2304 :104550009A43114304E04978FF239A43C9B21143B7 -:10456000016300221000F2BD0CF40010FFFFFBFFFE +:10456000016300221000F2BDB0F30010FFFFFBFF5B :10457000FFFFFCFFFFFFFFF8FFFFFFBFFFFBFFFF99 -:10458000FFFDFFFFFFFEFFFFFFCFFFFF10F4001056 -:10459000FFFF00FFFF00FFFF14F4001018F40010ED -:1045A000FFF3FFFFFFFCFFFF1CF4001020F40010DE -:1045B00024F400100168802212060A43026070474A +:10458000FFFDFFFFFFFEFFFFFFCFFFFFB4F30010B3 +:10459000FFFF00FFFF00FFFFB8F30010BCF30010A7 +:1045A000FFF3FFFFFFFCFFFFC0F30010C4F3001098 +:1045B000C8F300100168802212060A4302607047A7 :1045C000016849004908016070474068704701610F :1045D00070470069704781607047006A704700004B :1045E00030B4434351435D18010C0C00130C5C4381 @@ -1169,7 +1169,7 @@ :1048F000142060432818007C787103A914206043B9 :104900002818C06802F0EAFC0028E7D0300002F066 :10491000E1FC002003E0012071490870012019B07A -:10492000F0BD000024F3001000000440FF00FFFF72 +:10492000F0BD0000C8F2001000000440FF00FFFFCF :10493000FF0000FFF8B584B00E00C0B20290002561 :1049400028006946088004212A0003AC200002F0F8 :1049500033FC2C0062480078002806D0674F387876 @@ -1191,16 +1191,16 @@ :104A500001D0002018E0631E042201212800FFF786 :104A6000EFFD002801D000200EE0042201212800E3 :104A7000FFF771FE002801D0002005E0009821001A -:104A8000FFF7C0FB38600120F2BD0000FCF200100F +:104A8000FFF7C0FB38600120F2BD0000A0F200106B :104A90007CB505001D4C062100226E46300002F058 :104AA0008BFB6946280002F011FE6846007800285A :104AB00003D1641E002CF4D101E0012000E00020AD -:104AC00076BD000014F10010E0B511490CC9083999 +:104AC00076BD0000B8F00010E0B511490CC90839F6 :104AD00069460CC10839694602F0A2FD07BD000015 -:104AE000B71C0028901B0028A81B00282CEA0010E7 +:104AE000B71C0028901B0028A81B002820EA0010F3 :104AF000FFC99A3B00CA9A3B00109040B81C00289E :104B00005F8DCB00608DCB0001000100A08601000D -:104B100004F300100000000000000000000000008E +:104B1000A8F20010000000000000000000000000EB :104B2000801C800803D0401C801EFCD100BF704751 :104B3000EFF3108072B6704780F3108870474030F2 :104B4000016810220A430260704740300168082261 @@ -1234,10 +1234,10 @@ :104D0000A54394000C2222402A43C264174A146827 :104D10004C228A5C1A400725AC4322434D240C5D8B :104D200023407024A2431C01702323401343436596 -:104D3000497D816700221000F2BD0000F8F30010E9 +:104D3000497D816700221000F2BD00009CF3001045 :104D4000FFFFFBFFFFFFFCFFFFFFFFF8FFFFFFBFC1 -:104D5000FCF30010FFFF00FFFF00FFFF00F4001056 -:104D600004F40010FFF3FFFFFFFCFFFF08F4001046 +:104D5000A0F30010FFFF00FFFF00FFFFA4F300100F +:104D6000A8F30010FFF3FFFFFFFCFFFFACF3001000 :104D70000168802212060A43026070477830016899 :104D800001220A4302607047C16F0800C007C00FCC :104D90007047010070310A68012313430B60006FF4 @@ -1246,7 +1246,7 @@ :104DC0000B79002B01D00122FFE763005B08D207BB :104DD0001A430B4B0024095F89005A5000E0064833 :104DE00030BD00B5074A1368002B03D0126800B22B -:104DF0008000115000BD000001005600F4F30010C7 +:104DF0008000115000BD00000100560098F3001023 :104E0000008020402412002870B4103A03D378C9DF :104E1000103A78C0FBD2530701D318C918C001D586 :104E200008C908C0D20704D30B880380891C801CE2 @@ -1296,8 +1296,8 @@ :1050E0000021C1239B00C15080239B00C25081221C :1050F00092008150124A8150154A8150114A8150C4 :10510000114A8150134A8150134A81507047000060 -:105110000140AA00DCF30010E0F30010E4F30010FB -:10512000E8F30010FF3FFFFFFFFEFFFFFFFFFFFC64 +:105110000140AA0080F3001084F3001088F300100F +:105120008CF30010FF3FFFFFFFFEFFFFFFFFFFFCC0 :10513000FFFDFFFFFFFBFFFFFFF7FFFF0F00000377 :10514000C80E0000C80F0000880F0000EFCDAB00B4 :10515000880E0000080F0000480F0000F8B504009A @@ -1313,7 +1313,7 @@ :1051F00002F0DAFE0020E075E52040000A49086070 :10520000002002F031FF2C0030681049FEF7FAFF51 :10521000002122000023FFF7E3F9FFF79FFF70BD95 -:10522000181B00288CED0010F01B002810E000E097 +:10522000181B0028A0ED0010F01B002810E000E083 :10523000BE1C0028FFFFFF00010000FFFFFFFFFF73 :1052400014E000E0941C002818E000E040420F0049 :1052500043490A680860704742490A680860704715 @@ -1402,8 +1402,8 @@ :1057800000B5114B1870114B1970002A05D0117813 :105790000F4B197091880F4A1180C0B2002803D0B6 :1057A00001209949087002E0002097490870012003 -:1057B0009649087000BD0000AC1A002834F40010AF -:1057C000D0F2001038F40010D71C0028D81C002894 +:1057B0009649087000BD0000AC1A0028D8F300100C +:1057C00074F20010DCF30010D71C0028D81C00284D :1057D000D91C0028B01C0028F8B50500002D05D004 :1057E0000C00002C02D01600002E01D1002039E060 :1057F0000A2100222700380001F0DEFC68682070D2 @@ -1866,7 +1866,7 @@ :107480009205C02000041040084300998860E08AFB :10749000390C090480B20843218B82B208041043DE :1074A0000099C86021001A31009800F055F801E0F9 -:1074B00020480190019805B0F0BD00004CF3001089 +:1074B00020480190019805B0F0BD0000F0F20010E6 :1074C000FFFEFFFFFFFDFFFFFFFBFFFFFFFFFFDFF3 :1074D0003830016801220A43026070473830016881 :1074E00001229143016070473C30016801220A4348 @@ -1874,8 +1874,8 @@ :1075000014D05B6A1C000C80190F0C00E407E40F18 :107510001470990F0C00E407E40F9470590F0C00DD :10752000E407E40F5470D90FD17000E0014810BD9A -:10753000FFFFFFBF0100DA0050F3001054F300100A -:1075400058F300105CF3001060F30010FFFCFFFF25 +:10753000FFFFFFBF0100DA00F4F20010F8F20010C4 +:10754000FCF2001000F3001004F30010FFFCFFFF3A :10755000FFF7FFFFFFF8FFFFF0B50A000023190057 :107560006F4C2468002A49D05579002D01D001269E :1075700000E01E006B4D2540B4028026F60034402A @@ -1905,8 +1905,8 @@ :1076F000E407E40FCC7053081C00E407E40F4C705F :1077000013001C00E407E40F0C7092081300DB0761 :10771000DB0F8B7000E0014810BD00000100DA00B3 -:1077200064F30010FFFBFFFFFFFDFFFFFFFEFFFF05 -:1077300068F3001000B5012027490A68520802408A +:1077200008F30010FFFBFFFFFFFDFFFFFFFEFFFF61 +:107730000CF3001000B5012027490A6852080240E6 :10774000002A01D0022001E00968084000BD274856 :1077500001680800C007C00F704700B5C21E022AAA :1077600001D3012002E0C01E0860002000BD1CB54E @@ -1929,7 +1929,7 @@ :107870008007800F32BD0000401626400F48006888 :10788000000AC0B270470E480068000AC0B27047D4 :10789000000000000019264010002740181B002897 -:1078A000D4EC00105B080000F01B002840122640BA +:1078A00008ED00105B080000F01B00284012264085 :1078B00004480068000AC0B2704700001010204061 :1078C0000C1020400810204038B50400072C0ED3BF :1078D000124D18221249280000F066FB0020E875BE @@ -1937,7 +1937,7 @@ :1078F000A10040584007400F010009B2042907D1F8 :107900000448A1004058C106C90E80204000084329 :1079100000B232BD0001264000122640181B00288C -:1079200030ED0010F01B00281E4909688A03920BF5 +:1079200054ED0010F01B00281E4909688A03920BD1 :107930000260090E0A00D207D20FC2711749096806 :107940000A00D204D20C82800A00120CD2B2028148 :10795000090CC9B24181264909680F220B00134066 @@ -2030,7 +2030,7 @@ :107EC000002805D002280BD006D303280CD00FE0E1 :107ED00000982860002032BD009840082860F9E72B :107EE000009880082860F5E70098C0082860F1E74E -:107EF000002028600320EEE7181B002830ED00105A +:107EF000002028600320EEE7181B002854ED001036 :107F000063090000F01B0028001626407F090000CE :107F1000001926401CB5040069460020FFF7AFFF9A :107F20000100C9B2002903D000212160C0B209E0DC @@ -2352,7 +2352,7 @@ :1092E0000C482A00D2B2805CFFF724FE002808D088 :1092F00068463221405C002803D020000124AC40A5 :1093000004436D1C2800C0B20428E8D32000C0B27A -:109310000FB030BD28F40010C027090000B5017857 +:109310000FB030BDCCF30010C027090000B50178B4 :109320000A290ED0417804290BD04178062908D0AB :109330004178072905D04178082902D040780928CA :1093400001D1012000E00020C0B200BDF1B590B015 @@ -2378,7 +2378,7 @@ :10948000691848832B895A2270682100C9B24900A3 :10949000415A8B482700FFB2C05DFFF719FC002836 :1094A000E1D1002000E00120F2BD0000FFFF00003C -:1094B0009CF00010781A0028F8B590B00700002F33 +:1094B00040F00010781A0028F8B590B00700002F8F :1094C00004D008003B7BC0B2984201D30020BEE02C :1094D000B8680B00DBB218246343C0180190904EAB :1094E00002207070F17001980078B07032610198BC @@ -2411,13 +2411,13 @@ :10969000641C2000C0B2042810D22B89B27D2000A7 :1096A000C0B240002818418D1E482700FFB2C05D9F :1096B000FFF70EFB0028EBD1002000E00120F2BDF7 -:1096C00028F4001038B5164D2C89641C200080B297 +:1096C000CCF3001038B5164D2C89641C200080B2F4 :1096D000F8F7A0FFF8F750FE200080B2FEF756FF23 :1096E0002C8103206870F9F724FB286131BD38B55F :1096F0000A000B4BD978491C0C00057BE4B2AC4244 :1097000003D30021DC88641CDC80C9B2FFF7D4FEDF :10971000002801D1FFF7D6FF31BD000030750000F1 -:10972000781A002828F40010F8B5C0B0002001F025 +:10972000781A0028CCF30010F8B5C0B0002001F082 :1097300046FD20A9002000F098FA070069460120A4 :1097400000F093FA04003800C0B2022801D10125CC :1097500000E000252000C0B2022801D1012600E06F @@ -2591,7 +2591,7 @@ :10A1D0001500EDB22D015D196D6865601500EDB2D9 :10A1E0002D015D19AD68A5601500EDB22D015B195B :10A1F0001B7B2373521C1300DBB2102B00D30022F5 -:10A200000A70FAF799FC012032BD000024F3001017 +:10A200000A70FAF799FC012032BD0000C8F2001074 :10A2100000000440FF00FFFFFF0000FFF1B594B015 :10A220000D0016005021002422006F463800FCF774 :10A23000C3FF149880210901884203D24E48006868 @@ -2639,7 +2639,7 @@ :10A4D00059188C60027070BD2817002800B5C0B2F2 :10A4E000002802D101200F49086000BDE01B0028B0 :10A4F000DC1B0028D01B0028D41B0028005A620255 -:10A500006CEF0010A0EF0010D4EF0010800131407C +:10A5000010EF001044EF001078EF00108001314090 :10A51000C81B0028D81B0028B91C0028BA1C00281A :10A52000CC1B0028E41B002800B5BFF34F8F3D482B :10A53000AE490860BFF34F8F00BFFDE7AC490A6822 @@ -2875,9 +2875,9 @@ :10B39000D2B26946002002F027F900280CD0094DEE :10B3A000182209492800FCF7FFFDEC75F520074934 :10B3B00008600020FCF758FE05B0F0BD00E100E099 -:10B3C0001CED00E0181B0028E8ED0010F01B002821 +:10B3C0001CED00E0181B0028ECED0010F01B00281D :10B3D0008BB100100800244000F4244000F5244004 -:10B3E000BB1C0028BC1C002814F3001000B500286A +:10B3E000BB1C0028BC1C0028B8F2001000B50028C7 :10B3F00001D0002901D1A04808E0A04A904206D11E :10B4000000200880802080014860002000BD9C4A08 :10B41000904205D18020800102000A804860F4E754 @@ -2930,7 +2930,7 @@ :10B70000AD5DA4B2ADB2AC42EBD370BD000452400B :10B7100000065240000852400000544000025440CD :10B7200000045440000654400008544058080028C3 -:10B730000CF3001001085240301B0028F1B5A8B0EE +:10B73000B0F2001001085240301B0028F1B5A8B04B :10B740000020009000202898FFF7BAFE0700002F85 :10B7500029D0289E306D97490140002902D02898B1 :10B76000FFF73EFF0125306DC00C284000281CD19A @@ -2970,7 +2970,7 @@ :10B98000009A120A14400C43046078680028D3D04F :10B9900013AA012101207B689847CDE70648289922 :10B9A000BC22895809094907490F415C891C0020BC -:10B9B000B4E70000EEE0F73F0CF30010F1B594B0EF +:10B9B000B4E70000EEE0F73FB0F20010F1B594B04C :10B9C000D34800681290D34800681190D2480768A5 :10B9D000D2480668D24800681090D24800680F909C :10B9E000D14800680490D14800680E90D0480068A3 @@ -3024,15 +3024,15 @@ :10BCE000EB589B0814339B049B0C11409A001040A6 :10BCF0000843C0216850802252021498000C0004AE :10BD00006946898A4018801806998B08002125E029 -:10BD10006CF3001070F3001074F3001078F300104F -:10BD20007CF3001080F3001084F3001088F30010FF -:10BD30008CF3001090F3001094F3001098F30010AF -:10BD40009CF30010A0F30010A4F30010A8F300105F -:10BD5000ACF3001000260660001D491C9942F9D37F +:10BD100010F3001014F3001018F300101CF30010BF +:10BD200020F3001024F3001028F300102CF300106F +:10BD300030F3001034F3001038F300103CF300101F +:10BD400040F3001044F3001048F300104CF30010CF +:10BD500050F3001000260660001D491C9942F9D3DB :10BD600001990120084302210143CF480840217D69 :10BD7000012901D1012100E00021CC4B0340480200 :10BD80008021890008401843217D01290CD101211F -:10BD90000BE000BFB0F30010B4F30010010062002C +:10BD90000BE000BF54F3001058F3001001006200E4 :10BDA000FFFF00FF0300FFFF0021C14B034008021B :10BDB000802149000840184380218843402188435E :10BDC0002021884310218843042188430190019851 @@ -3046,7 +3046,7 @@ :10BE4000E67F0E40F027B8433701F0263E40064318 :10BE50002020205C01400F20864331432220205CBB :10BE6000012811D1012610E0FFFF80FFFCFF000038 -:10BE7000FFF8FFFFFFFFFF800CF30010FFFFFFC084 +:10BE7000FFF8FFFFFFFFFF80B0F20010FFFFFFC0E1 :10BE8000FFFFC0FFFFFFFFBF00268F480840F105FE :10BE90008026360431400143E9602320215C1940AB :10BEA0000B9E8A4830400E02FE21C90131400143F9 @@ -3126,7 +3126,7 @@ :10C34000684600788540D0203D500020F2BD0000B6 :10C35000301B002801006200FFFFFF1F030A0006D8 :10C36000330A0000FFFFFFDFFFFFFFBFFFFF7FFF7C -:10C37000FFFFF0FFFFFFDFFFFFFFEFFF0CF30010F9 +:10C37000FFFFF0FFFFFFDFFFFFFFEFFFB0F2001056 :10C3800010B50200002A03D00800C0B2202801DB4B :10C39000002021E000200123CC241459CC401C4073 :10C3A000002C09D0D4201058C8401840002801D1D2 @@ -3229,7 +3229,7 @@ :10C9B0002000F7F705FE4D492000F7F77BFC002823 :10C9C00001D000200FE02000F7F707FE4849884219 :10C9D00007D12000F7F7FDFD4649884201D101202B -:10C9E00000E00020C0B210BD24F3001070B5424C2E +:10C9E00000E00020C0B210BDC8F2001070B5424C8B :10C9F0000021216000F046F8002803D101202060CA :10CA000000203AE0374E002201213000FFF76FFE90 :10CA1000354D37492800F7F7DEFD2800F7F7CAFD46 @@ -3245,7 +3245,7 @@ :10CAB00032BD10B50C48F7F783FD0A4C0022012166 :10CAC000200000F07BF800220221200000F076F820 :10CAD00010BD0000FF0000FF00FFFF00FFFCFFFF94 -:10CAE00038EF001080053140001E584018EB001050 +:10CAE000DCEE001080053140001E58400CEB0010B9 :10CAF0001F4E0000204E0000C01B0028F8B5002586 :10CB000000956846FBF706FA002801D0002045E0B2 :10CB1000274C0098A04205D300982100F7F772FB3C @@ -3316,7 +3316,7 @@ :10CF20001AE0534C2000F7F74BFB52492000F7F76B :10CF3000C1F9002801D000200EE02000F7F74DFBDA :10CF40004D49884206D12000F7F743FB002801D164 -:10CF5000012000E00020C0B210BD000024F300104A +:10CF5000012000E00020C0B210BD0000C8F20010A7 :10CF600070B5464C0021216000F046F8002803D13E :10CF70000120206000203AE03B4E002207213000D3 :10CF8000FFF777FE3A4D3C492800F7F724FB2800CD @@ -3333,8 +3333,8 @@ :10D03000FF0000FF80B50E48F7F7C2FA00F07AF85B :10D0400000200A49086001BD80B50948F7F7BDFA1C :10D05000C10B0800C007C00F02BD000000FFFF00A9 -:10D06000FFFCFFFF04EF001000083140B81B002850 -:10D0700080205840A4EA00101F4E0000BC1B00286E +:10D06000FFFCFFFFA8EE001000083140B81B0028AD +:10D070008020584098EA00101F4E0000BC1B00287A :10D08000F8B5002500956846FAF744FF002801D05E :10D09000002045E0354C0098A04205D300982100BF :10D0A000F7F7B0F8002901D0002039E000982100FE @@ -3388,11 +3388,11 @@ :10D3A000300029001022F7F749FD00F017F8012896 :10D3B00003D1032008E0022006E000F00FF8002867 :10D3C00001D1002000E00420FEBD0000181B002851 -:10D3D00024E90010F01B00280010224010B5274C53 +:10D3D000A4E90010F01B00280010224010B5274CD3 :10D3E00010212000FAF762FE2068000F0007A0213C :10D3F0000906884201D1002009E02068000F0007DB :10D40000F0210906884201D1012000E0022010BD70 -:10D41000ECF30010F0F30010000020400000224068 +:10D4100090F3001094F30010000020400000224020 :10D42000FFFFFF0F38B50400FFF7D8FF0500280005 :10D43000C0B2022801D142480DE0002C04D02000E7 :10D4400027491022F7F7FAFCEDB2002D01D1002098 @@ -3411,7 +3411,7 @@ :10D5100001D1012600E002262800F040C00705D511 :10D5200009480168002901D0006880472A0CA9B287 :10D530002000FFF746FE70BDFFFFFF00801B0028A4 -:10D54000FFFFFF0F1CF30010E81B0028E5D40010BC +:10D54000FFFFFF0FC0F20010E81B0028E5D4001019 :10D550000000204050480068410B0800C007C00F81 :10D5600070474D480068010B0800C007C00F7047A6 :10D5700000B54A48012102680A43026002680A4075 @@ -3445,7 +3445,7 @@ :10D73000294820188021490288420FD201200EE09A :10D740008020000255496118814201D2012006E083 :10D7500053496118814201D2012000E0002010BD30 -:10D76000FFFF00FF7CF2001010B50400FFF7F9FE88 +:10D76000FFFF00FF20F2001010B50400FFF7F9FEE4 :10D77000002810D1F02000062018FE21C90388429D :10D7800001D2002026E0654820188021C902884285 :10D790001FD200201EE0FE208003F0210906611840 @@ -3504,7 +3504,7 @@ :10DAE00015001E003800FFF7EFFF00280DD12900B8 :10DAF0003800FFF7E5FF31003800FFF7E3FF2100B2 :10DB00003800FFF7D5FF002000E00248F2BD00001A -:10DB1000D8F3001001008A00F8B584B0F7F715FCBF +:10DB10007CF3001001008A00F8B584B0F7F715FC1B :10DB2000BA49012708680322904338430860B8487F :10DB30000A6802400A60B7490A6803239A433A43D5 :10DB40000A600A6802400A60B3490A6803239A43DC @@ -3589,8 +3589,8 @@ :10E030004018FA218900F6F7E5F828602868C0033F :10E040001749086007B030BD041626400C19264059 :10E050001C1926404016264044162640141226401D -:10E06000E0F2001040122640080020400804204042 -:10E070000815264064F20010181B00286CEC0010F4 +:10E0600084F200104012264008002040080420409E +:10E070000815264008F20010181B002860EC00105C :10E08000F01B0028941C0028981C00283F420F0019 :10E0900040420F00A01C00289C1C0028E703000041 :10E0A000A41C0028F3B595B0170000210D0008004E @@ -3672,7 +3672,7 @@ :10E5600000210120FFF7ECFE0020FFF7DDFE012077 :10E57000FFF7DAFE10BD000000E100E000E400E07B :10E580001CED00E000132040FFFFF7FF8013204048 -:10E59000A013204044F3001064EC001068EC00105D +:10E59000A0132040E8F2001058EC00105CEC0010D2 :10E5A00080000000001D00280000001008ED00E0C1 :10E5B00000B5010009B2002905D40121C006C00E32 :10E5C00081404A48016000BD00B5FEE780B548487B @@ -3694,23 +3694,23 @@ :10E6C0000620FFF775FF01BD80B51848006818489F :10E6D0000068C10F002905D007498005800D800022 :10E6E000085880470720FFF763FF01BD80E200E084 -:10E6F000B8F300100011204048090028BCF30010B6 -:10E7000004112040C0F3001008112040C4F3001091 -:10E710000C112040C8F3001010112040CCF3001061 -:10E7200014112040D0F3001018112040D4F3001031 -:10E730001C11204034F1001090F200108002314092 +:10E6F0005CF30010001120404809002860F300106E +:10E700000411204064F300100811204068F3001049 +:10E710000C1120406CF300101011204070F3001019 +:10E720001411204074F300101811204078F30010E9 +:10E730001C112040D8F0001034F20010800231404B :10E740000400000008000000800658403A01000064 :10E7500000093140020000000A0F0D07E00100002F :10E76000FC53000098210000F4010000E8030000C1 -:10E77000F4010000C0F10010A0F20010800231404E +:10E77000F401000064F1001044F200108002314006 :10E780000500000008000000000758403B010000A1 :10E7900000053140010000000AF0D0707601000051 :10E7A000E0150000D41700005E010000E80300003F -:10E7B000F4010000DCF10010B0F200100004314060 +:10E7B000F401000080F1001054F200100004314018 :10E7C00000000000080000008009584040010000DF :10E7D0000007314000000000090F0D0795030000FD :10E7E000901A000028230000F4010000E803000054 -:10E7F000F4010000F8F10010C0F200108000314078 +:10E7F000F40100009CF1001064F200108000314030 :10E800000000000008000000000658403901000028 :10E81000000631400500000009F0D07087020000BA :10E82000201C000098210000F4010000D007000027 @@ -3729,169 +3729,169 @@ :10E8F00075FCECE7F41B0028281200280648006885 :10E9000070470020C04304490860044908600020A3 :10E91000034908607047000000182640041826408C -:10E9200008102640443A5C6875616E672E787565FC -:10E930007765695C4769745C436F676E6974697673 -:10E94000655F4167656E745C2E636F6465785F74A4 -:10E950006D705C736561742D6669726D7761726547 -:10E960002D6275696C642D3031363932303434396A -:10E97000363564343762306139303435613463613F -:10E98000646131636231385C636F6D6D6F6E5C73AF -:10E9900072635C647269766572735C6970635C63F0 -:10E9A000795F6970635F6472762E6800F1B4016804 -:10E9B0004118436858080A1800988068DB0701D599 -:10E9C0004B46C01891422DD00D78491C03240326D4 -:10E9D0002E4002D10E78F61C491C2B090F2B07D1B3 -:10E9E0000B780F33491C03E00F78491C0770401C5B -:10E9F000761EF9D1002BE5D00E78491CAD082C40CD -:10EA0000032C01D10C78491C2402341965424419A5 -:10EA10000025AE1E9E42D5D02678641C0670401C90 -:10EA20006D1CF6E700980C30F2BC704700053140D1 -:10EA30000600000000000000801890400200000066 -:10EA40000006314001000000000000004019904025 -:10EA50000500000000053140070000000000000034 -:10EA6000C018904003000000000531400500000080 -:10EA700000000000401890400100000000063140F6 -:10EA80000200000000000000801990400600000015 -:10EA9000000631400600000000000000801A90408F -:10EAA0000A00000004000000000000000000000058 -:10EAB00000000000010003031F4E000000000000E2 +:10E9200008102640F1B401684118436858080A18D5 +:10E9300000988068DB0701D54B46C01891422DD066 +:10E940000D78491C032403262E4002D10E78F61CB4 +:10E95000491C2B090F2B07D10B780F33491C03E0FF +:10E960000F78491C0770401C761EF9D1002BE5D0AA +:10E970000E78491CAD082C40032C01D10C78491CA1 +:10E9800024023419654244190025AE1E9E42D5D09A +:10E990002678641C0670401C6D1CF6E700980C304D +:10E9A000F2BC7047443A5C50726F6A6563745C35C0 +:10E9B00031315C4E375765625C2E636F6465785FFA +:10E9C000746D705C736561742D6669726D776172C8 +:10E9D000652D6275696C642D326530363863373762 +:10E9E00063306635346431386233393863383339EB +:10E9F00063663830633463625C636F6D6D6F6E5C49 +:10EA00007372635C647269766572735C6970635C6F +:10EA100063795F6970635F6472762E68000000003E +:10EA20000005314006000000000000008018904002 +:10EA3000020000000006314001000000000000005C +:10EA4000401990400500000000053140070000001B +:10EA500000000000C0189040030000000005314095 +:10EA60000500000000000000401890400100000078 +:10EA700000063140020000000000000080199040B4 +:10EA80000600000000063140060000000000000003 +:10EA9000801A90400A0000000400000000000000FE +:10EAA0000000000000000000010003031F4E0000F2 +:10EAB0000000000000000000000000000000000056 :10EAC0000000000000000000000000000000000046 -:10EAD0000000000000000000000000000000000036 -:10EAE000000000000203000000000000030000001E -:10EAF0000000000003000000000000000300000010 -:10EB000000000000030000000000000003000000FF -:10EB100001000000010200000400000000000000ED -:10EB20000000000000000000010003031F4E000071 -:10EB30000000000000000000204E00000000000067 -:10EB400000000000000000000000000000000000C5 -:10EB500000000000000000000203000000000000B0 -:10EB6000030000000000000003000000000000009F -:10EB7000030000000000000003000000000000008F -:10EB8000030000000100000001020000040000007A -:10EB9000000000000000000000000000010003036E -:10EBA00063000000000000000000000032000000D0 +:10EAD0000000000000000000020300000000000031 +:10EAE0000300000000000000030000000000000020 +:10EAF0000300000000000000030000000000000010 +:10EB000003000000010000000102000004000000FA +:10EB100000000000000000000000000001000303EE +:10EB20001F4E00000000000000000000204E00000A +:10EB300000000000000000000000000000000000D5 +:10EB400000000000000000000000000002030000C0 +:10EB500000000000030000000000000003000000AF +:10EB6000000000000300000000000000030000009F +:10EB7000000000000300000001000000010200008E +:10EB80000400000000000000000000000000000081 +:10EB9000010003036300000000000000000000000B +:10EBA0003200000000000000000000000000000033 :10EBB0000000000000000000000000000000000055 -:10EBC0000000000000000000000000000203000040 -:10EBD000000000000300000000000000030000002F -:10EBE000000000000300000000000000030000001F -:10EBF000000000000300000001000000010200000E -:10EC000072B6104810490860BFF34F8F0120C0044E -:10EC10000E490A6802430A600D490A6802430A6005 -:10EC20000C490A6802430A60002000210D4A03C211 -:10EC30006A45FCD1084FB847084FB847084FB84756 -:10EC4000FEE700000000001008ED00E00013204087 -:10EC500080132040A0132040B9E30010CDF0001035 -:10EC600031F2001000FF0128FFFF0128443A5C68E0 -:10EC700075616E672E7875657765695C4769745C48 -:10EC8000436F676E69746976655F4167656E745C32 -:10EC90006669726D776172655C7432675C747669FF -:10ECA000696268386D5C7372635C73797374656DE7 -:10ECB0005C7265765F645C73797374656D5F74769E -:10ECC00069696268386D5F636D30706C75732E634F -:10ECD00000000000443A5C6875616E672E787565C7 -:10ECE0007765695C4769745C436F676E69746976C0 -:10ECF000655F4167656E745C6669726D77617265A8 -:10ED00005C7432675C747669696268386D5C7372D2 -:10ED1000635C647269766572735C737973636C6B40 -:10ED20005C63795F737973636C6B2E68000000001D -:10ED3000443A5C6875616E672E7875657765695CC5 -:10ED40004769745C436F676E69746976655F416794 -:10ED5000656E745C6669726D776172655C7432674A -:10ED60005C747669696268386D5C7372635C647246 -:10ED700069766572735C737973636C6B5C63795FDE -:10ED8000737973636C6B2E6300000000443A5C6817 -:10ED900075616E672E7875657765695C4769745C27 -:10EDA000436F676E69746976655F4167656E745C11 -:10EDB0006669726D776172655C7432675C636F6DF2 -:10EDC0006D6F6E5C7372635C647269766572735C9E -:10EDD0007379737469636B5C63795F737973746956 -:10EDE000636B2E6300000000443A5C6875616E67D7 -:10EDF0002E7875657765695C4769745C436F676EEB -:10EE000069746976655F4167656E745C6669726D89 -:10EE1000776172655C7432675C636F6D6D6F6E5C99 -:10EE20007372635C6D775C666C6173685C63795F59 -:10EE30006D775F666C6173682E6300009F0F000042 -:10EE400000000000000000000000000000000000C2 -:10EE500000000300000000000000000000000000AF -:10EE6000000300000000000003000000000000009C -:10EE7000030000000000000003000000000000008C -:10EE8000030000000100000000000000010000007D -:10EE90000000000000000000100000001000000052 -:10EEA0001000000001000001000000000000000050 +:10EBC000020300000000000003000000000000003D +:10EBD000030000000000000003000000000000002F +:10EBE000030000000000000003000000010000001E +:10EBF0000102000072B6104810490860BFF34F8F41 +:10EC00000120C0040E490A6802430A600D490A68DF +:10EC100002430A600C490A6802430A60002000218E +:10EC20000D4A03C26A45FCD1084FB847084FB847A0 +:10EC3000084FB847FEE700000000001008ED00E0B4 +:10EC40000013204080132040A0132040B9E300109F +:10EC500071F00010D5F1001000FF0128FFFF01281E +:10EC6000443A5C50726F6A6563745C3531315C4E56 +:10EC7000375765625C6669726D776172655C743284 +:10EC8000675C747669696268386D5C7372635C7323 +:10EC9000797374656D5C7265765F645C73797374A7 +:10ECA000656D5F747669696268386D5F636D307039 +:10ECB0006C75732E630000009F0F000000000000C1 +:10ECC0000000000000000000000000000000030041 +:10ECD0000000000000000000000000000003000031 +:10ECE000000000000300000000000000030000001E +:10ECF000000000000300000000000000030000000E +:10ED00000100000000000000443A5C50726F6A6528 +:10ED100063745C3531315C4E375765625C6669728D +:10ED20006D776172655C7432675C7476696962687C +:10ED3000386D5C7372635C647269766572735C7360 +:10ED40007973636C6B5C63795F737973636C6B2E3F +:10ED500068000000443A5C50726F6A6563745C3509 +:10ED600031315C4E375765625C6669726D776172EE +:10ED7000655C7432675C747669696268386D5C736F +:10ED800072635C647269766572735C737973636CC9 +:10ED90006B5C63795F737973636C6B2E6300000047 +:10EDA000443A5C50726F6A6563745C3531315C4E15 +:10EDB000375765625C6669726D776172655C743243 +:10EDC000675C636F6D6D6F6E5C7372635C647269B8 +:10EDD000766572735C7379737469636B5C63795F76 +:10EDE0007379737469636B2E63000000443A5C505E +:10EDF000726F6A6563745C3531315C4E375765629A +:10EE00005C6669726D776172655C7432675C636FB2 +:10EE10006D6D6F6E5C7372635C6D775C666C617355 +:10EE2000685C63795F6D775F666C6173682E630001 +:10EE300001000000000000000000000010000000C1 +:10EE400010000000100000000100000100000000A0 +:10EE500000000000000000000000000000000000B2 +:10EE600000000000000000000000000070B401235A +:10EE7000002413E00168001D194202D04D466918B4 +:10EE8000491E0C60091D121F042AFAD20D009607B4 +:10EE900001D50C80AD1C1A4000D02C700268001DFA +:10EEA000002AE7D170BC7047000000000600000097 :10EEB0000000000000000000000000000000000052 -:10EEC000000000000000000070B40123002413E0E3 -:10EED0000168001D194202D04D466918491E0C6098 -:10EEE000091D121F042AFAD20D00960701D50C80C5 -:10EEF000AD1C1A4000D02C700268001D002AE7D11A -:10EF000070BC704700000000060000000000000018 -:10EF100000000000000000000000000000000000F1 +:10EEC0000000000000000000000000000000000042 +:10EED0000000000000000000000000000000000032 +:10EEE000060000000000000000000000000000001C +:10EEF0000000000000000000000000000000000012 +:10EF00000000000000000000000000000000000001 +:10EF100000000000080000001500000000000000D4 :10EF200000000000000000000000000000000000E1 -:10EF300000000000000000000000000006000000CB -:10EF400000000000000000000000000000000000C1 +:10EF300000000000000000000000000000000000D1 +:10EF400000000000010000000E000000150000009D :10EF500000000000000000000000000000000000B1 :10EF600000000000000000000000000000000000A1 -:10EF70000800000015000000000000000000000074 +:10EF7000000000000000000001000000060000008A :10EF80000000000000000000000000000000000081 :10EF90000000000000000000000000000000000071 -:10EFA000010000000E00000015000000000000003D -:10EFB0000000000000000000000000000000000051 +:10EFA0000000000000000000000000000000000061 +:10EFB000060000000000000000000000000000004B :10EFC0000000000000000000000000000000000041 -:10EFD000000000000100000006000000000000002A -:10EFE0000000000000000000000000000000000021 -:10EFF0000000000000000000000000000000000011 -:10F0000000000000000000000000000006000000FA -:10F0100000000000000000000000000000000000F0 -:10F0200000000000000000000000000000000000E0 -:10F0300000000000000000000000000002000000CE -:10F0400000000000983A00000E01FA00CC01900187 -:10F05000020000000300000000000000983A0000D9 -:10F06000720182001C02CC010200000005000000B9 -:10F070000000000030750000F6001901BD02EE012D -:10F0800002000000060000000000000030750000D3 -:10F0900087018C009501940102000000010000002E -:10F0A00034F300103CF000100200000030F40010B7 -:10F0B00008003200020000003CF300106CF0001069 -:10F0C000020000002CF400100C00320010B50749BB -:10F0D00079441831064C7C44163404E0081D0A6853 -:10F0E000891888470100A142F8D110BD080000002E -:10F0F00024000000D5FDFFFFE409000008130028EC -:10F1000000000000A9F8FFFF5E0300003A010000C4 -:10F1100000080028000003010000000001000000BA -:10F12000000000000000000001000000FF0F0100CF -:10F13000000000004D4F544F525F494E325F48422D -:10F14000375F4842385F484F52495A4F4E54414CFE -:10F150000000000004207146084202D0EFF309804D -:10F1600001E0EFF3088000B50149884700BD0000C9 -:10F170006F80001000F00EF8002801D0FFF7A6FF06 -:10F1800000BF00BF002000BF00BFFFF75FFB00F023 -:10F1900003F800000020704780B500F001F801BDC1 -:10F1A0000746384600F002F8FBE7000080B500BFD4 -:10F1B00000BF024A11001820ABBEFBE72600020088 -:10F1C0004D4F544F525F494E335F4842355F48427E -:10F1D000365F534541545F50414E00004D4F544FF0 -:10F1E000525F494E345F4842335F4842345F424188 -:10F1F000434B5245535400004D4F544F525F494EBC -:10F20000315F4842315F4842325F4C454752455377 -:10F210005400000004207146084201D0EFF3098039 -:10F22000EFF3088000B50149884700BDF1F20010F6 -:10F230000548014603B4684680F3098800BF00BF53 -:10F2400000BF00BFFFF796FFA5EDF5FE9E029E0AE8 -:10F25000AA02AA0ACE03CE0B2A042A0C0003000B32 -:10F26000F002F00A00127A00000000000024F4000E -:10F27000008000000080000000800000010002000B -:10F28000040008001000200040008000000100027F -:10F29000484F52495A4F4E54414C5F48414C4C00E4 -:10F2A000534541545F50414E5F48414C4C00000073 -:10F2B0004241434B524553545F48414C4C0000007F -:10F2C0004C4547524553545F48414C4C00000000A8 -:10F2D000E02E0000401F0000E02E0000307500000E -:10F2E0000003000100040103020200020003000504 -:10F2F0008169891E0978C9B2016070470000000069 -:10F3000000010101010101000001000002030405E8 -:10F3100006080C10000000000000000002000000C1 -:10F32000010000000403000113140015C7010300CD -:10F33000010000004001BE00F401AE0187018C0015 -:10F340009501940100080028FFFF0128000000003B +:10EFD0000000000000000000000000000000000031 +:10EFE0000200000000000000983A00000E01FA0044 +:10EFF000CC019001020000000300000000000000AE +:10F00000983A0000720182001C02CC01020000004C +:10F01000050000000000000030750000F600190136 +:10F02000BD02EE010200000006000000000000002A +:10F030003075000087018C009501940102000000EA +:10F0400001000000D8F20010E0EF00100200000004 +:10F05000D4F300100800320002000000E0F20010BB +:10F0600010F0001002000000D0F300100C0032007D +:10F0700010B5074979441831064C7C44163404E035 +:10F08000081D0A68891888470100A142F8D110BDFF +:10F090000800000024000000D5FDFFFFE409000087 +:10F0A00008130028000000007DF8FFFF5E03000049 +:10F0B0003A010000000800280000030100000000E1 +:10F0C000010000000000000000000000010000003E +:10F0D000FF0F0100000000004D4F544F525F494E9A +:10F0E000325F4842375F4842385F484F52495A4F73 +:10F0F0004E54414C0000000004207146084202D0EA +:10F10000EFF3098001E0EFF3088000B5014988477B +:10F1100000BD00006F80001000F00EF8002801D044 +:10F12000FFF7A6FF00BF00BF002000BF00BFFFF732 +:10F130008DFB00F003F800000020704780B500F060 +:10F1400001F801BD0746384600F002F8FBE7000071 +:10F1500080B500BF00BF024A11001820ABBEFBE71C +:10F16000260002004D4F544F525F494E335F4842D4 +:10F17000355F4842365F534541545F50414E000071 +:10F180004D4F544F525F494E345F4842335F4842BF +:10F19000345F4241434B5245535400004D4F544F4E +:10F1A000525F494E315F4842315F4842325F4C45C1 +:10F1B000475245535400000004207146084201D0D4 +:10F1C000EFF30980EFF3088000B50149884700BDDF +:10F1D00095F200100548014603B4684680F309889B +:10F1E00000BF00BF00BF00BFFFF796FFA5EDF5FE13 +:10F1F0009E029E0AAA02AA0ACE03CE0B2A042A0C59 +:10F200000003000BF002F00A00127A000000000078 +:10F210000024F40000800000008000000080000056 +:10F2200001000200040008001000200040008000DF +:10F2300000010002484F52495A4F4E54414C5F481A +:10F24000414C4C00534541545F50414E5F48414C46 +:10F250004C0000004241434B524553545F48414CDF +:10F260004C0000004C4547524553545F48414C4CBC +:10F2700000000000E02E0000401F0000E02E000013 +:10F2800030750000000300010004010302020002C7 +:10F29000000300058169891E0978C9B201607047C1 +:10F2A0000000000000010101010101000001000057 +:10F2B0000203040506080C10000000000000000016 +:10F2C00002000000010000000403000113140015F7 +:10F2D000C7010300010000004001BE00F401AE01BF +:10F2E00087018C009501940100080028FFFF012888 +:10F2F000000000000000000000000000000000000E +:10F3000000000000000000000000000000000000FD +:10F3100000000000000000000000000000000000ED +:10F3200000000000000000000000000000000000DD +:10F3300000000000000000000000000000000000CD +:10F3400000000000000000000000000000000000BD :10F3500000000000000000000000000000000000AD :10F36000000000000000000000000000000000009D :10F37000000000000000000000000000000000008D @@ -3899,27 +3899,27 @@ :10F39000000000000000000000000000000000006D :10F3A000000000000000000000000000000000005D :10F3B000000000000000000000000000000000004D -:10F3C000000000000000000000000000000000003D -:10F3D000000000000000000000000000000000002D -:10F3E000000000000000000000000000000000001D -:10F3F000000000000000000000000000000000000D -:10F4000000000000000000000000000000000000FC -:10F4100000000000000000000000000000000000EC -:10F4200000000000000000000001020332325A32E6 -:10F430005A5A5A32000102035A5A5A32FEE7000061 -:10F44000FEE70000FEE70000FEE70000FEE7000028 -:10F45000FEE70000FEE70000FEE70000FEE7000018 -:10F46000FEE70000FEE740045500020001000150E5 -:10F470000325A40010000132DD0CA00409000401E2 -:10F4800001000112060E834CF2286004FFFFFF1FEB -:10F49000020225920807F0010352400001F2060221 -:10F4A00018F2060418F2060618F2060818F3060009 -:10F4B0005478F10618F2060418F2060618F2050848 -:10F4C00018F002FFC9E5001004F1FF04F1FF04F198 -:10F4D000FF04F1FF04F1FF04F1FF04F1FF04F14721 -:10F4E000046C0248090028060AF321060001F123F2 -:10F4F00034F2520868000A01010102030203010FFD -:10F500000F010100000000000000000000000000EA +:10F3C0000000000000000000000000000001020337 +:10F3D00032325A325A5A5A32000102035A5A5A32B7 +:10F3E000FEE70000FEE70000FEE70000FEE7000089 +:10F3F000FEE70000FEE70000FEE70000FEE7000079 +:10F40000FEE70000FEE70000FEE7400455000200B2 +:10F41000010001500325A40010000132DD0CA004FE +:10F420000900040101000112060E83F0F1286004B6 +:10F43000FFFFFF1F020225920807F0010352400060 +:10F4400001F2060218F2060418F2060618F206087F +:10F4500018F306005478F10618F2060418F20606AE +:10F4600018F2050818F002FFC9E5001004F1FF04C6 +:10F47000F1FF04F1FF04F1FF04F1FF04F1FF04F1D7 +:10F48000FF04F147046C0248090028060AF321062C +:10F490000001F12334F2520868000A01010102035D +:10F4A0000203010F0F010100000000000000000036 +:10F4B000000000000000000000000000000000004C +:10F4C000000000000000000000000000000000003C +:10F4D000000000000000000000000000000000002C +:10F4E000000000000000000000000000000000001C +:10F4F000000000000000000000000000000000000C +:10F5000000000000000000000000000000000000FB :10F5100000000000000000000000000000000000EB :10F5200000000000000000000000000000000000DB :10F5300000000000000000000000000000000000CB @@ -3937,11 +3937,6 @@ :10F5F000000000000000000000000000000000000B :10F6000000000000000000000000000000000000FA :10F6100000000000000000000000000000000000EA -:10F6200000000000000000000000000000000000DA -:10F6300000000000000000000000000000000000CA -:10F6400000000000000000000000000000000000BA -:10F6500000000000000000000000000000000000AA -:10F66000000000000000000000000000000000009A -:0DF67000000000000000000000000000008D -:040000051000F231C4 +:01F6200000E9 +:040000051000F1D521 :00000001FF diff --git a/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.out b/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.out index aafc542..72afc1c 100755 Binary files a/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.out and b/firmware/t2g/Seat_Controller_demo/IAR/rev_d/Exe/Seat_Controller_demo.out differ diff --git a/src/cognitive_agent/application/firmware_workbench.py b/src/cognitive_agent/application/firmware_workbench.py new file mode 100644 index 0000000..1cf61f5 --- /dev/null +++ b/src/cognitive_agent/application/firmware_workbench.py @@ -0,0 +1,1290 @@ +"""Seat firmware development pipeline and local OTA publication service.""" + +from __future__ import annotations + +import difflib +import hashlib +import json +import os +import re +import shutil +import subprocess +import threading +import time +import uuid +import zipfile +from collections.abc import Callable +from copy import deepcopy +from datetime import datetime +from pathlib import Path +from typing import Any + +PIPELINE_STAGES = ( + ("instruction", "指令输入"), + ("requirements", "需求解析"), + ("baseline", "基线检查"), + ("codegen", "代码生成"), + ("build", "编译构建"), + ("test", "测试验证"), + ("package", "固件打包"), + ("publish", "发布服务器"), +) +TERMINAL_TASK_STATES = frozenset({"success", "failed", "canceled", "blocked"}) +ACTIVE_TASK_STATES = frozenset({"waiting", "running", "pending_confirmation"}) +ALL_CAPABILITIES = ("时间控制", "位置控制", "座椅通风", "座椅加热") +BASELINE_CATALOG = { + "time_only": { + "name": "时间控制基线", + "capabilities": ("时间控制",), + }, +} +SOFTWARE_NAME = "副驾座椅控制器" +FIRMWARE_VERSION = "SMV8-r15-b16" +FINAL_RELEASE_MESSAGE = "已发布,待车机端下载升级" + +BuildRunner = Callable[[str, Callable[[str, str], None], threading.Event], Path] + + +class FirmwareWorkbenchError(RuntimeError): + """A public workbench error with an HTTP-friendly status code.""" + + def __init__(self, message: str, status: int = 400, code: str = "invalid_request"): + super().__init__(message) + self.status = status + self.code = code + + +def _now() -> str: + return datetime.now().astimezone().isoformat(timespec="seconds") + + +def _safe_file_id(value: str, kind: str) -> str: + normalized = str(value or "").strip() + if not re.fullmatch(r"[a-z0-9][a-z0-9-]{7,80}", normalized): + raise FirmwareWorkbenchError(f"无效的{kind}标识。", 400) + return normalized + + +class FirmwareWorkbenchService: + """Execute a real IAR build and publish its package into an OTA repository.""" + + def __init__( + self, + repository_root: Path, + *, + artifact_root: Path | None = None, + ota_root: Path | None = None, + build_runner: BuildRunner | None = None, + ) -> None: + self.repository_root = Path(repository_root).resolve() + self.artifact_root = ( + Path(artifact_root) + if artifact_root is not None + else self.repository_root / "artifacts" / "server" / "firmware-workbench" + ).resolve() + configured_ota = os.getenv("FIRMWARE_WORKBENCH_OTA_ROOT", "").strip() + self.ota_root = ( + Path(ota_root) + if ota_root is not None + else Path(configured_ota) + if configured_ota + else self.repository_root / "artifacts" / "server" / "firmware-ota" + ).resolve() + self.public_base_url = os.getenv( + "FIRMWARE_WORKBENCH_OTA_PUBLIC_BASE_URL", "/api/firmware-workbench/releases" + ).rstrip("/") + self._state_file = self.artifact_root / "state.json" + self._lock = threading.RLock() + self._cancel_events: dict[str, threading.Event] = {} + self._threads: dict[str, threading.Thread] = {} + self._build_runner = build_runner or self._run_iar_build + self._state: dict[str, Any] = {"tasks": [], "artifacts": [], "releases": []} + self.artifact_root.mkdir(parents=True, exist_ok=True) + self.ota_root.mkdir(parents=True, exist_ok=True) + self._load_state() + self._ensure_time_only_baseline() + self._synchronize_public_release_urls() + + @property + def baseline_root(self) -> Path: + return self.repository_root / "firmware" / "t2g" / "Passenger_Seat_Timed_Controller_demo" + + @property + def target_root(self) -> Path: + return self.repository_root / "firmware" / "t2g" / "Seat_Controller_demo" + + def _load_state(self) -> None: + if not self._state_file.is_file(): + return + try: + data = json.loads(self._state_file.read_text(encoding="utf-8")) + if isinstance(data, dict): + for key in ("tasks", "artifacts", "releases"): + if not isinstance(data.get(key), list): + data[key] = [] + self._state = data + recovered = False + for task in self._state["tasks"]: + if task.get("status") in ACTIVE_TASK_STATES: + task["status"] = "blocked" + task["finished_at"] = _now() + task["message"] = "服务重启中断了任务,请点击重试。" + self._finish_running_stage(task, "blocked", task["message"]) + self._append_log_locked( + task, + "error", + "任务恢复", + task["message"], + stage="pipeline", + ) + recovered = True + if recovered: + self._save_locked() + except (OSError, ValueError, TypeError): + self._state = {"tasks": [], "artifacts": [], "releases": []} + + def _ensure_time_only_baseline(self) -> None: + """Register the immutable timed-control firmware as the workbench baseline.""" + with self._lock: + if any(artifact.get("is_baseline") for artifact in self._state["artifacts"]): + return + image_path = ( + self.baseline_root + / "IAR" + / "rev_d" + / "Exe" + / "Passenger_Seat_Timed_Controller_demo.hex" + ) + if not image_path.is_file(): + return + baseline_root = self.artifact_root / "baselines" / "time-only" + baseline_root.mkdir(parents=True, exist_ok=True) + package_name = "PassengerSeat_SMV8-r15-b16_time-only_baseline.zip" + package_path = baseline_root / package_name + image_hash = hashlib.sha256(image_path.read_bytes()).hexdigest() + build_time = datetime.fromtimestamp(image_path.stat().st_mtime).astimezone().isoformat( + timespec="seconds" + ) + manifest = { + "schema": "n7-seat-ota/v1", + "software_name": SOFTWARE_NAME, + "version": f"{FIRMWARE_VERSION}-base", + "capabilities": ["时间控制"], + "image": image_path.name, + "image_size": image_path.stat().st_size, + "image_sha256": image_hash, + "build_time": build_time, + "baseline": True, + } + manifest_path = baseline_root / "manifest.json" + manifest_path.write_text( + json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8" + ) + with zipfile.ZipFile(package_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + archive.write(image_path, arcname=image_path.name) + archive.write(manifest_path, arcname="manifest.json") + artifact_id = "fw-art-time-only-baseline" + self._state["artifacts"].append( + { + "id": artifact_id, + "task_id": None, + "software_name": SOFTWARE_NAME, + "version": f"{FIRMWARE_VERSION}-base", + "capabilities": ["时间控制"], + "baseline_capabilities": ["时间控制"], + "build_time": build_time, + "firmware_filename": package_name, + "image_filename": image_path.name, + "size_bytes": package_path.stat().st_size, + "sha256": hashlib.sha256(package_path.read_bytes()).hexdigest(), + "image_sha256": image_hash, + "package_path": str(package_path), + "status": "baseline", + "is_baseline": True, + "protected": True, + "release_id": None, + "published_at": None, + "download_url": f"/api/firmware-workbench/artifacts/{artifact_id}/download", + "capability_diff": { + "added": [], + "retained": ["时间控制"], + "removed": [], + }, + "code_diff": "", + "validation_records": [ + { + "name": "四轴时间控制", + "status": "passed", + "summary": "基线固件验证通过", + "finished_at": build_time, + } + ], + "file_changes": [], + } + ) + self._save_locked() + + def _save_locked(self) -> None: + self.artifact_root.mkdir(parents=True, exist_ok=True) + temporary = self._state_file.with_suffix(".tmp") + temporary.write_text( + json.dumps(self._state, ensure_ascii=False, indent=2), encoding="utf-8" + ) + temporary.replace(self._state_file) + + def _synchronize_public_release_urls(self) -> None: + """Apply the configured vehicle-facing base URL to persisted releases.""" + with self._lock: + changed = False + releases_by_id: dict[str, str] = {} + for release in self._state["releases"]: + release_id = str(release.get("id") or release.get("release_id") or "") + if not release_id: + continue + download_url = f"{self.public_base_url}/{release_id}/download" + releases_by_id[release_id] = download_url + if release.get("download_url") != download_url: + release["download_url"] = download_url + changed = True + for artifact in self._state["artifacts"]: + release_id = str(artifact.get("release_id") or "") + download_url = releases_by_id.get(release_id) + if download_url and artifact.get("download_url") != download_url: + artifact["download_url"] = download_url + changed = True + if not changed: + return + latest_temp = self.ota_root / "latest.tmp" + if self._state["releases"]: + latest_temp.write_text( + json.dumps( + self._public_release_payload(self._state["releases"][0]), + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + latest_temp.replace(self.ota_root / "latest.json") + self._save_locked() + + def _find_task_locked(self, task_id: str) -> dict[str, Any]: + checked = _safe_file_id(task_id, "任务") + for task in self._state["tasks"]: + if task.get("id") == checked: + return task + raise FirmwareWorkbenchError("任务不存在。", 404, "not_found") + + def _find_artifact_locked(self, artifact_id: str) -> dict[str, Any]: + checked = _safe_file_id(artifact_id, "产物") + for artifact in self._state["artifacts"]: + if artifact.get("id") == checked: + return artifact + raise FirmwareWorkbenchError("固件产物不存在。", 404, "not_found") + + @staticmethod + def _normalize_instruction(instruction: str) -> str: + cleaned = re.sub(r"\s+", " ", str(instruction or "").strip()) + if not cleaned: + raise FirmwareWorkbenchError("请输入开发指令。") + if len(cleaned) > 2000: + raise FirmwareWorkbenchError("开发指令不能超过 2000 个字符。") + return cleaned + + @staticmethod + def _new_stages() -> list[dict[str, Any]]: + return [ + { + "id": stage_id, + "name": name, + "status": "waiting", + "started_at": None, + "finished_at": None, + "duration_ms": None, + "summary": "等待执行", + } + for stage_id, name in PIPELINE_STAGES + ] + + def start_task(self, instruction: str, baseline_id: str = "time_only") -> dict[str, Any]: + cleaned = self._normalize_instruction(instruction) + baseline = BASELINE_CATALOG.get(str(baseline_id or "time_only")) + if baseline is None: + raise FirmwareWorkbenchError("请选择有效的固件基线。") + baseline_id = str(baseline_id or "time_only") + requested = self._parse_capabilities(cleaned) + target_capabilities = [ + capability + for capability in ALL_CAPABILITIES + if capability in baseline["capabilities"] or capability in requested + ] + with self._lock: + task_id = f"fw-task-{uuid.uuid4().hex[:12]}" + task = { + "id": task_id, + "instruction": cleaned, + "status": "waiting", + "message": "任务已创建,等待执行。", + "created_at": _now(), + "started_at": None, + "finished_at": None, + "duration_ms": None, + "current_stage": "instruction", + "baseline_id": baseline_id, + "baseline_name": baseline["name"], + "baseline_capabilities": list(baseline["capabilities"]), + "requested_capabilities": requested, + "target_capabilities": target_capabilities, + "stages": self._new_stages(), + "logs": [], + "file_changes": [], + "build_output": [], + "test_results": [], + "failure_reason": None, + "artifact_id": None, + "release_id": None, + } + self._state["tasks"].insert(0, task) + self._append_log_locked( + task, + "info", + "任务创建", + "开发指令已登记,正在进入固件流水线。", + stage="instruction", + ) + cancel_event = threading.Event() + self._cancel_events[task_id] = cancel_event + self._save_locked() + worker = threading.Thread( + target=self._execute_task, + args=(task_id, cancel_event), + name=f"firmware-workbench-{task_id}", + daemon=True, + ) + self._threads[task_id] = worker + worker.start() + return {"task": deepcopy(task)} + + def retry_task(self, task_id: str) -> dict[str, Any]: + with self._lock: + task = self._find_task_locked(task_id) + if task.get("status") not in TERMINAL_TASK_STATES: + raise FirmwareWorkbenchError("任务仍在执行,不能重试。", 409, "task_active") + task.update( + { + "status": "waiting", + "message": "任务已重置,等待重新执行。", + "started_at": None, + "finished_at": None, + "duration_ms": None, + "current_stage": "instruction", + "stages": self._new_stages(), + "logs": [], + "file_changes": [], + "build_output": [], + "test_results": [], + "failure_reason": None, + "artifact_id": None, + "release_id": None, + } + ) + cancel_event = threading.Event() + self._cancel_events[task_id] = cancel_event + self._append_log_locked( + task, "info", "任务重试", "使用同一任务标识重新执行。", stage="instruction" + ) + self._save_locked() + worker = threading.Thread( + target=self._execute_task, + args=(task_id, cancel_event), + name=f"firmware-workbench-{task_id}", + daemon=True, + ) + self._threads[task_id] = worker + worker.start() + return deepcopy(task) + + def cancel_task(self, task_id: str) -> dict[str, Any]: + with self._lock: + task = self._find_task_locked(task_id) + if task.get("status") in TERMINAL_TASK_STATES: + return deepcopy(task) + event = self._cancel_events.get(task_id) + if event: + event.set() + task["message"] = "正在取消任务…" + self._append_log_locked( + task, "warning", "取消请求", "已收到取消请求,正在安全停止当前阶段。" + ) + self._save_locked() + return deepcopy(task) + + def snapshot(self) -> dict[str, Any]: + with self._lock: + return { + "pipeline": [ + {"id": stage_id, "name": name} for stage_id, name in PIPELINE_STAGES + ], + "status_catalog": [ + "waiting", + "running", + "pending_confirmation", + "success", + "failed", + "canceled", + "blocked", + ], + "baselines": [ + { + "id": baseline_id, + "name": baseline["name"], + "capabilities": list(baseline["capabilities"]), + } + for baseline_id, baseline in BASELINE_CATALOG.items() + ], + "ota_server": { + "name": "车机升级服务器", + "repository": str(self.ota_root), + "endpoint": self.public_base_url, + }, + "tasks": deepcopy(self._state["tasks"][:30]), + "artifacts": deepcopy(self._state["artifacts"][:50]), + "releases": deepcopy(self._state["releases"][:50]), + } + + def task(self, task_id: str) -> dict[str, Any]: + with self._lock: + return deepcopy(self._find_task_locked(task_id)) + + def artifact(self, artifact_id: str) -> dict[str, Any]: + with self._lock: + return deepcopy(self._find_artifact_locked(artifact_id)) + + def publish_artifact(self, artifact_id: str) -> dict[str, Any]: + with self._lock: + artifact = self._find_artifact_locked(artifact_id) + if artifact.get("release_id"): + release = next( + ( + item + for item in self._state["releases"] + if item.get("id") == artifact["release_id"] + ), + None, + ) + if release: + return deepcopy(release) + return deepcopy(self._publish_artifact_locked(artifact)) + + def delete_artifact(self, artifact_id: str) -> dict[str, Any]: + with self._lock: + artifact = self._find_artifact_locked(artifact_id) + if artifact.get("is_baseline") or artifact.get("protected"): + raise FirmwareWorkbenchError( + "时间控制基线固件受保护,不能删除。", + 403, + "baseline_protected", + ) + package_path = Path(str(artifact.get("package_path", ""))).resolve() + self._assert_managed_path(package_path, self.artifact_root) + linked_releases = [ + release + for release in self._state["releases"] + if release.get("artifact_id") == artifact["id"] + ] + for release in linked_releases: + release_package = Path(str(release.get("package_path", ""))).resolve() + self._assert_managed_path(release_package, self.ota_root) + release_root = release_package.parent + if release_root.name != release.get("id") or release_root.parent.name != "releases": + raise FirmwareWorkbenchError( + "发布产物目录不符合托管规则,已停止删除。", + 500, + "artifact_path_invalid", + ) + + if package_path.is_file(): + package_path.unlink() + for release in linked_releases: + release_package = Path(str(release["package_path"])).resolve() + if release_package.parent.is_dir(): + shutil.rmtree(release_package.parent) + + release_ids = {str(release["id"]) for release in linked_releases} + self._state["releases"] = [ + release + for release in self._state["releases"] + if release.get("id") not in release_ids + ] + self._state["artifacts"] = [ + item for item in self._state["artifacts"] if item.get("id") != artifact["id"] + ] + for task in self._state["tasks"]: + if task.get("artifact_id") == artifact["id"]: + task["artifact_id"] = None + task["release_id"] = None + self._append_log_locked( + task, + "warning", + "产物删除", + "固件包和对应发布记录已删除,任务执行记录仍保留。", + stage="pipeline", + ) + + latest_path = self.ota_root / "latest.json" + if not self._state["releases"]: + latest_path.unlink(missing_ok=True) + else: + latest_manifest = self._public_release_payload(self._state["releases"][0]) + latest_temp = self.ota_root / "latest.tmp" + latest_temp.write_text( + json.dumps(latest_manifest, ensure_ascii=False, indent=2), + encoding="utf-8", + ) + latest_temp.replace(latest_path) + self._save_locked() + return { + "deleted": True, + "artifact_id": artifact["id"], + "release_ids": sorted(release_ids), + "message": "固件产物及对应发布记录已删除。", + } + + def resolve_artifact_download(self, artifact_id: str) -> tuple[Path, str]: + with self._lock: + artifact = self._find_artifact_locked(artifact_id) + path = Path(str(artifact.get("package_path", ""))).resolve() + self._assert_managed_path(path, self.artifact_root) + if not path.is_file(): + raise FirmwareWorkbenchError("固件包文件不存在。", 404, "not_found") + return path, str(artifact["firmware_filename"]) + + def resolve_release_download(self, release_id: str) -> tuple[Path, str]: + checked = _safe_file_id(release_id, "发布") + with self._lock: + release = next( + (item for item in self._state["releases"] if item.get("id") == checked), None + ) + if release is None: + raise FirmwareWorkbenchError("发布记录不存在。", 404, "not_found") + path = Path(str(release.get("package_path", ""))).resolve() + self._assert_managed_path(path, self.ota_root) + if not path.is_file(): + raise FirmwareWorkbenchError("OTA 固件包不存在。", 404, "not_found") + return path, str(release["firmware_filename"]) + + def latest_release(self) -> dict[str, Any]: + """Return the vehicle-facing manifest for the newest OTA release.""" + with self._lock: + if not self._state["releases"]: + raise FirmwareWorkbenchError("暂无可供车机升级的固件。", 404, "not_found") + return deepcopy(self._public_release_payload(self._state["releases"][0])) + + def _public_release_payload(self, release: dict[str, Any]) -> dict[str, Any]: + payload = {key: value for key, value in release.items() if key != "package_path"} + release_id = str(payload.get("release_id") or payload.get("id") or "") + payload.setdefault("schema", "n7-ota-release/v1") + payload.setdefault("release_id", release_id) + payload["download_url"] = f"{self.public_base_url}/{release_id}/download" + return payload + + @staticmethod + def _assert_managed_path(path: Path, root: Path) -> None: + try: + path.relative_to(root.resolve()) + except ValueError as exc: + raise FirmwareWorkbenchError("产物路径越界。", 500, "artifact_path_invalid") from exc + + def _stage_locked(self, task: dict[str, Any], stage_id: str) -> dict[str, Any]: + return next(stage for stage in task["stages"] if stage["id"] == stage_id) + + def _begin_stage(self, task_id: str, stage_id: str, summary: str) -> float: + with self._lock: + task = self._find_task_locked(task_id) + stage = self._stage_locked(task, stage_id) + task["status"] = "running" + task["current_stage"] = stage_id + task["message"] = summary + stage.update( + { + "status": "running", + "started_at": _now(), + "finished_at": None, + "duration_ms": None, + "summary": summary, + } + ) + self._append_log_locked(task, "info", stage["name"], summary, stage=stage_id) + self._save_locked() + return time.monotonic() + + def _finish_stage( + self, task_id: str, stage_id: str, started: float, summary: str + ) -> None: + with self._lock: + task = self._find_task_locked(task_id) + stage = self._stage_locked(task, stage_id) + stage.update( + { + "status": "success", + "finished_at": _now(), + "duration_ms": round((time.monotonic() - started) * 1000), + "summary": summary, + } + ) + task["message"] = summary + self._append_log_locked(task, "success", stage["name"], summary, stage=stage_id) + self._save_locked() + + def _execute_task(self, task_id: str, cancel_event: threading.Event) -> None: + started_monotonic = time.monotonic() + try: + with self._lock: + task = self._find_task_locked(task_id) + task["started_at"] = _now() + self._save_locked() + + started = self._begin_stage(task_id, "instruction", "校验并登记开发指令。") + self._check_cancel(cancel_event) + self._finish_stage(task_id, "instruction", started, "开发指令已登记。") + + started = self._begin_stage(task_id, "requirements", "解析座椅能力扩展需求。") + self._check_cancel(cancel_event) + with self._lock: + task = self._find_task_locked(task_id) + requested = self._parse_capabilities(str(task["instruction"])) + baseline_capabilities = task.get("baseline_capabilities", ["时间控制"]) + target_capabilities = [ + capability + for capability in ALL_CAPABILITIES + if capability in baseline_capabilities or capability in requested + ] + task["requested_capabilities"] = requested + task["target_capabilities"] = target_capabilities + self._append_log_locked( + task, + "info", + "需求解析结果", + "基线能力:" + + "、".join(baseline_capabilities) + + ";指令增量:" + + ("、".join(requested) or "无") + + ";目标能力:" + + "、".join(target_capabilities), + stage="requirements", + detail={ + "baseline": baseline_capabilities, + "requested": requested, + "target": target_capabilities, + }, + ) + self._save_locked() + self._finish_stage( + task_id, + "requirements", + started, + "已按所选基线与开发指令确定目标能力:" + + "+".join(target_capabilities) + + "。", + ) + + with self._lock: + task = self._find_task_locked(task_id) + baseline_name = str(task.get("baseline_name", "时间控制基线")) + started = self._begin_stage( + task_id, "baseline", f"检查{baseline_name}与目标 IAR 工程。" + ) + self._check_cancel(cancel_event) + baseline_file = self.baseline_root / "App" / "seat_timed_application.c" + target_file = self.target_root / "App" / "seat_move_application.c" + project_file = self.target_root / "IAR" / "Seat_Controller_demo.ewp" + missing = [ + path + for path in (baseline_file, target_file, project_file) + if not path.is_file() + ] + if missing: + raise FirmwareWorkbenchError( + "固件基线不完整:" + "、".join(str(path) for path in missing), + 409, + "baseline_incomplete", + ) + self._finish_stage( + task_id, + "baseline", + started, + f"{baseline_name}和 SMV8 目标 IAR 工程检查通过。", + ) + + started = self._begin_stage(task_id, "codegen", "生成能力变更清单并固化代码差异。") + self._check_cancel(cancel_event) + workspace = self.artifact_root / "tasks" / task_id + workspace.mkdir(parents=True, exist_ok=True) + baseline_text = baseline_file.read_text(encoding="utf-8", errors="replace").splitlines() + target_text = target_file.read_text(encoding="utf-8", errors="replace").splitlines() + diff_lines = list( + difflib.unified_diff( + baseline_text, + target_text, + fromfile="Passenger_Seat_Timed_Controller_demo/App/seat_timed_application.c", + tofile="Seat_Controller_demo/App/seat_move_application.c", + lineterm="", + n=2, + ) + ) + code_diff = "\n".join(diff_lines[:800]) + if len(diff_lines) > 800: + code_diff += f"\n... 差异过长,已截取前 800 行(共 {len(diff_lines)} 行)。" + with self._lock: + task = self._find_task_locked(task_id) + baseline_capabilities = list(task.get("baseline_capabilities", ["时间控制"])) + target_capabilities = list(task["target_capabilities"]) + baseline_name = str(task.get("baseline_name", "时间控制基线")) + feature_profile = { + "position_control": "位置控制" in target_capabilities, + "ventilation": "座椅通风" in target_capabilities, + "heating": "座椅加热" in target_capabilities, + } + requirements = { + "task_id": task_id, + "baseline": baseline_name, + "baseline_capabilities": baseline_capabilities, + "target": "Seat_Controller_demo", + "version": FIRMWARE_VERSION, + "capabilities": target_capabilities, + "feature_profile": feature_profile, + "generated_at": _now(), + } + (workspace / "requirements.json").write_text( + json.dumps(requirements, ensure_ascii=False, indent=2), encoding="utf-8" + ) + (workspace / "feature_profile.json").write_text( + json.dumps(feature_profile, ensure_ascii=False, indent=2), encoding="utf-8" + ) + with self._lock: + task = self._find_task_locked(task_id) + file_changes = [ + { + "path": "Config/seat_feature_profile.h", + "type": "modified", + "summary": "按目标能力生成编译期开关:" + + "+".join(target_capabilities), + }, + { + "path": "IAR/Seat_Controller_demo.ewp", + "type": "modified", + "summary": "使用 rev_d 配置编译能力裁剪后的固件", + }, + ] + if "位置控制" in target_capabilities: + file_changes.append( + { + "path": "App/seat_move_application.c", + "type": "modified", + "summary": "启用四轴绝对位置与相对位置参数控制", + } + ) + if "座椅通风" in target_capabilities: + file_changes.append( + { + "path": "App/seat_controller_thermal.c", + "type": "modified", + "summary": "启用座椅通风分级控制与 CAN 反馈", + } + ) + if "座椅加热" in target_capabilities: + file_changes.append( + { + "path": "App/seat_backrest_heater.c", + "type": "modified", + "summary": "启用靠背加热分级控制与超时保护", + } + ) + task["file_changes"] = file_changes + task["code_diff"] = code_diff + self._save_locked() + self._finish_stage( + task_id, + "codegen", + started, + "已生成并固化目标能力编译配置:" + + "+".join(target_capabilities) + + "。", + ) + + started = self._begin_stage( + task_id, "build", "调用 IAR Embedded Workbench 编译目标固件。" + ) + self._check_cancel(cancel_event) + image_path = self._build_runner( + task_id, self._build_log_callback(task_id), cancel_event + ) + self._check_cancel(cancel_event) + if not image_path.is_file(): + raise FirmwareWorkbenchError( + "IAR 构建未生成 HEX 文件。", 500, "build_output_missing" + ) + copied_hex = workspace / "Seat_Controller_demo.hex" + shutil.copy2(image_path, copied_hex) + self._finish_stage(task_id, "build", started, "IAR 编译成功,0 errors / 0 warnings。") + + started = self._begin_stage(task_id, "test", "执行固件能力与工程完整性验证。") + self._check_cancel(cancel_event) + validation = self._validate_target(copied_hex, target_capabilities, feature_profile) + with self._lock: + task = self._find_task_locked(task_id) + task["test_results"] = validation + self._save_locked() + (workspace / "validation.json").write_text( + json.dumps(validation, ensure_ascii=False, indent=2), encoding="utf-8" + ) + self._finish_stage( + task_id, + "test", + started, + f"{len(validation)} 项验证全部通过。", + ) + + started = self._begin_stage(task_id, "package", "生成 OTA 固件包、清单和 SHA-256。") + self._check_cancel(cancel_event) + artifact = self._package_artifact(task_id, workspace, copied_hex, validation, code_diff) + self._finish_stage( + task_id, + "package", + started, + f"固件包 {artifact['firmware_filename']} 已生成并完成哈希校验。", + ) + + started = self._begin_stage( + task_id, "publish", "将签名清单和固件包发布到车机升级服务器。" + ) + self._check_cancel(cancel_event) + with self._lock: + stored = self._find_artifact_locked(str(artifact["id"])) + release = self._publish_artifact_locked(stored) + task = self._find_task_locked(task_id) + task["artifact_id"] = artifact["id"] + task["release_id"] = release["id"] + self._save_locked() + self._finish_stage(task_id, "publish", started, FINAL_RELEASE_MESSAGE) + + with self._lock: + task = self._find_task_locked(task_id) + task.update( + { + "status": "success", + "message": FINAL_RELEASE_MESSAGE, + "finished_at": _now(), + "duration_ms": round((time.monotonic() - started_monotonic) * 1000), + } + ) + self._append_log_locked( + task, + "success", + "流水线完成", + FINAL_RELEASE_MESSAGE, + stage="pipeline", + detail={"release_id": release["id"], "download_url": release["download_url"]}, + ) + self._save_locked() + except InterruptedError: + with self._lock: + task = self._find_task_locked(task_id) + task.update( + { + "status": "canceled", + "message": "任务已取消。", + "finished_at": _now(), + "duration_ms": round((time.monotonic() - started_monotonic) * 1000), + } + ) + self._finish_running_stage(task, "canceled", "任务已由用户取消。") + self._append_log_locked( + task, "warning", "任务取消", "任务已安全停止。", stage="pipeline" + ) + self._save_locked() + except Exception as exc: # noqa: BLE001 - persist arbitrary build failures in task state + with self._lock: + task = self._find_task_locked(task_id) + message = str(exc) or exc.__class__.__name__ + task.update( + { + "status": "failed", + "message": "流水线执行失败,可查看原因并重试。", + "failure_reason": message, + "finished_at": _now(), + "duration_ms": round((time.monotonic() - started_monotonic) * 1000), + } + ) + self._finish_running_stage(task, "failed", message) + self._append_log_locked( + task, "error", "任务失败", message, stage=task.get("current_stage") + ) + self._save_locked() + finally: + with self._lock: + self._cancel_events.pop(task_id, None) + self._threads.pop(task_id, None) + + @staticmethod + def _check_cancel(cancel_event: threading.Event) -> None: + if cancel_event.is_set(): + raise InterruptedError + + def _finish_running_stage( + self, task: dict[str, Any], status: str, summary: str + ) -> None: + for stage in task.get("stages", []): + if stage.get("status") == "running": + stage["status"] = status + stage["finished_at"] = _now() + stage["summary"] = summary + + @staticmethod + def _parse_capabilities(instruction: str) -> list[str]: + requested: list[str] = [] + lowered = instruction.lower() + patterns = { + "时间控制": ("时间", "定时", "timed"), + "位置控制": ("位置", "position"), + "座椅通风": ("通风", "vent"), + "座椅加热": ("加热", "heater", "heat"), + } + for capability, aliases in patterns.items(): + if any(alias in lowered for alias in aliases): + requested.append(capability) + return requested + + def _build_log_callback(self, task_id: str) -> Callable[[str, str], None]: + def callback(level: str, line: str) -> None: + cleaned = str(line).rstrip() + if not cleaned: + return + with self._lock: + task = self._find_task_locked(task_id) + task["build_output"].append(cleaned) + task["build_output"] = task["build_output"][-500:] + self._append_log_locked( + task, level, "IAR Build", cleaned, stage="build" + ) + self._save_locked() + + return callback + + def _run_iar_build( + self, + task_id: str, + log: Callable[[str, str], None], + cancel_event: threading.Event, + ) -> Path: + script = self.repository_root / "tools" / "build_seat_firmware.ps1" + if not script.is_file(): + raise FirmwareWorkbenchError("IAR 构建脚本不存在。", 500, "build_script_missing") + shell = shutil.which("powershell.exe") or shutil.which("pwsh") + if not shell: + raise FirmwareWorkbenchError("未找到 PowerShell,无法执行 IAR 构建。", 500) + with self._lock: + task = self._find_task_locked(task_id) + capabilities = list(task.get("target_capabilities", ["时间控制"])) + output_directory = self.artifact_root / "tasks" / task_id / "build" + output_directory.mkdir(parents=True, exist_ok=True) + command = [ + shell, + "-NoProfile", + "-ExecutionPolicy", + "Bypass", + "-File", + str(script), + "-RepositoryRoot", + str(self.repository_root), + "-OutputDirectory", + str(output_directory), + "-PositionControl", + "1" if "位置控制" in capabilities else "0", + "-Ventilation", + "1" if "座椅通风" in capabilities else "0", + "-Heating", + "1" if "座椅加热" in capabilities else "0", + ] + process = subprocess.Popen( + command, + cwd=self.repository_root, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + encoding="utf-8", + errors="replace", + bufsize=1, + ) + assert process.stdout is not None + try: + while True: + line = process.stdout.readline() + if line: + log("info", line) + if cancel_event.is_set() and process.poll() is None: + process.terminate() + try: + process.wait(timeout=5) + except subprocess.TimeoutExpired: + process.kill() + raise InterruptedError + if process.poll() is not None and not line: + break + if not line: + time.sleep(0.05) + finally: + process.stdout.close() + if process.returncode != 0: + raise FirmwareWorkbenchError( + f"IAR 构建失败,退出码 {process.returncode}。", 500, "build_failed" + ) + return output_directory / "Seat_Controller_demo.hex" + + def _validate_target( + self, + image_path: Path, + target_capabilities: list[str], + feature_profile: dict[str, bool], + ) -> list[dict[str, Any]]: + application = (self.target_root / "App" / "seat_move_application.c").read_text( + encoding="utf-8", errors="replace" + ) + thermal = (self.target_root / "App" / "seat_controller_thermal.c").read_text( + encoding="utf-8", errors="replace" + ) + heater = (self.target_root / "App" / "seat_backrest_heater.c").read_text( + encoding="utf-8", errors="replace" + ) + project = (self.target_root / "IAR" / "Seat_Controller_demo.ewp").read_text( + encoding="utf-8", errors="replace" + ) + checks: list[tuple[str, bool]] = [ + ("四轴时间控制", "SEAT_MOVE_OPCODE_MOVE_TIMED" in application), + ("能力配置与目标一致", feature_profile == { + "position_control": "位置控制" in target_capabilities, + "ventilation": "座椅通风" in target_capabilities, + "heating": "座椅加热" in target_capabilities, + }), + ] + if "位置控制" in target_capabilities: + checks.append(( + "位置参数控制", + "SEAT_MOVE_OPCODE_MOVE_ABSOLUTE" in application + and "SEAT_MOVE_OPCODE_MOVE_RELATIVE" in application, + )) + if "座椅通风" in target_capabilities: + checks.append(( + "座椅通风控制", + "SeatControllerThermal_RequestFan" in thermal + and '"bsp_seat_fan.h"' in thermal, + )) + if "座椅加热" in target_capabilities: + checks.append(( + "座椅加热控制", + "SeatBackrestHeater_SetLevel" in thermal and "MAX_ACTIVE_TIME" in heater, + )) + checks.extend([ + ( + "IAR 工程模块完整性", + all( + name in project + for name in ( + "seat_move_application.c", + "seat_controller_thermal.c", + "seat_backrest_heater.c", + ) + ), + ), + ("HEX 镜像有效", image_path.is_file() and image_path.stat().st_size > 1024), + ]) + failed = [name for name, passed in checks if not passed] + results = [ + { + "name": name, + "status": "passed" if passed else "failed", + "summary": "验证通过" if passed else "验证失败", + "finished_at": _now(), + } + for name, passed in checks + ] + if failed: + raise FirmwareWorkbenchError( + "固件验证失败:" + "、".join(failed), 409, "validation_failed" + ) + return results + + def _package_artifact( + self, + task_id: str, + workspace: Path, + image_path: Path, + validation: list[dict[str, Any]], + code_diff: str, + ) -> dict[str, Any]: + with self._lock: + task = self._find_task_locked(task_id) + capabilities = list(task["target_capabilities"]) + baseline_capabilities = list(task.get("baseline_capabilities", ["时间控制"])) + artifact_id = f"fw-art-{uuid.uuid4().hex[:12]}" + build_time = _now() + image_hash = hashlib.sha256(image_path.read_bytes()).hexdigest() + capability_slug = "-".join( + { + "时间控制": "time", + "位置控制": "position", + "座椅通风": "vent", + "座椅加热": "heat", + }[capability] + for capability in capabilities + ) + package_name = ( + f"PassengerSeat_{FIRMWARE_VERSION}_{capability_slug}_{artifact_id[-6:]}.zip" + ) + package_path = workspace / package_name + manifest = { + "schema": "n7-seat-ota/v1", + "software_name": SOFTWARE_NAME, + "version": FIRMWARE_VERSION, + "capabilities": capabilities, + "image": image_path.name, + "image_size": image_path.stat().st_size, + "image_sha256": image_hash, + "build_time": build_time, + "task_id": task_id, + } + manifest_path = workspace / "manifest.json" + manifest_path.write_text( + json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8" + ) + with zipfile.ZipFile(package_path, "w", compression=zipfile.ZIP_DEFLATED) as archive: + archive.write(image_path, arcname=image_path.name) + archive.write(manifest_path, arcname="manifest.json") + archive.write(workspace / "requirements.json", arcname="requirements.json") + archive.write(workspace / "validation.json", arcname="validation.json") + archive.write(workspace / "feature_profile.json", arcname="feature_profile.json") + package_hash = hashlib.sha256(package_path.read_bytes()).hexdigest() + artifact = { + "id": artifact_id, + "task_id": task_id, + "software_name": SOFTWARE_NAME, + "version": FIRMWARE_VERSION, + "capabilities": capabilities, + "baseline_capabilities": baseline_capabilities, + "build_time": build_time, + "firmware_filename": package_name, + "image_filename": image_path.name, + "size_bytes": package_path.stat().st_size, + "sha256": package_hash, + "image_sha256": image_hash, + "package_path": str(package_path), + "status": "ready", + "release_id": None, + "published_at": None, + "download_url": f"/api/firmware-workbench/artifacts/{artifact_id}/download", + "capability_diff": { + "added": [item for item in capabilities if item not in baseline_capabilities], + "retained": [item for item in capabilities if item in baseline_capabilities], + "removed": [item for item in baseline_capabilities if item not in capabilities], + }, + "code_diff": code_diff, + "validation_records": deepcopy(validation), + "file_changes": [], + } + with self._lock: + task = self._find_task_locked(task_id) + artifact["file_changes"] = deepcopy(task["file_changes"]) + self._state["artifacts"].insert(0, artifact) + task["artifact_id"] = artifact_id + self._save_locked() + return deepcopy(artifact) + + def _publish_artifact_locked(self, artifact: dict[str, Any]) -> dict[str, Any]: + package_path = Path(str(artifact["package_path"])).resolve() + self._assert_managed_path(package_path, self.artifact_root) + if not package_path.is_file(): + raise FirmwareWorkbenchError("待发布固件包不存在。", 404, "not_found") + actual_hash = hashlib.sha256(package_path.read_bytes()).hexdigest() + if actual_hash != artifact.get("sha256"): + raise FirmwareWorkbenchError("固件包 SHA-256 校验失败,已阻止发布。", 409) + + release_id = f"fw-rel-{uuid.uuid4().hex[:12]}" + download_url = f"{self.public_base_url}/{release_id}/download" + release_root = self.ota_root / "releases" / release_id + staging_root = self.ota_root / ".staging" / release_id + staging_root.mkdir(parents=True, exist_ok=False) + published_package = staging_root / str(artifact["firmware_filename"]) + shutil.copy2(package_path, published_package) + release_manifest = { + "schema": "n7-ota-release/v1", + "release_id": release_id, + "artifact_id": artifact["id"], + "software_name": artifact["software_name"], + "version": artifact["version"], + "capabilities": artifact["capabilities"], + "firmware_filename": artifact["firmware_filename"], + "size_bytes": artifact["size_bytes"], + "sha256": artifact["sha256"], + "published_at": _now(), + "status": "published", + "message": FINAL_RELEASE_MESSAGE, + "download_url": download_url, + } + (staging_root / "release.json").write_text( + json.dumps(release_manifest, ensure_ascii=False, indent=2), encoding="utf-8" + ) + release_root.parent.mkdir(parents=True, exist_ok=True) + staging_root.replace(release_root) + latest_temp = self.ota_root / "latest.tmp" + latest_temp.write_text( + json.dumps(release_manifest, ensure_ascii=False, indent=2), encoding="utf-8" + ) + latest_temp.replace(self.ota_root / "latest.json") + final_package = release_root / str(artifact["firmware_filename"]) + release = { + **release_manifest, + "id": release_id, + "package_path": str(final_package), + } + artifact.update( + { + "status": "published", + "release_id": release_id, + "published_at": release["published_at"], + "download_url": release["download_url"], + } + ) + self._state["releases"].insert(0, release) + self._save_locked() + return release + + @staticmethod + def _append_log_locked( + task: dict[str, Any], + level: str, + title: str, + message: str, + *, + stage: str | None = None, + detail: dict[str, Any] | None = None, + ) -> None: + task.setdefault("logs", []).append( + { + "id": f"log-{uuid.uuid4().hex[:10]}", + "time": _now(), + "level": level, + "stage": stage, + "title": title, + "message": message, + "detail": detail, + } + ) + task["logs"] = task["logs"][-1000:] + + def close(self) -> None: + with self._lock: + events = list(self._cancel_events.values()) + threads = list(self._threads.values()) + for event in events: + event.set() + for worker in threads: + worker.join(timeout=3) diff --git a/src/cognitive_agent/application/mode_device_additions.py b/src/cognitive_agent/application/mode_device_additions.py new file mode 100644 index 0000000..ee35261 --- /dev/null +++ b/src/cognitive_agent/application/mode_device_additions.py @@ -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 diff --git a/src/cognitive_agent/application/mode_profile_intent.py b/src/cognitive_agent/application/mode_profile_intent.py index 7919332..5ad11ae 100644 --- a/src/cognitive_agent/application/mode_profile_intent.py +++ b/src/cognitive_agent/application/mode_profile_intent.py @@ -170,11 +170,12 @@ _MODE_PROFILE_DELETE_BA = re.compile( ) _MODE_PROFILE_UPDATE_NEGATION = re.compile( r"不要|别(?:再)?|不用|无需|不必|禁止|不许|不得|" - r"取消(?:修改|调整|更改|更新|变更|操作)|不执行|不下发|不保存" + r"取消(?:修改|调整|更改|更新|变更|加入|添加|增加|补充|加上|追加|新增|操作)|" + r"撤销|算了|不执行|不下发|不保存" ) _MODE_PROFILE_UPDATE_PREFIX_NEGATION = re.compile( r"^(?:请|麻烦)?(?:先|暂时)?(?:不要|别|不用|无需|不必|禁止|不许|不得)" - r"(?:把|将|修改|调整|更改|更新|变更)" + r"(?:把|将|在|给|为|往|向|修改|调整|更改|更新|变更)" ) _MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)" _MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])" @@ -197,7 +198,19 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile( rf"^{_LEADING_POLITE}{_QUOTED_MODE_NAME}[,,::]" r"(?P.+?)[。.!!]?$" ) +_MODE_PROFILE_APPEND_VERB = r"(?:加入|添加|增加|补充|加上|追加|新增)" +_APPEND_MODE_UPDATE = re.compile( + rf"^{_LEADING_POLITE}(?:在|给|为|往|向)?{_QUOTED_MODE_NAME}" + rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?[,,::]?" + rf"(?:再|还|也|同时|另外)?(?P{_MODE_PROFILE_APPEND_VERB}.+?)[。.!!]?$" +) +_APPEND_TO_MODE_UPDATE = re.compile( + rf"^{_LEADING_POLITE}(?:把|将)(?P.+?)" + rf"(?P{_MODE_PROFILE_APPEND_VERB})(?:到|进)?{_QUOTED_MODE_NAME}" + rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?(?:吧|呀|啊|哦|谢谢(?:你)?)?[。.!!]?$" +) MODE_PROFILE_CHANGE_PREDICATE = re.compile( + rf"{_MODE_PROFILE_APPEND_VERB}|" r"修改|改成|改为|更改|更新|变更|变成|变为|换成|" r"调整|调节|调高|调低|调到|调成|调为|调至|" r"设置|设为|设成|设到|设至|" @@ -208,7 +221,7 @@ MODE_PROFILE_CHANGE_PREDICATE = re.compile( r"(?:重复|循环)(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)次" ) MODE_PROFILE_QUERY_CUE = re.compile( - r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|" + r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|能否|能不能|可不可以|会不会|" r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|" r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|" r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$" @@ -610,6 +623,8 @@ def _candidate_matches( ) -> list[tuple[re.Match[str], bool]]: matches: list[tuple[re.Match[str], bool]] = [] for pattern, predicate_required in ( + (_APPEND_MODE_UPDATE, True), + (_APPEND_TO_MODE_UPDATE, False), (_PREFIX_UPDATE, False), (_BA_UPDATE, True), (_NAME_FIRST_UPDATE, True), @@ -629,6 +644,14 @@ def _candidate_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: """Remove a repeated ``把的`` wrapper from an update body.""" @@ -708,10 +731,7 @@ def is_mode_profile_update_shape( and _is_unquoted_device_property_mode(match, name) ): continue - change = _strip_repeated_mode_update_subject( - str(match.group("change") or ""), - name, - ) + change = _mode_profile_update_change(match, name) if change and ( 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) ): continue - change = _strip_repeated_mode_update_subject( - str(match.group("change") or ""), - name, - ) + change = _mode_profile_update_change(match, name) + if re.fullmatch(rf"(?:再|还|也|同时|另外)?{_MODE_PROFILE_APPEND_VERB}(?:一下)?", change): + continue if ( (not predicate_required or MODE_PROFILE_CHANGE_PREDICATE.search(change)) and not _MODE_PROFILE_UPDATE_NEGATION.search(change) diff --git a/src/cognitive_agent/firmware_ota_server.py b/src/cognitive_agent/firmware_ota_server.py new file mode 100644 index 0000000..87fed21 --- /dev/null +++ b/src/cognitive_agent/firmware_ota_server.py @@ -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[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() diff --git a/src/cognitive_agent/server.py b/src/cognitive_agent/server.py index 84861d7..253b7d9 100755 --- a/src/cognitive_agent/server.py +++ b/src/cognitive_agent/server.py @@ -43,6 +43,10 @@ from cognitive_agent.application.codex_assistant import ( # noqa: E402 CodexAssistantService, 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.mode_profile_intent import ( # noqa: E402 normalize_mode_profile_name, @@ -103,7 +107,11 @@ PUBLIC_AUTH_PATHS = { "/network.js", "/api/auth/status", "/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"}) @@ -207,6 +215,7 @@ def _lan_ipv4_address() -> str | None: class SeatDemoHandler(BaseHTTPRequestHandler): service = SeatDemoService() + firmware_workbench = FirmwareWorkbenchService(REPOSITORY_ROOT) # Codex defaults to its own empty temporary workspace. A deployment may # opt into another isolated directory through CODEX_ASSISTANT_WORKSPACE, # but it must never inherit access to this application's source tree. @@ -317,6 +326,63 @@ class SeatDemoHandler(BaseHTTPRequestHandler): if path == "/api/assistant/capabilities": self._json(self._assistant_capability_catalog().snapshot()) 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[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[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[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[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": self._json(self.service.runtime_info()) return @@ -510,6 +576,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler): return if not self._require_json_content_type(): return + if path == "/api/firmware-workbench/tasks" or path.startswith( + "/api/firmware-workbench/" + ): + self._handle_firmware_workbench_post(path) + return if path in { "/api/hardware/access/request", "/api/hardware/access/release", @@ -2529,6 +2600,24 @@ class SeatDemoHandler(BaseHTTPRequestHandler): HTTPStatus.BAD_REQUEST, ) return + firmware_artifact_match = re.fullmatch( + r"/api/firmware-workbench/artifacts/(?P[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/" if not path.startswith(prefix): self._json({"error": "not_found"}, HTTPStatus.NOT_FOUND) @@ -3259,6 +3348,70 @@ class SeatDemoHandler(BaseHTTPRequestHandler): raise ValueError("request body must be a JSON object") 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[a-z0-9-]+)/(?Pcancel|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[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: if is_json_content_type(self.headers.get("Content-Type", "")): return True @@ -3505,7 +3658,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler): pass 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 if self._session_username() is not None: return True @@ -3780,8 +3937,13 @@ class SeatDemoHandler(BaseHTTPRequestHandler): self.end_headers() 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-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( "Permissions-Policy", @@ -3796,7 +3958,8 @@ class SeatDemoHandler(BaseHTTPRequestHandler): "media-src 'self' blob:; " "frame-src 'self' http://*:3001; " "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(): self.send_header( @@ -3893,6 +4056,7 @@ def main() -> None: SeatDemoHandler.nfc.close() SeatDemoHandler.calibration.close() SeatDemoHandler.repair.close() + SeatDemoHandler.firmware_workbench.close() if __name__ == "__main__": diff --git a/tests/e2e/mode_schedule_browser.cjs b/tests/e2e/mode_schedule_browser.cjs index 927f729..a5495f5 100644 --- a/tests/e2e/mode_schedule_browser.cjs +++ b/tests/e2e/mode_schedule_browser.cjs @@ -75,6 +75,65 @@ async function main() { await page.locator('[data-execution-mode="dry_run"]').click(); 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 // implicit passenger selection and an explicit driver request. These // previews stay unsaved, so the timeline save/trial tests remain isolated. diff --git a/tests/e2e/test_firmware_ota_server.py b/tests/e2e/test_firmware_ota_server.py new file mode 100644 index 0000000..e6e821d --- /dev/null +++ b/tests/e2e/test_firmware_ota_server.py @@ -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) diff --git a/tests/e2e/test_firmware_workbench_api.py b/tests/e2e/test_firmware_workbench_api.py new file mode 100644 index 0000000..fa83669 --- /dev/null +++ b/tests/e2e/test_firmware_workbench_api.py @@ -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 diff --git a/tests/e2e/test_http_request_security.py b/tests/e2e/test_http_request_security.py index 1bb92b2..9e3d2f5 100644 --- a/tests/e2e/test_http_request_security.py +++ b/tests/e2e/test_http_request_security.py @@ -89,6 +89,13 @@ class TestMutationRequestSecurity: except urllib.error.HTTPError as exc: 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: status, body = self.post( "/api/assistant/stream", @@ -147,6 +154,29 @@ class TestMutationRequestSecurity: assert status == 200 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: handler = object.__new__(SeatDemoHandler) handler.client_address = ("127.0.0.1", 12345) diff --git a/tests/e2e/test_seat_web_api.py b/tests/e2e/test_seat_web_api.py index 575fdbe..5b0d41f 100755 --- a/tests/e2e/test_seat_web_api.py +++ b/tests/e2e/test_seat_web_api.py @@ -734,7 +734,7 @@ operations: ) as response: extension_styles = response.read().decode("utf-8").replace("\r\n", "\n") 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: assistant_script = response.read().decode("utf-8") with self.opener.open( @@ -796,8 +796,8 @@ operations: self.assertNotIn("滚动查看更多", html) self.assertIn('aria-label="发送消息"', html) self.assertIn('id="assistantTranscript"', html) - self.assertIn('href="/assistant-chat.css?v=codex-assistant-29"', html) - self.assertIn('src="/assistant-chat.js?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-30"', html) self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html) self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html) self.assertNotIn('"admin"', console_script) @@ -1075,11 +1075,11 @@ operations: self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script) self.assertIn('canvas.addEventListener("webglcontextlost"', 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.js?v=local-first-66', service_worker) - self.assertIn('/assistant-chat.css?v=codex-assistant-29', service_worker) - self.assertIn('/assistant-chat.js?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-30', 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('"/app.js?v=dual-seat-133"', service_worker) diff --git a/tests/integration/test_mode_device_additions.py b/tests/integration/test_mode_device_additions.py new file mode 100644 index 0000000..efc8bd3 --- /dev/null +++ b/tests/integration/test_mode_device_additions.py @@ -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"] diff --git a/tests/unit/assistant_stream_runtime.test.cjs b/tests/unit/assistant_stream_runtime.test.cjs index 7d7daa6..c57a225 100644 --- a/tests/unit/assistant_stream_runtime.test.cjs +++ b/tests/unit/assistant_stream_runtime.test.cjs @@ -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); }); +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 () => { const positions = [0, 30, 50, 100]; const events = [ diff --git a/tests/unit/test_assistant_frontend_contract.py b/tests/unit/test_assistant_frontend_contract.py index 52df94c..f5f8489 100644 --- a/tests/unit/test_assistant_frontend_contract.py +++ b/tests/unit/test_assistant_frontend_contract.py @@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface(): assert 'aria-label="AI助手对话"' in html assert 'placeholder="向 AI助手提问或交代工作"' in html assert 'aria-label="发送消息"' in html - assert 'href="/assistant-chat.css?v=codex-assistant-29"' in html - assert 'src="/assistant-chat.js?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-30"' in html 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") - assert 'const CACHE_VERSION = "cognitive-agent-pad-v283"' in service_worker - assert '"/assistant-chat.css?v=codex-assistant-29"' in service_worker - assert '"/assistant-chat.js?v=codex-assistant-29"' in service_worker + assert 'const CACHE_VERSION = "cognitive-agent-pad-v284"' in service_worker + assert '"/assistant-chat.css?v=codex-assistant-30"' 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(): @@ -214,7 +214,7 @@ def test_assistant_carries_only_the_active_mode_draft_reference_to_follow_up_tur assert "rememberModeDraftContext(draft)" in assistant assert "activeModeDraftContext = loadActiveModeDraftContext()" 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(): diff --git a/tests/unit/test_energy_monitor_frontend_contract.py b/tests/unit/test_energy_monitor_frontend_contract.py index d24cc61..40c7f8b 100644 --- a/tests/unit/test_energy_monitor_frontend_contract.py +++ b/tests/unit/test_energy_monitor_frontend_contract.py @@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None: ): assert asset in html 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" ) - 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: diff --git a/tests/unit/test_firmware_workbench.py b/tests/unit/test_firmware_workbench.py new file mode 100644 index 0000000..e902c9a --- /dev/null +++ b/tests/unit/test_firmware_workbench.py @@ -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() diff --git a/tests/unit/test_firmware_workbench_frontend_contract.py b/tests/unit/test_firmware_workbench_frontend_contract.py new file mode 100644 index 0000000..4ab4783 --- /dev/null +++ b/tests/unit/test_firmware_workbench_frontend_contract.py @@ -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 '