12 Commits

Author SHA1 Message Date
Miayui 448c8b31a0 feat: improve mode and peripheral orchestration 2026-09-21 00:13:47 +08:00
Miayui 89707a42e2 feat: make mode creation preview-only and cache-safe 2026-09-20 19:32:03 +08:00
Miayui bfff1b515c feat: integrate learned peripherals into mode creation
Expose safe NFC-bound infrared actions to semantic scene composition, require flashing-light actions for romantic and Mid-Autumn modes, and preserve mixed CAN/IR mode flows.

Fix NFC device reassignment and retry extension/toy MQTT publication when an active card's binding catalog changes.
2026-09-20 16:37:19 +08:00
Miayui 904f5f8a6c docs: streamline project README 2026-09-18 14:48:07 +08:00
Miayui 24aa1371dc fix: optimize firmware notification action UI 2026-09-18 13:22:05 +08:00
Miayui 0ab1d76781 feat: notify vehicle of published firmware 2026-09-18 12:52:36 +08:00
Miayui e920a92505 Fix workbench dark theme and enlarge dialogs 2026-09-18 09:11:59 +08:00
Miayui c812a1a595 Merge origin/feature/N7Web into feature/N7Web
Integrate the remote firmware workbench, OTA delivery, and timed mode schedules while preserving the local N7 scene and runtime changes.
2026-09-17 12:46:51 +08:00
vee ab8e43050c feat: add seat firmware workbench and OTA delivery 2026-09-16 17:55:30 +08:00
vee 3eeca2ea75 merge: integrate remote MQTT routing with seat-scoped dynamic modes 2026-09-12 15:10:16 +08:00
vee d8dad22ec0 feat: add seat-scoped static and dynamic assistant modes 2026-09-12 14:56:30 +08:00
vee d09b5967f8 fix: restore mode clicks and compose seat-aware assistant scenes 2026-09-12 13:33:38 +08:00
167 changed files with 38388 additions and 1159 deletions
+9
View File
@@ -32,4 +32,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/
+219 -226
View File
@@ -1,294 +1,287 @@
# Cognitive Agent
面向车载执行器的认知编排 Agent 单仓工程。系统把自然语言请求转换为可校验的语义配置,再由确定性的领域插件编译为 CAN 命令计划,最后经安全策略和执行器下发。
面向车载执行器的认知编排单仓工程。系统把自然语言或结构化请求转换为可校验的语义配置,由领域插件确定性编译为 `CommandPlan`,经过平台安全门禁后以 dry-run、模拟台架或真实 CAN 方式执行,并保存可审计结果。
Web 控制台与同源 API 由同一个 Python 服务提供。本地启动:
当前仓库以腾势 N7 / T2G 座舱演示和硬件台架为主。默认环境不会打开真实硬件;气候、天幕和氛围灯领域的矩阵目前也只允许虚拟执行。
```bash
.\.venv\Scripts\python.exe .\tools\start_n7web.py
```
## 能做什么
启动脚本会先安装缺失的前端依赖并执行 Vue 3 + Vite 生产构建,然后启动 `8000` 主服务和独立的 `8090` 只读模式卡服务。已经有可复用的 `apps/web-console/dist` 时,可使用 `--skip-web-build` 跳过本次构建。
- 同源 Python HTTP 服务与 Vue 3 + Vite Web 控制台。
- 能力目录驱动的自然语言编排、模式卡片、跨域场景和 NDJSON 助手流。
- `IntentIR`、`FunctionConfig`、`SceneConfig`、`CommandPlan`、`TraceEvent` 和 `ExecutionReport` 公共契约。
- 座椅、空调、天幕、氛围灯四个领域插件,以及平台侧门窗、插座、阅读灯等原生 CAN 能力。
- 虚拟 CAN、模拟硬件、T2G ZLG CAN 台架和双通道车身 CAN 接入。
- 语音输入、MediaPipe 手势、NFC/红外外设、模式目录同步和只读固件 OTA 服务。
### Vue 3 + Vite 前端
AI 模型只能提出受契约约束的语义计划,不能生成或发送原始 CAN 帧。真实执行仍由服务端的能力白名单、参数 Schema、硬件授权、安全策略、互斥和反馈检查共同决定。
前端根应用位于 `apps/web-console/src/App.vue`,入口为 `src/main.js`。Vue 和 Vite 均由 npm 锁定版本管理,浏览器不再依赖 CDN 或全局 `window.Vue`。现有 MQTT、NFC、语音、模式卡和车辆控制脚本作为兼容运行层继续使用原 DOM 契约,由 Vue 根应用承载页面结构。
单独进行前端开发时,先保持 Python 主服务运行,再启动 Vite:
```powershell
cd .\apps\web-console
npm ci
npm run dev
```
开发页面为 `http://127.0.0.1:5173/`,`/api` 与 `/artifacts` 会代理到 `http://127.0.0.1:8000`。生成生产静态资源:
```powershell
cd .\apps\web-console
npm run build
```
构建输出位于 `apps/web-console/dist`;Python 主服务检测到该目录后会优先提供生产构建。上述流程不会改变主页的 `8000` 端口,也不会改变模式卡服务的 `8090` 端口。
Windows PowerShell 未安装 `make` 时可直接运行:
```powershell
$env:PYTHONPATH = "src;domains/seat/src"
python -m cognitive_agent.server
```
## 建设阶段
- 阶段一:每个领域独立完成能力定义、功能配置生成、配置校验、CAN 编译和单执行器测试。
- 阶段二:由中央编排器生成 `SceneConfig`,引用多份 `FunctionConfig`,按依赖关系、时间偏移和失败策略组合执行。
- 最终形态:Web 控制台接收一句话请求,展示结构化决策摘要、配置文件、校验结果和执行状态。
## 核心约束
1. LLM 只能生成 `IntentIR`、`FunctionConfig` 或 `SceneConfig`,不能直接生成或发送原始 CAN 帧。
2. `FunctionConfig` 只允许包含一个 `domain`;跨域需求必须使用 `SceneConfig` 引用多份领域配置。
3. CAN 编译必须是确定性的:相同配置、相同平台标定和相同插件版本必须生成相同 `CommandPlan`。
4. DBC、通道定义和安全标定由集成人员在 `platforms/` 统一维护;领域成员只维护语义信号映射。
5. 所有真实执行必须经过公共 Schema 校验、领域校验、安全策略、dry-run 和审计记录。
6. UI 展示可审计的决策摘要和阶段事件,不展示模型私有思维链。
## 目录
## 处理链路
```text
.
├── apps/ # API 服务、Web 控制台和内置手势模型资源
├── artifacts/ # 每次请求的生成物和执行记录(不入库)
├── config/ # 全局环境和策略配置
├── contracts/ # 冻结的公共 Schema、示例和版本规则
├── deploy/ # 容器及部署资源
├── docs/ # 架构、流程、治理和 ADR
├── firmware/t2g/ # 仓库内统一座椅 IAR 工程与可烧录产物
├── domains/ # 可独立开发、测试、交付的领域插件
│ ├── _template/ # 新领域模板
│ ├── seat/ # 座椅
│ ├── climate/ # 空调
│ ├── ambient_light/ # 氛围灯
│ └── roof/ # 天幕/遮阳帘/调光玻璃
├── platforms/ # 车型、DBC、总线和安全标定
├── src/cognitive_agent/ # 中央 Agent、编排器、运行时和基础设施
├── tests/ # 跨域契约、集成和端到端测试
└── tools/ # 脚手架、校验和开发工具
```
完整依赖关系和处理链路见 [总体架构](docs/architecture/overview.md),成员交付步骤见 [领域交付流程](docs/workflows/domain-delivery.md),修改边界见 [所有权规则](docs/governance/ownership.md)。
整体排期和阶段准入条件见 [集成路线](docs/workflows/integration-roadmap.md)。
## 配置处理链路
```text
用户文本
-> GPT + Capability Registry + 当前会话状态
-> 通用能力请求(领域、能力、作用域、操作和参数)
-> IntentIR(意图抽取、领域识别、歧义与置信度)
-> 单领域:FunctionConfig
-> 多领域:SceneConfig + N 个 FunctionConfig
-> 公共 Schema 校验 + 领域语义校验
用户请求
-> 能力目录 / 会话状态 / Codex 或本地解析
-> IntentIR
-> FunctionConfig(单领域)或 SceneConfig(跨领域)
-> 公共 Schema + 领域语义校验
-> 领域 Compiler -> CommandPlan
-> 平台安全校验 -> 调度 -> CAN Gateway
-> 回读/超时/结果 -> TraceEvent + ExecutionReport
-> 平台安全校验 -> dry-run / simulated_hardware / physical CAN
-> 回读、超时和审计 -> TraceEvent + ExecutionReport
```
运行时产物统一保存在:
运行时产物位于 `artifacts/runs/<request_id>/`,典型结构如下:
```text
artifacts/runs/<request_id>/
├── intent.yaml
├── scene.yaml # 仅跨域请求存在
├── functions/<domain>.yaml
├── command-plans/<domain>.yaml
├── trace.jsonl
└── execution-report.yaml
intent.yaml
scene.yaml # 跨域请求才有
functions/<domain>.yaml
command-plans/<domain>.yaml
trace.jsonl
execution-report.yaml
```
`artifacts/` 中禁止写入 API Key、完整隐私文本或未脱敏的车辆标识。
不要在 `artifacts/` 写入 API Key、完整隐私文本或未脱敏车辆标识。
## 可执行领域
## 当前能力边界
座椅与空调等车身设备使用同一 ZLG CAN 盒双通道联动时,参见 [T2G 座椅与车身双通道 CAN 联动](docs/workflows/t2g-cabin-dual-channel-can.md)。
| 能力面 | 当前状态 | 事实来源 |
| --- | --- | --- |
| `seat` | 虚拟矩阵闭环;T2G 台架使用独立硬件映射和校准 | [`domains/seat/`](domains/seat/)、[台架流程](docs/workflows/t2g-seat-hardware-bench.md) |
| `climate` | dry-run;支持分区温度、电源、风量、A/C、循环、风向和前除霜 | [`domains/climate/`](domains/climate/) |
| `roof` | dry-run;支持透光率、后遮阳帘位置和自动调光 | [`domains/roof/`](domains/roof/) |
| `ambient_light` | dry-run;支持开关、RGB、亮度和矩阵场景模式 | [`domains/ambient_light/`](domains/ambient_light/) |
| 原生车身 CAN | 按平台和运行时能力目录启用,不等同于领域插件 | [`config/native_can_capabilities/`](config/native_can_capabilities/)、[`platforms/`](platforms/) |
| 模式卡片 | `config/scene_templates/` 提供内置模板;用户模式持久化到 `artifacts/mode-profiles/` | [模式时间轴](docs/mode-schedules.md) |
当前已接入四个相互隔离的可执行领域:
`platforms/reference_vehicle` 的 `execution_enabled` 为 `false`。`platforms/t2g_seat_bench` 和 `platforms/t2g_cabin_bench` 才是仓库内允许配置真实执行的台架平台,但仍需硬件开关、授权令牌、总线配置和反馈校验全部满足。
- 座椅:所有热管理、位置移动和姿态预设能力仅面向副驾;未指定座位时默认副驾,显式主驾请求拒绝执行。
- 空调:基于 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的电源、18–32℃ 分区温度、0–7 档风量、A/C、内外循环、风向和前除霜。
- 天幕:基于同一矩阵的 0–100% 透光率、后遮阳帘绝对位置和自动调光。
- 氛围灯:基于同一矩阵的总开关、24-bit RGB、0–100% 亮度和 ECU 场景模式。
## 快速开始
一句话只涉及一个领域时直接生成一份 `FunctionConfig`;涉及多个领域时生成一份 `SceneConfig` 引用各领域独立的 `FunctionConfig`,并由各自编译器产生 `CommandPlan`。三份新增矩阵仍缺少车型适用性、完整计数器、校验和和发送周期,因此与座椅演示一样只允许虚拟执行。
### 环境要求
```bash
make validate
make test
make dev
```
- Python 3.11 或更高版本。
- Node.js 与 npm(构建 Web 控制台)。
- Codex CLI `0.150.0-alpha.8`,用于内置 AI 助手。
- 真实台架另需 ZLG 驱动/桥接程序、对应 DBC、IAR 工具链和硬件授权。
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器用于未配置 GPT,或 GPT/`auto` 模式因 API Key、模型权限、网络或结构化响应异常而失败时的安全降级;结果会明确标记 `local_rules_fallback`。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
### 安装(PowerShell)
### Codex AI 助手
```powershell
py -3.11 -m venv .venv
.\.venv\Scripts\python.exe -m pip install --upgrade pip
.\.venv\Scripts\python.exe -m pip install -e ".[test,dev,api,can,llm,ir]"
首页 AI 助手以长驻 `codex app-server --stdio` 为会话与推理核心。浏览器仍只连接同源 `/api/assistant/stream`,不会接触 Codex 协议或凭据;后端把 App Server 的 thread、turn 和 item 事件转换为 NDJSON 流。普通问题由 Codex 直接回答。涉及本工程能力时,`/api/assistant/capabilities`、回合上下文和只读 MCP 工具 `n7_vehicle.get_n7web_capabilities` 都从同一个运行时能力注册表生成,座舱模式、NFC 绑定红外外设及 CAN0/CAN1 执行绑定会随实际状态更新,不允许模型凭常识猜测。常见能力询问直接由本地权威目录回答,无需等待模型生成。
cd apps\web-console
npm ci
npm run build
cd ..\..
显式及复杂车辆需求通过唯一放行的 `n7_vehicle.execute_cockpit_request` MCP 工具进入工程。服务端把一次性工具令牌绑定到本回合原始指令;Codex 可以为原子设备控制提交结构化语义计划,但不能提交新的自然语言指令或原始 CAN。计划必须通过原文证据、能力白名单、目标和参数契约校验,本地确定性解析结果优先,之后仍进入既有 Schema、硬件授权、安全预检、互斥和 CAN 执行链。模型不能扩展用户未请求的控制,也不能覆盖执行模式和硬件授权。
“车上有点热”“车里有点冷”等与受限模式完整匹配、且不包含否定或知识问法的高置信度舒适请求,使用 N7Web 内部快速路由:分别归一化为空调开启、全车 23℃/26℃、2 档风量等确定性指令,绕过模型和 MCP 往返以尽快给出进度与结果。该快速路由只缩短意图理解路径,不绕过能力注册、Schema、执行模式、硬件授权、安全预检、互斥、CAN 编译或反馈确认;其他隐式、复合或含糊请求仍交给 Codex 判断,并受上述 MCP 边界约束。
能力目录中的 `service_surfaces` 仅用于显示语音输入、手势输入、NFC 绑定、座椅自标定和自修复等服务的运行状态,条目固定标记为 `assistant_access=informational`。这里的 `available=true` 只表示对应服务面可用,不表示 AI 助手可以调用它;可执行边界以 `capabilities`、`modes`、`learned_peripherals` 中的 `assistant_callable` 以及服务端再次校验结果为准。
本地运行前需要安装并登录固定兼容版本的 Codex CLI:
```bash
npm install --global @openai/codex@0.150.0-alpha.8
codex login status
make dev
```
也可以让 Codex 使用进程环境中的 `OPENAI_API_KEY`。未配置自定义地址时,N7Web 会在 App Server 没有缓存账户的情况下通过 API-key 登录;配置非官方 `OPENAI_BASE_URL` 时,会自动为线程注入完整的 OpenAI-compatible provider,使 Codex 确实请求该地址,而不是回落到 `api.openai.com`。助手默认选择低推理强度的 `gpt-5.6-luna`,可用 `CODEX_ASSISTANT_MODEL` 覆盖;`CODEX_ASSISTANT_REASONING_EFFORT` 默认为 `low`,`CODEX_ASSISTANT_TURN_TIMEOUT_SECONDS` 默认为 300 秒,`CODEX_ASSISTANT_WORKSPACE` 用来指定隔离的只读工作目录。当前工程的常用场景登记在 `config/scene_templates`:明确名称会直接做严格别名匹配;“适合/用于/用来……的功能模式”等用途描述会先通过受限语法提炼为简洁模式名,再做同样的严格匹配,不使用子串或相似度猜测。命中且没有同名已保存模式时,模板会在本地复制为新的待保存草稿,不调用 Codex,也不会修改内置模板;草稿仍会经过实时能力契约、完整性、CAN 编译和 dry-run 校验。未命中的长尾名称继续进入独立的单轮结构化 Scene Composer;该路径默认超时 45 秒,已验证的候选计划在当前进程内按车型和能力契约版本缓存 3600 秒,同场景跨用户并发请求会合并,但每个请求仍会独立经过实时安全校验。可分别通过 `CODEX_ASSISTANT_SCENE_TIMEOUT_SECONDS`、`CODEX_ASSISTANT_SCENE_CACHE_TTL_SECONDS`、`CODEX_ASSISTANT_SCENE_CACHE_MAX_ENTRIES` 和 `CODEX_ASSISTANT_SCENE_MAX_CONCURRENT_PLANS` 调整超时、缓存和并行规划上限(默认 8)。为避免兼容服务的 WebSocket 断流造成长时间重试,自定义 provider 默认关闭 WebSocket,并把模型流重试限制为一次;可通过 `CODEX_ASSISTANT_DISABLE_WEBSOCKETS` 和 `CODEX_ASSISTANT_STREAM_MAX_RETRIES` 调整。
语音识别是可选安装项:
启动采用失败关闭语义:CLI 缺失、App Server 握手失败、模型认证配置缺失/登录失败,或有效 MCP 配置未能收敛并验证为仅启用 `n7_vehicle` 时,`cognitive_agent.server` 会直接启动失败,不会继续提供一个看似可用的降级网页。进程启动后若 App Server 退出,`/api/health` 返回 `503`,`/api/assistant/status` 显示不可用;凭据被服务端撤销、模型权限、网络或生成失败等只有真实模型请求才能发现的问题,则由当前问答流返回 `assistant_model_failed`。这些情况都不会静默回退到旧的正则问答路由;本地能力快答和冷热快速控制仍只按各自严格的确定性条件工作。
App Server 的协议与审批字段以 [Codex App Server 官方文档](https://learn.chatgpt.com/docs/app-server) 和 [Codex 配置参考](https://learn.chatgpt.com/zh-Hans/docs/config-file/config-reference) 为准。线程级 `approvalPolicy=never` 与 MCP 工具级 `default_tools_approval_mode=approve` 作用于不同边界:前者禁止命令/文件修改请求进入交互审批,后者仅避免受限的 N7Web MCP 工具产生前端无法处理的第二次审批;真实执行授权仍完全由 N7Web 的一次性令牌、原文绑定和车辆安全链决定。
### AI 助手语音输入、快捷模式与手势控制
首页左侧的 Codex AI 助手接受普通问题、工程工作和车辆控制需求;Enter 发送,Shift+Enter 换行。观影、午休和零重力是可直接调用的快捷模式,“保存当前”可把当前副驾驶姿态保存为命名模式。能力快答和高置信度舒适请求走确定性快速链,其他请求由 Codex App Server 理解后再通过工程能力与安全边界执行。
语音入口分为两种明确模式:用户点击输入框右侧的麦克风按钮时,以 `manual` 模式立即开始本地中文录音,不要求也不剥离唤醒词;浏览器已获得麦克风权限、AI 助手处于展开状态且页面可见时,则自动进入低干扰的 `wake` 待机,只在识别到“你好,小迪”后展示并继续接收问题。首次使用仍需通过一次手动点击完成浏览器麦克风授权。前端优先选择 `TAKSTAR V2`,使用 `AudioWorklet` 在音频线程采集(旧浏览器才降级到 `ScriptProcessor`),并通过 `X-Speech-Input-Mode: manual|wake` 将两条链路显式隔离。
累积音频按节流窗口发送到同源 `/api/speech/preview`:手动模式直接把未语义清洗的实时转录显示在输入框,唤醒模式在命中前不改动输入框、命中后只显示唤醒词之后的内容,partial 阶段均不会提交或执行。浏览器检测到静音端点后立即停止音轨,再把 16 kHz PCM WAV 发送到 `/api/speech/transcribe` 生成权威 final。服务端使用 Silero VAD 保留句首/句尾保护区,SenseVoiceSmall 负责声学识别,统一 `TranscriptAssembler` 负责 final commit、顺序、重复回调和保守的相邻重叠消除。
最终响应把不同信任层明确拆开:`captured_text` 保留完整声学识别结果;`raw_text` 在 `manual` 模式保留完整原文,在 `wake` 模式仅剥离已确认的唤醒词;`assistant_text` 在保留普通问答的前提下处理重复、结巴、false start、修正、撤销、否定、多设备、多数字和小数,供 AI 助手输入框使用;`clean_text/control_text`、结构化 `commands`、`cancelled_fragments` 和 `execution_eligible` 继续作为严格车控旁路。未知能力或非车控内容不会被清空,也不会绕过 Codex 助手直接执行。听写本身不自动发送消息、不自动执行控制,也不保存录音。首次部署:
```bash
python -m pip install -e ".[speech]"
python tools/install_sensevoice_model.py
```powershell
.\.venv\Scripts\python.exe -m pip install -e ".[speech]"
.\.venv\Scripts\python.exe tools\install_sensevoice_model.py
```
安装脚本同时校验并部署 `artifacts/models/silero-vad/silero_vad.onnx`。默认使用完整精度 SenseVoice、`language=zh`、ITN 和最多 8 个 CPU 线程;VAD 针对短会话使用阈值 `0.45`、最短语音 `0.16s`、结束静音 `0.65s`、最长分段 `12s`、前置 `0.25s`、后置 `0.35s`。可通过 `SPEECH_MODEL_*`、`SPEECH_LANGUAGE`、`SPEECH_USE_ITN`、`SPEECH_NUM_THREADS`、`SPEECH_PROVIDER` 与 `SPEECH_VAD_*` 覆盖;`SPEECH_WAKE_WORD_ENABLED` 控制唤醒门,`SPEECH_WAKE_WORDS` 使用 `|` 分隔主唤醒词和别名。
配置模型认证时使用 `OPENAI_API_KEY`,或让 Codex CLI 使用已登录账户。`.env` 和 `.env.auth` 已被 Git 忽略;可参照 [`config/environments/`](config/environments/) 中的示例配置。不要把密钥提交到仓库。
当前 sherpa-onnx 版本支持 SenseVoice 的 `hr_lexicon`/`hr_rule_fsts`,只有同时提供经过验证的文件时才会启用:`SPEECH_HR_LEXICON` 和 `SPEECH_HR_RULE_FSTS`。未对非 transducer 的 SenseVoice 使用 hotword decoding。可插拔 `RecoveryRecognizer` 默认关闭,只为将来让 uncertain case 使用二次识别,不会下载大型模型。
### 启动完整本地演示
诊断时设置 `SPEECH_DEBUG=1` 输出按 `utteranceId`、segment 边界、raw final、拼接前后、理解结果、Intent 与阶段耗时组织的 JSON 日志;只有额外设置 `SPEECH_DEBUG_RESPONSE=1` 才把 trace 返回浏览器。前端访问参数 `?speechDebug=1`(或 localStorage 同名值)启用控制台日志,生产默认不打印。回归与指标基准:
推荐使用启动器。它会管理主服务、只读模式卡片服务和只读 OTA 服务:
```bash
pytest tests/unit/test_speech_transcription.py tests/unit/test_transcript_assembler.py
python tools/benchmark_speech_pipeline.py --audio-dir artifacts/tmp/speech-regression-audio
```powershell
.\.venv\Scripts\python.exe tools\start_n7web.py --skip-web-build
```
麦克风仅可用于安全来源:本机使用 `http://127.0.0.1:8000`,远程使用 HTTPS。
默认端口:
顶部“手势控制”打开同页抽屉。浏览器在本机完成 MediaPipe 推理,识别结果仍通过同一自然语言编排、安全校验与执行链路处理。运行必需资源已经纳入本工程:
| 服务 | 地址 | 说明 |
| --- | --- | --- |
| 主服务和 Web 控制台 | `http://127.0.0.1:8000` | API、控制台、助手和车控入口 |
| 模式卡片目录 | `http://127.0.0.1:8090/n7/scenes` | HTTP/UDP 只读目录服务 |
| 固件 OTA | `http://127.0.0.1:8012` | 只读发布清单和下载 |
```text
apps/web-console/assets/gesture/gesture_recognizer.task
config/gesture/default.yaml
```
因此源码打包和容器构建不依赖相邻的 `seat_gesture_control` 目录。模型文件只保留一份,不包含原工程的测试视频、日志或验证输出。浏览器摄像头要求安全来源,本地使用 `http://127.0.0.1:8000`,远程部署使用 HTTPS;首次使用仍需浏览器授予摄像头权限。MediaPipe Web 运行时从 jsDelivr 按需加载,视频帧不上传到后端。
手势安全状态机采用张掌保持 1 秒激活、动作保持 600 ms 且至少 8 帧确认、单次运动 1.5 秒上限、连续 3 秒无有效动作自动锁止。换动作不需要重新张掌;握拳立即停止当前动作但保持激活;多手画面不执行动作。手势映射可在抽屉内自定义并保存在当前浏览器。
同一局域网的其他电脑需要体验时,使用 `make dev-lan` 监听全部网络接口。服务启动日志会打印实际 LAN 地址,例如 `http://172.20.10.5:8000`。前端和 API 使用同源访问,不需要 CORS,也不会把 `OPENAI_API_KEY` 发送到浏览器。若没有启用 `.env.auth`,任何可达设备都能触发服务端 GPT 请求,因此未认证的 LAN 模式只应在可信网络内临时演示。
### N7 模式卡片局域网目录
服务正常启动时,会把当前车型可见的工程预设模式和用户自定义模式原子同步到相邻目录 `../scenses/demo-scenes.json`,并提供无需登录、只读的 `GET /n7/scenes`。YAML 模式库仍是唯一真源;创建、更新、删除、恢复模式都会刷新 JSON,原有模式执行和安全校验链路不变。JSON 保留 `n7-scenes` v1 的 `id/label/background_time/description/actions` 字段,同时在动作中保留完整的 `domain/capability/target_scope/operation/parameters`,因此座椅、空调、天幕和氛围灯配置不会在导出时丢失。可用 `N7_SCENE_CATALOG_PATH` 或启动参数 `--scene-catalog` 覆盖输出路径。
主项目和客户端只读目录服务可以用一个监督脚本同时启动:
启动前端构建:
```powershell
.\.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` 时,脚本会停止另一个服务。
启动器支持 `--host`、`--port`、`--scene-port`、`--ota-port`、`--catalog`、`--server-id`、`--python` 和 `--dry-run`。使用 `--host 0.0.0.0` 前请确认网络可信,并为公网部署启用认证和 HTTPS。
同步由模式注册表的创建、更新、删除、恢复和显式重载事件触发,不会持续扫描目录。每次变更只重新生成并原子替换一份小型 JSON;独立目录服务在收到 HTTP 请求时才读取它。因此空闲时的额外开销主要是一个等待请求的 Python 进程和一个 UDP 发现线程,而不是持续的 CPU 或磁盘轮询。
健康检查:
前端通过 `POST /api/mode-profiles` 新增模式卡片,或通过 `POST /api/mode-profiles/{id}` 修改模式卡控制流后,主服务会先完成 YAML 持久化和 `/n7/scenes` 原子同步,再从同一份目录投影中按模式 ID 提取一个 `scenes[]` 元素,并在 1 秒内以约 333 ms 间隔向 MQTT `from_pc/scenes` 发布 3 次。每帧都是完全相同的更新后单卡 JSON,不包含目录外层或其他已有卡片,QoS 沿用车辆 MQTT 配置且 `retain=false`。删除和页面刷新只更新原有持久化/目录链路,不会触发该 Topic。
检测到已绑定外设的 NFC 卡时,主服务会按英文稳定 `device_id` 对已学习且启用的红外能力分组,并向 MQTT `extension/toy` 发布 3 帧相同的设备能力消息(约在 0、333、666 ms 发布,QoS 1、`retain=false`)。`extension_name` 使用中文显示名,`actions[].target_scope` 使用英文 `device_id`;卡片持续停留不会重复发送,移开后再次检测才会重新发送。该消息仅广播能力目录,不会触发红外发射。
车端需要控制这些 TCP 红外外设时,向同一个车辆 MQTT Broker 的 `extension/ctl` 发布 QoS 1、`retain=false` 的 JSON。PC 主服务固定订阅该 Topic;消息外层必须且只能包含 `domain`、`operation_count`、`actions`,其中 `domain` 为 `self_extension`,`operation_count` 必须与动作数量一致(1–64),`actions` 使用 `/n7/scenes` 中的红外动作格式:
```json
{
"domain": "self_extension",
"operation_count": 2,
"actions": [
{
"type": "infrared_emit",
"at_ms": 0,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": "toy-light-001",
"operation": "emit",
"parameters": {"action": "打开", "slot": 2}
},
{
"type": "infrared_emit",
"at_ms": 1000,
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": "toy-light-001",
"operation": "emit",
"parameters": {"action": "切换为蓝色", "slot": 5}
}
]
}
```powershell
Invoke-RestMethod http://127.0.0.1:8000/api/health
Invoke-RestMethod http://127.0.0.1:8000/api/assistant/status
```
服务在执行首条动作前会对完整控制流做原子鉴权:每一项都必须与本机目录中“已学习、已启用、具有持久 NFC 绑定”的 `device_id + action + slot` 完全一致;NFC 卡此时无需仍在感应区。执行前还会逐项重新鉴权,以阻止排队期间已被禁用或删除的能力。动作按 `at_ms` 排序后通过本机 TCP 红外网关串行下发,相邻发射至少间隔 800 ms;任一动作失败即停止剩余控制流。为避免重连误执行,retained 消息和 MQTT `dup` 重投消息都会被拒绝,消息体上限为 256 KiB。接收、鉴权、排队、执行结果和拒绝原因可在 `GET /api/extension/health` 的 `extension_control` 字段查看。
`/api/health` 依赖 Codex App Server 就绪;助手进程不可用时返回 HTTP `503`,不会伪装成可用服务。
如果只需单独启动 UDP 发现和只读目录服务,也可以在另一个 PowerShell 终端运行:
### 只启动主服务
需要自行运行前端开发服务器或不需要模式目录/OTA 服务时,可直接运行:
```powershell
$env:PYTHONPATH = "src;domains/seat/src"
.\.venv\Scripts\python.exe -m cognitive_agent.lan_scene_server --bind 0.0.0.0 --port 8090
.\.venv\Scripts\python.exe -m cognitive_agent.server
```
客户端向 UDP `8090` 发送 `N7_SCENES_DISCOVER_V1`,收到 `N7_SCENES_V1` 后访问 `http://<服务端IP>:8090/n7/scenes`。独立服务每次请求都读取最新的 JSON,并为只读接口开放 CORS;它不提供模式创建、修改、删除或执行能力。
前端热更新:
Linux 上使用 USBCANFD-200U 被动同步 CH1 车辆状态时,使用
`zlgcan-linux` 后端。USB 权限、60 MHz/500k/2M 位时序、只读配置和状态接口
验证见 [Linux ZLG 车辆状态接入](docs/workflows/linux-zlg-vehicle-state.md)。
有线 ZLG 与本机 NanoMQ 无线链路使用相同的通道角色:CAN0 只对应副驾座椅,
CAN1 对应空调、车窗、灯光、遮阳帘、主驾座椅等其余车辆状态和控制。车辆控制
后端不允许回退到 CAN0;MQTT 主题、原始 CAN 帧 JSON 合约和有线/无线共用安全
门禁见同一文档。
```powershell
cd apps\web-console
npm run dev
```
需要在终端独立核对双通道状态或下发一条明确的白名单语义命令时,可构建
`can_read_trans`。该程序由同一个 C++ 进程持有 ZLG 设备:CAN0 监听并控制副驾
座椅固件原生协议,CAN1 监听并控制其余车辆矩阵协议;默认只监听、默认不启用
适配器内部终端电阻,也不提供任意原始帧注入入口。`console --execute` 可在同一
设备会话中持续接收状态并交互下发 CAN0/CAN1 白名单命令。构建、命令示例、反馈
判定和安全限制见 [can_read_trans 双通道检测与控制](docs/workflows/can-read-trans.md)。
Vite 使用 `http://127.0.0.1:5173`,并把 `/api` 与 `/artifacts` 代理到 `http://127.0.0.1:8000`。生产构建输出为 `apps/web-console/dist`,主服务检测到该目录后优先提供构建产物。
## 认证与公网体验
在已安装 GNU Make 的环境中,`make dev` 等价于启动主服务;Windows PowerShell 推荐使用上面的显式 Python 命令,以确保使用仓库的 `.venv`。
公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。
## 开发与验证
生成认证材料:
先激活 `.venv`,或把下面的 `python` 替换为 `.\.venv\Scripts\python.exe`。仓库的 `Makefile` 使用 POSIX 风格环境变量,在 Windows 上也可以直接调用 Python 脚本。
```bash
PYTHONPATH=src python -m cognitive_agent.auth hash-password
PYTHONPATH=src python -m cognitive_agent.auth generate-secret
# 公共 Schema、领域清单、平台资源和生成 CAN 协议
make validate
# 全量 Python、前端契约和 CAN 协议回归
make test
# 仅前端运行时契约
make test-frontend
# 生成或检查共享 CAN 语义协议
make can-protocol-generate
make can-protocol-check
```
固定公网演示部署在 `https://511.kendeji.fun`。服务器使用 Docker Compose
运行应用,容器端口只绑定 `127.0.0.1:8511`,由 Caddy 终止 HTTPS 并反向
代理。部署结构、更新和运维命令见
[`deploy/server/README.md`](deploy/server/README.md)。本地 `make dev` 与
`make dev-lan` 仍分别用于回环地址和可信局域网开发。
不使用 `make` 时:
当前 dry-run 规划策略允许 GPT 在没有同名 preset 时,将可理解的模式目标分解为已注册原子能力并生成保守候选参数。候选值只用于模型理解演示,不能替代实车模式标定。
```powershell
.\.venv\Scripts\python.exe tools\validate_repository.py
.\.venv\Scripts\python.exe -m pytest
```
靠背/坐垫与坐垫/腿托支持演示级精确夹角,并可将当前五轴姿态保存为会话持久化的命名 `FunctionPreset`。保存操作不发 CAN;再次调用时先展开为五个原子电机动作,再进入同一编译、校验和虚拟执行链路。
仓库配置了 `pytest` 的 `contract`、`integration` 和 `hardware` markers。默认测试使用虚拟或模拟执行;真实台架测试必须按 [T2G 前端测试集](docs/testing/t2g-seat-frontend-test-suite.md) 的顺序执行,并显式确认硬件。
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
构建双通道 CAN 读取工具需要 CMake 和外部 CANFD SDK:
已验证的 T2G 加热、通风、四电机和原生动态模式能力已完整纳入 `firmware/t2g`,包括 IAR 工程及仓库内 `common/tviibh8m` 构建依赖。M0/M1/M2/M3 分别对应水平、坐盆、靠背和腿托;revision 13通过`0x306`反馈带斜向耦合补偿的木马模式与四轴全程联动动态摇篮的阶段/循环状态。木马模式围绕中间姿态在`[270,250,460,400]`和`[370,130,540,460]`之间连续俯仰往复:坐盆峰峰行程120 Hall、约18 mm垂直分量,水平峰峰行程100 Hall、90%巡航Duty,用额外水平行程抵消坐盆斜向运动约16.4 mm的反向水平分量;参数集中在`seat_dynamic_mode_config.h`并由N7长期库镜像。动态模式收到一次启动命令后由ECU连续循环,收到停止/全停命令或累计运行10分钟时停止;目标到位式阶段在底层堵转/无运动保护继续生效的前提下使用独立保护窗口。真实硬件请求先被动核对新版周期反馈与 480/374/917/647 Hall 量程,未检测到匹配固件时默认通过 IAR C-SPY 自动烧写并复验,再发送 CAN 并等待真实回读。当前前端、能力注册、编译、安全门禁和状态反馈只允许副驾 `passenger/front_right`;固件中的 `Driver_Seat_*` 仅是板端既有协议字段名。构建、ZLG bridge、接线、安全门禁、前端测试集和已知量程归零见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md) 与 [前端能力测试集](docs/testing/t2g-seat-frontend-test-suite.md)。
```bash
make can-read-trans
make can-read-trans-test
```
## API 入口
浏览器只访问同源服务。下面是主要集成面,完整路由以 [`src/cognitive_agent/server.py`](src/cognitive_agent/server.py) 为准:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/health` | 服务与助手健康状态 |
| `GET` | `/api/runtime` | 当前运行时、平台和执行模式摘要 |
| `GET` | `/api/assistant/status` | Codex App Server 状态 |
| `GET` | `/api/assistant/capabilities` | 权威能力目录快照 |
| `POST` | `/api/requests` | 提交自然语言或结构化车控请求 |
| `POST` | `/api/requests/preview` | 只规划/校验,不执行 |
| `POST` | `/api/assistant/stream` | AI 助手 NDJSON 流 |
| `GET` | `/api/hardware/seat-state` | 座椅状态快照 |
| `GET` | `/api/hardware/vehicle-state` | 车身状态快照 |
| `GET` | `/api/vehicle-display-sync` | 网页与车机任务同步 |
| `GET` | `/n7/scenes` | 只读模式目录 |
`/api/internal/codex/vehicle-control` 仅供本机 Codex MCP 回调使用,需要一次性令牌和原始请求绑定,不是浏览器或第三方 API。语音、手势、NFC、固件工作台和自修复接口也由服务端权限门禁保护。
## 配置与安全
默认 [`config/default.yaml`](config/default.yaml) 使用 `dry_run`、结构化输出和 `artifacts/runs`。真实硬件必须同时满足:
1. 平台 manifest 的 `execution_enabled: true`。
2. 对应 CAN 后端、通道、DBC 和校准可用。
3. 服务端硬件开关、授权令牌和安全策略通过校验。
4. 领域映射明确允许硬件执行,并收到真实反馈。
模拟台架显式使用:
```powershell
$env:COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED = "true"
```
模拟结果会标记为 `simulated_hardware`,不会打开真实 CAN、ZLG 或 IAR。真实 T2G 座椅需要参考 [`config/environments/t2g_seat_bench.example.yaml`](config/environments/t2g_seat_bench.example.yaml) 和 [硬件台架流程](docs/workflows/t2g-seat-hardware-bench.md);不要仅通过前端按钮声称已经获得硬件权限。
语音和摄像头只在安全来源工作:本机使用 `http://127.0.0.1:8000`,远程部署必须使用 HTTPS。局域网开发命令如下:
```bash
make dev-lan
```
该模式会监听全部网络接口,任何可达设备都可能触发服务端请求,只适合可信网络临时演示。
## 代码结构
```text
apps/web-console/ Vue 入口、兼容运行层、静态资源和 PWA
src/cognitive_agent/ HTTP 服务、应用编排、运行时、基础设施和助手适配
domains/*/ 领域插件:能力、Schema、映射、编译器和测试
platforms/*/ 车型、DBC、总线和校准;定义执行边界
contracts/schemas/ 冻结的公共 JSON Schema
config/ 环境、策略、车辆、模式和原生能力配置
tests/ 跨域契约、集成、端到端和前端测试
tools/ 启动器、仓库校验、协议生成、HIL 和诊断工具
firmware/t2g/ T2G 固件工程和可烧录依赖
deploy/ Docker Compose、服务器和 Kubernetes 占位配置
docs/ 架构、决策、治理、工作流和测试说明
artifacts/ 本地运行产物、模型、HIL 报告和持久化数据
```
依赖边界:领域插件只依赖 SDK 和公共契约,不直接调用 LLM、HTTP 或 CAN;编排层只组合语义配置,不按 CAN signal 编排;DBC 和安全标定由 `platforms/` 统一维护。详见 [总体架构](docs/architecture/overview.md)、[领域交付流程](docs/workflows/domain-delivery.md) 和 [所有权规则](docs/governance/ownership.md)。
## 可选能力
### 语音
安装 `.[speech]` 并运行 `tools/install_sensevoice_model.py` 后,控制台支持手动录音和唤醒词待机。预览和最终识别接口为 `/api/speech/preview` 与 `/api/speech/transcribe`;录音不持久化。调试使用 `SPEECH_DEBUG=1`,默认不向浏览器返回 trace。
### 手势
手势模型随仓库提供:`apps/web-console/assets/gesture/gesture_recognizer.task`。推理在浏览器完成,视频帧不上传;需要摄像头权限和安全来源。
### NFC / 红外外设
安装 `.[ir]` 后运行主服务。NFC、GSCU1B TCP 网关、红外槽位目录和 MQTT 回退由 `/api/extension/*` 管理;学习、绑定和发射均受服务端能力目录与持久 NFC 绑定约束。
### Android PAD
```powershell
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1 -Stop
```
脚本使用模拟台架,不打开真实硬件执行。
## 部署
服务器 Docker Compose 和 Caddy 反向代理说明见 [`deploy/server/README.md`](deploy/server/README.md)。镜像默认以非 root 用户、只读根文件系统运行,应用只绑定服务器回环地址,再由 Caddy 提供 HTTPS。部署时通过私密环境文件注入认证材料和模型凭据,不把 `.env`、`.env.auth`、Codex `auth.json` 或 API Key 放入镜像和版本库。
`deploy/docker/` 记录后续拆分 API/worker/Web 的边界;`deploy/k8s/` 目前是 Kubernetes 资源占位文档。真实 CAN 设备只能挂载到明确授权的执行器环境。
## 相关文档
- [总体架构](docs/architecture/overview.md)
- [T2G 座椅与车身双通道 CAN](docs/workflows/t2g-cabin-dual-channel-can.md)
- [T2G 座椅真实硬件台架](docs/workflows/t2g-seat-hardware-bench.md)
- [T2G 座椅前端测试集](docs/testing/t2g-seat-frontend-test-suite.md)
- [中文语音控制链路](docs/speech-recognition.md)
- [模式卡片与时间轴](docs/mode-schedules.md)
- [领域交付流程](docs/workflows/domain-delivery.md)
- [所有权与变更边界](docs/governance/ownership.md)
- [服务器部署](deploy/server/README.md)
- [Web 控制台细节](apps/web-console/README.md)
## 项目状态
这是面向演示、集成和受控台架验证的工程,不是未经适配即可部署到任意车型的通用车控产品。提交修改前至少运行 `validate` 和相关领域测试;涉及 DBC、平台标定、硬件执行或公共 Schema 的变更,需要同时更新对应专题文档与回归用例。
+266 -22
View File
@@ -17,6 +17,8 @@ function createConversationId() {
return createOpaqueId("conv");
}
const HARDWARE_CLIENT_ID_PATTERN = /^hwc-[a-z0-9-]{8,80}$/;
function loadConversationId() {
const stored = sessionStorage.getItem("cognitive-agent.conversation-id");
// A browser runtime restart is a new virtual run. Do not reuse the old
@@ -33,12 +35,19 @@ function loadConversationId() {
function loadHardwareClientId() {
const key = "cognitive-agent.hardware-client-id";
const stored = localStorage.getItem(key);
if (/^hwc-[a-z0-9-]{8,80}$/.test(stored || "")) return stored;
if (HARDWARE_CLIENT_ID_PATTERN.test(stored || "")) return stored;
const clientId = createOpaqueId("hwc");
localStorage.setItem(key, clientId);
return clientId;
}
function validatedHardwareClientId() {
if (!HARDWARE_CLIENT_ID_PATTERN.test(state.hardwareClientId || "")) {
state.hardwareClientId = loadHardwareClientId();
}
return state.hardwareClientId;
}
function loadControlTransport() {
const stored = localStorage.getItem("cognitive-agent.control-transport");
return ["wired_can", "wireless_mqtt"].includes(stored) ? stored : "wired_can";
@@ -5131,6 +5140,7 @@ const MODE_DOMAIN_LABELS = {
window: "车窗",
front_light: "车灯",
vehicle_status: "车辆状态",
android: "车机媒体",
};
const MODE_TARGET_LABELS = {
@@ -5140,6 +5150,7 @@ const MODE_TARGET_LABELS = {
front_right: "右前 / 副驾",
rear_left: "左后",
rear_right: "右后",
headunit: "车机",
front_row: "前排",
rear_row: "后排",
vehicle: "全车",
@@ -5166,7 +5177,18 @@ function modeOperationParameters(operation) {
return Object.entries(parameters);
}
function formatModeOperationValue(name, value) {
function formatModeOperationValue(name, value, capability = "") {
if (name === "level" && ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
return { 1: "高档", 2: "低档" }[value] || formatConfigValue(name, value);
}
if (capability === "seat.set_driver_massage_native") {
const labels = {
action: { mode: "切换按摩模式", strength: "调节按摩力度", off: "关闭" },
mode: { wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松" },
strength: { 1: "轻柔", 2: "中等", 3: "较强" },
};
if (labels[name]?.[value]) return labels[name][value];
}
const valueLabels = {
on: "开启",
off: "关闭",
@@ -5193,7 +5215,7 @@ function modeOperationView(operation) {
const parameters = modeOperationParameters(operation)
.map(([name, value]) => {
const label = CONFIG_PARAMETER_LABELS[name] || name;
return `${label}:${formatModeOperationValue(name, value)}`;
return `${label}:${formatModeOperationValue(name, value, capability)}`;
})
.join(",");
return {
@@ -5232,7 +5254,50 @@ function modeTimelineStepView(step, action) {
};
}
function modeScheduleDurationLabel(durationMs) {
const minutes = Math.floor(durationMs / 60000);
const seconds = Number(((durationMs % 60000) / 1000).toFixed(3));
return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`;
}
function modeScheduleView(mode) {
const schedule = mode?.schedule;
if (
schedule?.schema !== "cognitive-agent-mode-schedule/v1"
|| !Array.isArray(schedule.phases) || !schedule.phases.length
|| !Number.isInteger(schedule.repeat_count)
|| schedule.repeat_count < 1 || schedule.repeat_count > 20
|| schedule.phases.some((phase) => !Number.isFinite(phase?.duration_ms)
|| phase.duration_ms <= 0 || !Array.isArray(phase.operations))
) return null;
let offsetMs = 0;
const phases = schedule.phases.map((phase, index) => {
const view = {
name: String(phase.name || `阶段 ${index + 1}`),
timing: `+${modeScheduleDurationLabel(offsetMs)} 开始 · 持续 ${modeScheduleDurationLabel(phase.duration_ms)}`,
actions: phase.operations.map(modeOperationView),
};
offsetMs += phase.duration_ms;
return view;
});
return {
phases,
summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${modeScheduleDurationLabel(offsetMs * schedule.repeat_count)}`,
stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚;时间为每轮的相对时间。",
};
}
function modeExecutionActionViews(mode) {
const schedule = modeScheduleView(mode);
if (schedule) {
// Runtime operations may contain a flattened projection. Prefer phases so
// repeated device targets remain distinct and are not listed twice.
return schedule.phases.flatMap((phase, index) => phase.actions.map((action) => ({
...action,
phase: `阶段 ${index + 1}:${phase.name}`,
timing: phase.timing,
})));
}
const operations = Array.isArray(mode?.operations)
? mode.operations.map(modeOperationView)
: [];
@@ -5253,6 +5318,57 @@ function modeExecutionActionViews(mode) {
return [...operations, ...timelineSteps];
}
function modeScheduleTrialMatches(draft, payload) {
if (!draft?.schedule) return true;
if (!modeScheduleView(draft) || !modeScheduleView(payload)) return false;
const signature = (schedule) => JSON.stringify([
schedule.schema, schedule.repeat_count,
schedule.phases.map((phase) => [
phase.id, phase.name, phase.duration_ms,
phase.operations.map((operation) => [
operation.domain, operation.capability,
operation.target_scope || operation.target?.instance || operation.target?.zone,
modeOperationParameters(operation).sort(([left], [right]) => left.localeCompare(right)),
]),
]),
]);
const execution = payload.schedule_execution;
const totalDurationMs = draft.schedule.phases.reduce((total, phase) => total + phase.duration_ms, 0)
* draft.schedule.repeat_count;
return signature(draft.schedule) === signature(payload.schedule)
&& execution?.status === "completed" && execution.completed === true
&& execution.virtual_elapsed_ms === totalDurationMs;
}
function modeScheduleExecutionView(result) {
const execution = result?.schedule_execution;
if (!execution || typeof execution !== "object") return null;
const status = String(execution.status || "");
if (status === "running" && result.status === "running") {
return {
state: "processing", badge: "已启动", title: "动态时间轴已启动",
detail: "后续动作将按阶段执行;可输入“停止动态模式”提前结束。已执行的设备状态不会自动回滚。",
};
}
if (["stopped", "cancelled"].includes(status)) {
return {
state: "succeeded", badge: "已停止", title: "动态时间轴已停止",
detail: "后续阶段不再执行;已经执行的设备状态不会自动回滚。",
};
}
if (status === "completed" && execution.completed === true) {
const simulated = execution.virtual_elapsed_ms != null;
return {
state: "succeeded", badge: simulated ? "模拟通过" : "已结束",
title: simulated ? "动态时间轴模拟试运行通过" : "动态时间轴已结束",
detail: simulated
? "已使用虚拟时钟校验全部阶段;未按真实时间等待,也未向车辆下发控制。"
: "已处理全部计划阶段,请以车辆反馈确认各设备的实际状态。",
};
}
return null;
}
function isUserModeProfile(mode) {
return mode?.origin === "user" || mode?.deletable === true;
}
@@ -5264,6 +5380,7 @@ function isN7SyncedModeProfile(mode) {
function isDynamicModeProfile(mode) {
const behavior = String(mode?.behavior || "").trim().toLowerCase();
if (["cyclic", "dynamic"].includes(behavior)) return true;
if (modeScheduleView(mode)) return true;
if (behavior === "static") return false;
if (mode?.timeline && typeof mode.timeline === "object") return true;
return LEGACY_DYNAMIC_MODE_IDS.has(String(mode?.id || ""));
@@ -5348,6 +5465,34 @@ function createModeGalleryCard(mode) {
button.append(iconBox, copy);
shell.append(button);
const schedule = modeScheduleView(mode);
if (schedule) {
meta.textContent = schedule.summary;
const details = document.createElement("details");
details.className = "mode-gallery-schedule-details";
const summary = document.createElement("summary");
summary.textContent = "查看分阶段执行动作";
const list = document.createElement("ol");
schedule.phases.forEach((phase) => {
const item = document.createElement("li");
const title = document.createElement("strong");
title.textContent = phase.name;
const timing = document.createElement("small");
timing.textContent = phase.timing;
item.append(title, timing);
phase.actions.forEach((action) => {
const row = document.createElement("p");
row.textContent = `${action.target} · ${action.action}:${action.parameters}`;
item.append(row);
});
list.append(item);
});
const hint = document.createElement("p");
hint.textContent = schedule.stopHint;
details.append(summary, list, hint);
shell.append(details);
}
const editButton = document.createElement("button");
editButton.className = "mode-gallery-edit";
editButton.type = "button";
@@ -6313,25 +6458,37 @@ function installModeChipScroller() {
pointerStartScroll = group.scrollLeft;
dragged = false;
pointerCaptured = false;
suppressClick = false;
});
group.addEventListener("pointermove", (event) => {
if (pointerId !== event.pointerId) return;
if (event.buttons !== 1) {
finishPointerDrag(event);
return;
}
const distance = event.clientX - pointerStartX;
if (!dragged && Math.abs(distance) < 5) return;
dragged = true;
if (!pointerCaptured && typeof group.setPointerCapture === "function") {
group.setPointerCapture(pointerId);
pointerCaptured = true;
if (!dragged) {
// Capturing on pointerdown retargets a normal button click to the rail.
// Capture only after movement establishes that this gesture is a drag.
dragged = true;
if (typeof group.setPointerCapture === "function") {
group.setPointerCapture(pointerId);
pointerCaptured = true;
}
group.classList.add("is-dragging");
}
group.classList.add("is-dragging");
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
updateScrollState();
event.preventDefault();
});
const finishPointerDrag = (event) => {
if (pointerId !== event.pointerId) return;
if (pointerCaptured) group.releasePointerCapture?.(pointerId);
const finishedPointerId = pointerId;
pointerId = null;
if (pointerCaptured && group.hasPointerCapture?.(finishedPointerId)) {
group.releasePointerCapture(finishedPointerId);
}
pointerCaptured = false;
group.classList.remove("is-dragging");
if (dragged) {
@@ -6342,6 +6499,7 @@ function installModeChipScroller() {
};
group.addEventListener("pointerup", finishPointerDrag);
group.addEventListener("pointercancel", finishPointerDrag);
group.addEventListener("lostpointercapture", finishPointerDrag);
group.addEventListener("click", (event) => {
if (!suppressClick) return;
event.preventDefault();
@@ -6392,10 +6550,13 @@ function ensureModeActionPopover() {
function renderModeActionPopover(mode) {
const popover = ensureModeActionPopover();
const schedule = modeScheduleView(mode);
const commandFlow = modeEditorReadableCommands(mode?.command_flow);
const usesLatestCommandFlow = commandFlow.length > 0;
const usesLatestCommandFlow = !schedule && commandFlow.length > 0;
const actions = usesLatestCommandFlow ? commandFlow : modeExecutionActionViews(mode);
$(`[data-mode-action-eyebrow]`, popover).textContent = usesLatestCommandFlow
$(`[data-mode-action-eyebrow]`, popover).textContent = schedule
? "动态时间轴"
: usesLatestCommandFlow
? "最新控制指令流"
: "执行动作预览";
$("[data-mode-action-title]", popover).textContent = mode?.name || mode?.id || "功能模式";
@@ -6404,6 +6565,12 @@ function renderModeActionPopover(mode) {
: `${Number(mode?.operation_count || 0)} 项`;
const list = $("[data-mode-action-list]", popover);
list.replaceChildren();
if (schedule) {
const summary = document.createElement("li");
summary.className = "mode-schedule-popover-summary";
summary.textContent = `${schedule.summary}。${schedule.stopHint}`;
list.append(summary);
}
if (!actions.length) {
const empty = document.createElement("li");
empty.className = "mode-chip-action-empty";
@@ -6422,6 +6589,12 @@ function renderModeActionPopover(mode) {
actionName.textContent = usesLatestCommandFlow
? String(action.category || "控制指令")
: action.action;
if (action.phase) {
const phase = document.createElement("small");
phase.className = "mode-schedule-popover-phase";
phase.textContent = `${action.phase} · ${action.timing}`;
copy.append(phase);
}
const metadata = document.createElement("small");
const appendMetadata = (label, value) => {
const text = String(value || "").trim();
@@ -6703,8 +6876,17 @@ function populateModeChips() {
runIcon.dataset.modeRun = "true";
runIcon.title = `执行${mode.name || mode.id}`;
const timeline = mode.timeline;
const schedule = modeScheduleView(mode);
let cycleBadge = $(".mode-cycle-badge", button);
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
if (schedule) {
if (!cycleBadge) {
cycleBadge = document.createElement("small");
cycleBadge.className = "mode-cycle-badge";
button.append(cycleBadge);
}
cycleBadge.textContent = schedule.summary;
button.dataset.modeBehavior = "dynamic";
} else if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
if (!cycleBadge) {
cycleBadge = document.createElement("small");
cycleBadge.className = "mode-cycle-badge";
@@ -6736,7 +6918,7 @@ function populateModeChips() {
: `${phase.name} 目标到位`;
}).join(" → ")
: "";
button.title = [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
button.title = [mode.description || mode.name, schedule?.summary || phaseSummary, "悬浮或聚焦查看全部执行动作"]
.filter(Boolean)
.join("\n");
});
@@ -6850,7 +7032,7 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
$$(".mode-chips button[data-mode-profile-id], #modeGallery button[data-mode-profile-id]").forEach((button) => {
const active = button.dataset.modeProfileId === modeProfileId && modeState !== "idle";
button.classList.toggle("running", active && modeState === "running");
button.classList.toggle("succeeded", active && modeState === "succeeded");
button.classList.toggle("succeeded", active && ["succeeded", "started", "ended", "stopped"].includes(modeState));
button.classList.toggle("failed", active && modeState === "failed");
button.dataset.modeState = active ? modeState : "idle";
button.setAttribute("aria-pressed", active ? "true" : "false");
@@ -6859,6 +7041,12 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
if (stateLabel) {
stateLabel.textContent = active && modeState === "running"
? "执行中"
: active && modeState === "started"
? "已启动"
: active && modeState === "ended"
? "已结束"
: active && modeState === "stopped"
? "已停止"
: active && modeState === "succeeded"
? "已完成"
: active && modeState === "failed"
@@ -6866,7 +7054,7 @@ function setModeExecutionState(modeProfileId, modeState = "idle") {
: "点击执行";
}
});
if (modeProfileId && ["succeeded", "failed"].includes(modeState)) {
if (modeProfileId && ["succeeded", "failed", "started", "ended", "stopped"].includes(modeState)) {
state.modeRunTimer = window.setTimeout(() => setModeExecutionState(null), 2800);
}
}
@@ -6937,11 +7125,14 @@ async function executeSavedModeProfile(mode, { command = "" } = {}) {
);
if (!payload) throw new Error("模式卡片未返回执行结果");
renderResult(payload);
const scheduleRunning = modeScheduleExecutionView(payload)?.state === "processing";
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
$("#resultTitle").textContent = completed
$("#resultTitle").textContent = scheduleRunning
? `${mode.name}时间轴已启动`
: completed
? `${mode.name}执行完成`
: `${mode.name}执行未完成`;
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
setModeExecutionState(mode.id, scheduleRunning ? "started" : completed ? "succeeded" : "failed");
if (assistantModeHandle && assistant?.completeModeExecution) {
assistant.completeModeExecution(assistantModeHandle, mode, payload, {
command: modeCommand,
@@ -6950,7 +7141,9 @@ async function executeSavedModeProfile(mode, { command = "" } = {}) {
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
}
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
showToast(completed
showToast(scheduleRunning
? "时间轴已启动,可输入“停止动态模式”提前结束"
: completed
? `${mode.name}已直接执行保存的控制指令流`
: `${mode.name}未完成,请查看执行结果`);
} catch (error) {
@@ -6985,10 +7178,17 @@ function applyModeCommand(command, modeProfileId = null) {
showToast("AI助手正在处理上一条指令,请稍候");
return false;
}
const speech = window.CognitiveAgentSpeech;
if (["capturing", "finalizing"].includes(speech?.phase)
|| (speech?.phase === "arming" && speech?.inputMode === "manual")) {
showToast("请等待语音整理完成后再发送");
return false;
}
if ($("#runButton")?.disabled) {
showToast("上一条指令仍在执行,请稍候");
return false;
}
hideModeActionPopover({ immediate: true });
const mode = modeProfiles().find((item) => item.id === modeProfileId);
setModeRailSelection(mode);
closeModeGallery({ restoreFocus: false });
@@ -8052,11 +8252,15 @@ const CONFIG_DOMAIN_META = {
window: { label: "车窗", icon: "panels-top-left", className: "generic" },
outlet: { label: "电动风口", icon: "wind", className: "climate" },
interior_light: { label: "车内灯", icon: "lightbulb", className: "ambient" },
android: { label: "车机媒体", icon: "clapperboard", className: "generic" },
};
const CONFIG_CAPABILITY_LABELS = {
"seat.set_ventilation": "设置座椅通风",
"seat.set_heating": "设置座椅加热",
"seat.set_driver_ventilation_native": "设置主驾座椅通风",
"seat.set_driver_heating_native": "设置主驾座椅加热",
"seat.set_driver_massage_native": "设置主驾座椅按摩",
"seat.set_longitudinal": "调节座椅前后位置",
"seat.set_longitudinal_position": "调节座椅前后位置",
"seat.set_cushion_tilt": "调节座椅高度",
@@ -8087,14 +8291,21 @@ const CONFIG_CAPABILITY_LABELS = {
"ambient_light.set_illumination": "设置氛围灯颜色与亮度",
"ambient_light.set_color": "设置氛围灯颜色",
"ambient_light.set_brightness": "设置氛围灯亮度",
"ambient_light.set_brightness_level": "设置氛围灯亮度",
"ambient_light.set_color_index": "设置氛围灯色号",
"ambient_light.set_mode": "设置氛围灯模式",
"roof.set_transmittance": "设置天幕透光率",
"roof.set_shade_position": "设置遮阳帘位置",
"roof.set_shade_motion": "控制遮阳帘动作",
"roof.set_automatic_dimming": "设置自动调光",
"android.launch_app": "打开视频媒体",
};
const CONFIG_PARAMETER_LABELS = {
action: "动作",
strength: "力度",
color_index: "色号",
brightness_level: "亮度档位",
power: "电源",
state: "状态",
level: "档位",
@@ -8111,6 +8322,8 @@ const CONFIG_PARAMETER_LABELS = {
outlet_mode: "出风模式",
mode: "工作模式",
mode_id: "固件模式编号",
package_name: "应用包名",
activity_name: "启动页面",
};
function makeConfigElement(tagName, className, textValue) {
@@ -8381,6 +8594,24 @@ function renderExecution(result) {
const summary = $("#executionSummary");
const list = $("#resultList");
list.replaceChildren();
const scheduleExecution = modeScheduleExecutionView(result);
if (scheduleExecution) {
summary.className = `execution-summary ${scheduleExecution.state === "processing" ? "partial" : scheduleExecution.state}`;
const icon = document.createElement("i");
icon.dataset.lucide = scheduleExecution.state === "processing" ? "timer" : "circle-check";
const copy = document.createElement("div");
const title = document.createElement("strong");
title.textContent = scheduleExecution.title;
const detail = document.createElement("span");
detail.textContent = scheduleExecution.detail;
copy.append(title, detail);
summary.replaceChildren(icon, copy);
$("#resultNavMeta").textContent = scheduleExecution.badge;
$("#resultPanelState").textContent = scheduleExecution.badge;
$("#resultPanelState").className = `workspace-panel-state ${scheduleExecution.state}`;
refreshIcons();
return;
}
const report = result.execution_report;
if (!report) {
summary.className = "execution-summary failed";
@@ -8656,6 +8887,7 @@ window.addEventListener("cognitive-agent:extension-vehicle-result", (event) => {
function renderResult(result) {
state.result = result;
const scheduleExecution = modeScheduleExecutionView(result);
const reportMode = result.execution_report?.spec?.mode;
const feedbackSource = result.execution_report?.metadata?.labels?.seat_feedback;
const submittedUnverified = executionVerificationStatus(result) === "submitted_unverified";
@@ -8690,7 +8922,9 @@ function renderResult(result) {
};
const peripheralRoute = String(result.orchestration_route || "")
.startsWith("nfc_peripheral");
$("#resultTitle").textContent = peripheralRoute && executionSucceeded
$("#resultTitle").textContent = scheduleExecution
? `${result.mode_profile?.name ? `${result.mode_profile.name} · ` : ""}${scheduleExecution.title}`
: peripheralRoute && executionSucceeded
? `${result.peripheral?.name || "第三方外设"} · ${result.peripheral?.action || "动作已执行"}`
: partiallySucceeded
? `${domains.length} 域场景部分完成`
@@ -8714,7 +8948,7 @@ function renderResult(result) {
$("#requestMeta").textContent += ` / reasoning:${result.reasoning_effort_used}`;
}
setRunStatus(
partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed",
scheduleExecution?.state || (partiallySucceeded || submittedUnverified ? "partial" : executionSucceeded ? "succeeded" : "failed"),
submittedUnverified ? "SUBMITTED_UNVERIFIED" : result.status.toUpperCase(),
);
renderDecision(result);
@@ -9193,6 +9427,9 @@ window.CognitiveAgentConsole = {
getExecutionMode() {
return state.executionMode;
},
getHardwareClientId() {
return validatedHardwareClientId();
},
getConversationId() {
return state.conversationId;
},
@@ -9293,12 +9530,17 @@ window.CognitiveAgentConsole = {
&& expectedSignatures.every((signature, index) => signature === actualSignatures[index]));
if (payload.status !== "succeeded"
|| (expectedOperationCount > 0 && actualOperationCount < expectedOperationCount)
|| !sameActions) {
|| !sameActions || !modeScheduleTrialMatches(draft, payload)) {
throw new Error("试运行未完整覆盖草稿动作,请重新生成模式预览。");
}
return payload;
},
revealModeProfile,
updateScheduleExecutionStatus(modeProfileId, status) {
if (!modeProfileId || (state.pendingModeProfileId && state.pendingModeProfileId !== modeProfileId)) return;
const terminal = { completed: "ended", failed: "failed", cancelled: "stopped", stopped: "stopped", idle: "idle" }[status];
if (terminal) setModeExecutionState(modeProfileId, terminal);
},
notify(message) {
showToast(String(message));
},
@@ -9321,9 +9563,11 @@ window.addEventListener("cognitive-agent:request-complete", (event) => {
if (!modeProfileId) return;
state.pendingModeProfileId = null;
const result = event.detail?.result;
const scheduleRunning = !event.detail?.error
&& modeScheduleExecutionView(result)?.state === "processing";
const succeeded = !event.detail?.error
&& ["succeeded", "partially_succeeded"].includes(String(result?.status || ""));
setModeExecutionState(modeProfileId, succeeded ? "succeeded" : "failed");
setModeExecutionState(modeProfileId, scheduleRunning ? "started" : succeeded ? "succeeded" : "failed");
});
document.addEventListener("visibilitychange", () => {
+122 -1
View File
@@ -551,6 +551,120 @@
padding: 9px 10px 8px;
}
.assistant-mode-schedule-summary,
.assistant-mode-schedule-hint,
.assistant-mode-draft-preview-notice {
margin: 0;
padding: 9px 12px;
color: var(--assistant-muted, #64748b);
font-size: 10px;
line-height: 1.5;
overflow-wrap: anywhere;
}
.assistant-mode-schedule-summary {
color: var(--status-info-foreground, #3764d9);
background: var(--status-info-soft, #eef3ff);
font-weight: 750;
}
.assistant-mode-schedule-phase + .assistant-mode-schedule-phase,
.assistant-mode-schedule-hint,
.assistant-mode-draft-preview-notice {
border-top: 1px solid var(--assistant-border, #e5eaf1);
}
.assistant-mode-draft-preview-notice {
margin: 0;
color: var(--status-success-foreground, #087854);
background: var(--status-success-soft, #ebf9f3);
font-weight: 700;
}
.assistant-mode-schedule-phase > header {
display: grid;
gap: 3px;
padding: 10px 12px 5px;
color: var(--assistant-heading, #334155);
font-size: 11px;
overflow-wrap: anywhere;
}
.assistant-mode-schedule-phase > header small {
color: var(--assistant-muted, #64748b);
font-size: 9px;
font-weight: 500;
line-height: 1.5;
}
.assistant-mode-draft-scheduled .assistant-mode-draft-row {
grid-template-columns: 18px minmax(0, 1fr) minmax(0, 1fr);
gap: 5px;
}
.assistant-mode-draft-scheduled .assistant-mode-draft-row > strong {
white-space: normal;
overflow-wrap: anywhere;
text-align: right;
}
.mode-gallery-card-shell:has(> .mode-gallery-schedule-details) > .mode-gallery-card {
height: auto;
}
.mode-gallery-schedule-details {
padding: 9px 12px;
color: var(--foreground, #334155);
background: var(--card, #fff);
border: 1px solid var(--border, #e5eaf1);
border-radius: 10px;
font-size: 11px;
line-height: 1.6;
overflow-wrap: anywhere;
}
.mode-gallery-schedule-details > summary {
cursor: pointer;
color: var(--primary, #3764d9);
font-weight: 700;
}
.mode-gallery-schedule-details ol {
margin: 10px 0;
padding-left: 19px;
}
.mode-gallery-schedule-details li + li {
margin-top: 10px;
}
.mode-gallery-schedule-details li > strong,
.mode-gallery-schedule-details li > small {
display: block;
}
.mode-gallery-schedule-details small,
.mode-gallery-schedule-details > p {
color: var(--muted-foreground, #64748b);
}
.mode-gallery-schedule-details p {
margin: 4px 0;
}
.mode-chip-action-popover li.mode-schedule-popover-summary {
display: block;
color: var(--status-info-foreground, #3764d9);
background: var(--status-info-soft, #eef3ff);
font-size: 11px;
line-height: 1.6;
}
.mode-schedule-popover-phase {
color: var(--primary, #3764d9);
line-height: 1.5;
}
.assistant-mode-draft-actions[hidden] {
display: none;
}
@@ -1478,7 +1592,14 @@
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-operation-shell {
top: 60px;
/* Share the content row on compact screens, never cover the mode rail. */
position: relative;
grid-column: 1;
grid-row: 3;
width: 100%;
top: auto;
right: auto;
bottom: auto;
}
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-module-switcher {
+489 -42
View File
@@ -11,6 +11,7 @@
let pendingMessage = null;
let latestControlResult = null;
let activeModeDraftContext = null;
let activeSchedulePoll = null;
let initialized = false;
const $ = (selector, root = document) => root.querySelector(selector);
@@ -176,11 +177,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}[,,::]?(.*)$`),
@@ -194,14 +197,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))
);
});
@@ -530,6 +548,7 @@
"door.set_rear_lock": "后排门锁",
"door.set_vehicle_lock": "整车门锁",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
function assistantControlCategory(operation = {}, fallback = "控制指令") {
@@ -551,6 +570,11 @@
"seat.set_backrest_recline": "靠背",
"seat.set_legrest_height": "腿托",
"seat.set_legrest_extension": "腿托",
"seat.set_ventilation": "通风",
"seat.set_heating": "加热",
"seat.set_driver_ventilation_native": "通风",
"seat.set_driver_heating_native": "加热",
"seat.set_driver_massage_native": "按摩",
}[capability];
if (component) return `${seat}${component}`;
if (capability === "seat.set_driver_axis_native") {
@@ -567,7 +591,19 @@
return seat;
}
if (capability.startsWith("outlet.")) return "空调风口";
return String(fallback || capability || "控制指令");
const domain = String(operation.domain || capability.split(".", 1)[0] || "");
return {
climate: "空调",
window: "车窗",
roof: "遮阳帘",
ambient_light: "氛围灯",
interior_light: "室内灯",
self_extension: "红外外设",
door: "车门",
outlet: "空调风口",
}[domain] || (/^[\u4e00-\u9fff]/.test(String(fallback || ""))
? String(fallback)
: "控制指令");
}
function fallbackAssistantControlCommands(result = {}) {
@@ -602,6 +638,8 @@
}
function assistantDispatchedCommands(result = {}) {
const previewOnly = String(result.execution_mode || "dry_run") !== "hardware"
|| result.orchestration_route === "mode_profile_create_preview";
const projections = Array.isArray(result.validated_control_commands)
? result.validated_control_commands
: [];
@@ -634,22 +672,30 @@
} catch (_error) {
commands = [];
}
if (!commands.length) return fallbackAssistantControlCommands(result);
if (!commands.length) commands = fallbackAssistantControlCommands(result);
return commands.map((item, index) => {
const operation = projectionByRef.get(String(item.operationRef || ""))
|| operationByRef.get(String(item.operationRef || ""))
|| projections[index]?.operation
|| {};
const pendingDelivery = result.status === "processing"
const pendingDelivery = result.status === "processing" && !previewOnly
? {
deliveryState: "submitting",
deliveryLabel: "正在下发",
deliveryDetail: "",
}
: {};
const previewDelivery = previewOnly
? {
deliveryState: "not_submitted",
deliveryLabel: "仅预览 · 未下发",
deliveryDetail: "创建模式期间不会发送控制指令",
}
: {};
return {
...item,
...pendingDelivery,
...previewDelivery,
category: assistantControlCategory(operation, item.category),
operation,
};
@@ -673,6 +719,11 @@
manual: "手动",
internal: "内循环",
external: "外循环",
romantic: "浪漫",
cinema: "影院",
rest: "休息",
camping: "露营",
meditation: "冥想",
}[normalized] || String(value ?? "").trim();
}
@@ -695,6 +746,22 @@
const brightness = numericValue(parameters.brightness_level);
if (brightness !== null) return `${brightness}级`;
const level = numericValue(parameters.level ?? parameters.fan_level);
const capability = String(item.operation?.capability || "");
if (capability === "android.launch_app") {
const packageName = String(parameters.package_name || "").trim();
const activityName = String(parameters.activity_name || "").trim();
return [
packageName ? `应用 ${packageName}` : "视频应用",
activityName ? `页面 ${activityName}` : "",
].filter(Boolean).join(" · ");
}
if (
level !== null
&& ["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"]
.includes(capability)
) {
return ({ 1: "3档", 2: "1档" })[level] || "档位待确认";
}
if (level !== null) return `${level}档`;
const state = parameters.motion
?? parameters.state
@@ -719,6 +786,7 @@
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
if (capability === "infrared.emit") return "radio-tower";
if (capability === "android.launch_app") return "clapperboard";
return "binary";
}
@@ -746,6 +814,9 @@
message.dispatchedControlResult = mergedResult;
const commands = assistantDispatchedCommands(mergedResult);
if (!commands.length) return;
const previewOnly = String(mergedResult.execution_mode || "dry_run") !== "hardware"
|| mergedResult.orchestration_route === "mode_profile_create_preview";
const headingText = previewOnly ? "控制指令预览(未下发)" : "下发控制指令";
const signature = JSON.stringify(commands.map((item) => [
item.operationRef,
@@ -776,7 +847,7 @@
deliveryLabel: String(item.deliveryLabel || "未下发"),
deliveryDetail: String(item.deliveryDetail || ""),
}));
if (vueRenderer(host, viewCommands)) {
if (vueRenderer(host, viewCommands, { heading: headingText })) {
host.dataset.commandSignature = signature;
message.article?.classList.add("assistant-message-with-dispatched-controls");
focusLatestDispatchedControls(host);
@@ -803,7 +874,7 @@
headingIcon.className = "assistant-dispatched-controls-heading-icon";
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
const heading = document.createElement("b");
heading.textContent = "下发控制指令";
heading.textContent = headingText;
const count = document.createElement("strong");
count.className = "assistant-dispatched-controls-count";
const chevron = document.createElement("i");
@@ -817,6 +888,8 @@
}
card.dataset.commandSignature = signature;
const heading = $("summary b", card);
if (heading) heading.textContent = headingText;
const count = $(".assistant-dispatched-controls-count", card);
const list = $(".assistant-dispatched-controls-list", card);
if (count) count.textContent = String(commands.length);
@@ -894,7 +967,7 @@
const consumed = new Set();
const rows = [];
const targetZone = (action) => String(
action?.target?.instance || action?.target?.zone || action?.target || "",
action?.target?.instance || action?.target?.zone || action?.target_scope || action?.target || "",
);
const parameters = (action) => (
action?.parameters && typeof action.parameters === "object" ? action.parameters : {}
@@ -907,6 +980,31 @@
indexes.forEach((index) => consumed.add(index));
rows.push({ label, value, icon, order });
};
const domainLabels = {
climate: "空调",
window: "车窗",
roof: "遮阳帘",
seat: "座椅",
ambient_light: "氛围灯",
interior_light: "室内灯",
self_extension: "红外外设",
door: "车门",
outlet: "空调风口",
};
const capabilityLabels = {
"climate.set_power": "空调电源",
"climate.set_temperature": "空调温度",
"climate.set_fan_level": "空调风量",
"climate.set_cycle_mode": "空调循环",
"climate.set_control_mode": "空调控制模式",
"climate.set_outlet_mode": "空调出风模式",
"window.set_position": "车窗位置",
"roof.set_shade_position": "遮阳帘位置",
"ambient_light.set_power": "氛围灯",
"ambient_light.set_mode": "氛围灯模式",
"infrared.emit": "红外外设",
"android.launch_app": "打开视频媒体",
};
const zeroGravityIndexes = actions
.map((action, index) => ({ action, index }))
@@ -960,18 +1058,89 @@
if (position === 100) return `完全打开(${target})`;
return target;
};
const localizedParameterValue = (value) => ({
on: "开启", off: "关闭", open: "打开", close: "关闭", stop: "停止",
auto: "自动", manual: "手动", internal: "内循环", external: "外循环",
romantic: "浪漫", cinema: "影院", rest: "休息", camping: "露营",
}[String(value)] || String(value));
const genericParameterText = (params) => Object.entries(params)
.filter(([key]) => key !== "slot" && key !== "capability_ref")
.map(([key, rawValue]) => {
const name = {
state: "状态", motion: "动作", action: "动作", mode: "模式",
level: "档位", temperature_c: "温度", position_pct: "目标开度",
brightness_pct: "亮度", brightness_level: "亮度档位",
color_hex: "颜色", color_index: "色号",
}[key] || "参数";
const suffix = key === "temperature_c" ? "℃"
: ["position_pct", "brightness_pct"].includes(key) ? "%"
: key === "level" || key === "brightness_level" ? "档" : "";
return `${name}:${localizedParameterValue(rawValue)}${suffix}`;
})
.join(" · ");
const passengerComfort = actions
.map((action, index) => ({ action, index }))
.filter(({ action, index }) => !consumed.has(index)
&& ["passenger", "front_right"].includes(targetZone(action))
&& [
"seat.set_ventilation",
"seat.set_backrest_recline",
"seat.set_legrest_height",
].includes(String(action.capability || "")));
const comfortByCapability = new Map(passengerComfort.map(({ action, index }) => [
String(action.capability || ""), { action, index },
]));
if ([
"seat.set_ventilation",
"seat.set_backrest_recline",
"seat.set_legrest_height",
].every((capability) => comfortByCapability.has(capability))) {
const ventilation = parameters(comfortByCapability.get("seat.set_ventilation").action);
const backrest = parameters(comfortByCapability.get("seat.set_backrest_recline").action);
const legrest = parameters(comfortByCapability.get("seat.set_legrest_height").action);
const comfortMentions = ["座椅", "副驾", "舒适"]
.map((term) => instruction.indexOf(term))
.filter((index) => index >= 0);
const comfortOrder = comfortMentions.length
? Math.min(...comfortMentions)
: 10_000 + Math.max(...passengerComfort.map(({ index }) => index));
addRow(
"副驾座椅舒适模式",
[
`通风${ventilation.state === "off" ? "关闭" : `开启 ${ventilation.level ?? 1}档`}`,
`靠背 ${backrest.position_pct ?? "--"}%`,
`腿托 ${legrest.position_pct ?? "--"}%`,
].join(" · "),
"armchair",
comfortOrder,
passengerComfort.map(({ index }) => index),
);
}
actions.forEach((action, index) => {
if (consumed.has(index)) return;
const capability = String(action.capability || "");
const domain = String(action.domain || "");
const zone = targetZone(action);
const zoneLabel = {
driver: "主驾", passenger: "副驾", front_left: "主驾", front_right: "副驾",
front_row: "前排", rear_row: "后排", rear_left: "左后", rear_right: "右后",
}[zone] || "";
const params = parameters(action);
let label = capability || domain || "座舱设备";
let label = capabilityLabels[capability] || domainLabels[domain] || "座舱设备";
let value = "已配置";
let icon = "sliders-horizontal";
let order = 10_000 + index;
if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
if (capability === "infrared.emit" || domain === "self_extension") {
label = String(action?.target?.name || action?.target?.device || "红外外设").trim()
|| "红外外设";
value = params.action != null
? `动作:${localizedParameterValue(params.action)}`
: "动作已配置";
icon = "radio-tower";
order = instructionOrder(label, "外设", "灯");
} else if (capability === "roof.set_shade_motion" || capability === "roof.set_shade_position") {
label = "遮阳帘";
value = params.position_pct != null
? shadePositionLabel(params.position_pct)
@@ -989,7 +1158,13 @@
rear_right: "右后车窗",
};
label = windowLabels[zone] || "车窗";
value = params.position_pct != null ? `${params.position_pct}%` : motionLabel(params.motion);
value = params.position_pct != null
? Number(params.position_pct) === 0
? "完全关闭(目标开度 0%)"
: Number(params.position_pct) === 100
? "完全打开(目标开度 100%)"
: `目标开度 ${params.position_pct}%`
: motionLabel(params.motion);
icon = "panels-top-left";
order = instructionOrder("车窗");
} else if (domain === "seat") {
@@ -999,43 +1174,156 @@
"seat.set_cushion_tilt": "坐垫角度",
"seat.set_backrest_recline": "靠背角度",
"seat.set_legrest_height": "腿托高度",
"seat.set_ventilation": "座椅通风",
"seat.set_heating": "座椅加热",
"seat.set_driver_ventilation_native": "座椅通风",
"seat.set_driver_heating_native": "座椅加热",
"seat.set_driver_massage_native": "座椅按摩",
};
label = `${seatLabel}${seatCapabilityLabels[capability] || "座椅"}`;
value = params.position_pct != null
? `${params.position_pct}%${params.angle_deg != null ? `(${params.angle_deg}°)` : ""}`
: motionLabel(params.state || params.mode_id);
if (["seat.set_driver_ventilation_native", "seat.set_driver_heating_native"].includes(capability)) {
// CH1 encodes high as 1 and low as 2 internally. Present the
// occupant-facing three-speed scale instead of leaking CAN enums.
const level = { 1: "3档(高)", 2: "1档(低)" }[params.level ?? 2]
|| "档位待确认";
value = params.state === "off" ? "关闭"
: params.state === "on" ? `开启 · ${level}` : "开关状态待确认";
} else if (["seat.set_ventilation", "seat.set_heating"].includes(capability)) {
value = params.state === "off" ? "关闭"
: params.state === "on"
? `开启${params.level != null ? ` · ${params.level}档` : ""}`
: "开关状态待确认";
} else if (capability === "seat.set_driver_massage_native") {
const massageModes = {
wave: "波浪", pulse: "脉冲", stretch: "舒展", slack: "舒缓", relax: "放松",
};
const massageStrengths = { 1: "轻柔", 2: "中等", 3: "较强" };
value = params.action === "off" ? "关闭"
: params.action === "mode"
? (massageModes[params.mode] ? `${massageModes[params.mode]}模式` : "按摩模式待确认")
: params.action === "strength"
? (massageStrengths[params.strength] ? `${massageStrengths[params.strength]}力度` : "按摩力度待确认")
: "按摩动作待确认";
}
icon = "armchair";
order = instructionOrder("座椅", "副驾", "主驾");
} else if (capability === "climate.set_temperature") {
label = "空调温度";
label = `${zoneLabel}空调温度`;
value = `${params.temperature_c ?? "--"}℃`;
icon = "thermometer";
order = instructionOrder("空调", "温度");
} else if (domain === "climate") {
const cycleLabels = { internal: "内循环", external: "外循环", auto: "自动循环" };
label = capability === "climate.set_fan_level" ? "空调风量"
: capability === "climate.set_cycle_mode" ? "空调循环" : "空调";
const climateModeLabels = {
"climate.set_cycle_mode": { internal: "内循环", external: "外循环" },
"climate.set_control_mode": { manual: "手动控制", auto: "自动控制" },
"climate.set_outlet_mode": {
face: "吹面", face_feet: "吹面 / 吹脚", feet: "吹脚",
feet_defrost: "吹脚 / 除霜", defrost: "除霜",
face_feet_defrost: "吹面 / 吹脚 / 除霜", face_defrost: "吹面 / 除霜",
},
};
label = capability === "climate.set_power" ? "空调电源"
: capability === "climate.set_fan_level" ? "空调风量"
: capability === "climate.set_cycle_mode" ? "空调循环"
: capability === "climate.set_control_mode" ? "空调控制模式"
: capability === "climate.set_outlet_mode" ? "空调出风模式" : "空调";
label = `${zoneLabel}${label}`;
value = params.level != null ? `${params.level}档`
: params.mode != null ? (cycleLabels[String(params.mode)] || String(params.mode))
: params.mode != null ? (climateModeLabels[capability]?.[String(params.mode)] || String(params.mode))
: motionLabel(params.state);
icon = "fan";
order = instructionOrder("空调", "风量");
} else if (capability === "interior_light.set_reading_power") {
label = "阅读灯";
label = `${zoneLabel}阅读灯`;
value = motionLabel(params.state);
icon = "book-open";
order = instructionOrder("阅读灯", "灯光");
} else if (["interior_light", "ambient_light"].includes(domain)) {
label = domain === "ambient_light" ? "氛围灯" : "室内灯";
value = motionLabel(params.state);
if (capability === "ambient_light.set_brightness_level") {
label = "氛围灯亮度";
value = params.brightness_level != null ? `${params.brightness_level}档` : "亮度档位待确认";
} else if (capability === "ambient_light.set_color_index") {
label = "氛围灯颜色";
value = params.color_index != null ? `色号 ${params.color_index}` : "颜色待确认";
} else if (capability === "ambient_light.set_illumination") {
value = [
params.color_hex != null ? `颜色 ${params.color_hex}` : "颜色待确认",
params.brightness_pct != null ? `亮度 ${params.brightness_pct}%` : "亮度待确认",
].join(" · ");
}
icon = "lightbulb";
order = instructionOrder(label, "灯光");
}
if (value === "已配置") {
value = genericParameterText(params) || "参数已配置";
}
addRow(label, value, icon, order, [index]);
});
return rows.sort((left, right) => left.order - right.order);
}
function modeDraftScheduleView(draft) {
const schedule = draft?.schedule;
if (
schedule?.schema !== "cognitive-agent-mode-schedule/v1"
|| !Array.isArray(schedule.phases) || !schedule.phases.length
|| !Number.isInteger(schedule.repeat_count)
|| schedule.repeat_count < 1 || schedule.repeat_count > 20
|| schedule.phases.some((phase) => !Number.isFinite(phase?.duration_ms)
|| phase.duration_ms <= 0 || !Array.isArray(phase.operations))
) return null;
const durationLabel = (durationMs) => {
const minutes = Math.floor(durationMs / 60000);
const seconds = Number(((durationMs % 60000) / 1000).toFixed(3));
return minutes ? `${minutes}分${seconds ? `${seconds}秒` : ""}` : `${seconds}秒`;
};
let offsetMs = 0;
const phases = schedule.phases.map((phase, index) => {
// Each phase is rendered separately, without instruction-based grouping:
// the same seat or temperature can have a different target later on.
const rows = modeDraftRows({ instruction: "" }, phase.operations.map((operation) => ({
...operation,
target: operation.target || (operation.domain === "seat"
? { instance: operation.target_scope }
: { zone: operation.target_scope }),
})));
const view = {
id: String(phase.id || `phase-${index + 1}`),
name: String(phase.name || `阶段 ${index + 1}`),
timing: `+${durationLabel(offsetMs)} 开始 · 持续 ${durationLabel(phase.duration_ms)}`,
rows,
};
offsetMs += phase.duration_ms;
return view;
});
return {
phases,
operationCount: schedule.phases.reduce((count, phase) => count + phase.operations.length, 0),
summary: `${phases.length} 个阶段 · 重复 ${schedule.repeat_count} 次 · 总时长 ${durationLabel(offsetMs * schedule.repeat_count)}`,
stopHint: "执行后可输入“停止动态模式”提前结束,已执行状态不自动回滚。各阶段按顺序执行,时间为每轮的相对时间。",
};
}
function appendModeDraftRows(container, rows) {
rows.forEach((item) => {
const row = document.createElement("div");
row.className = "assistant-mode-draft-row";
const icon = document.createElement("i");
icon.dataset.lucide = item.icon;
const label = document.createElement("span");
label.textContent = item.label;
const value = document.createElement("strong");
value.textContent = item.value;
row.append(icon, label, value);
container.append(row);
});
}
async function saveModeProfileDraft(message, card, draft, buttons, status) {
buttons.forEach((button) => { button.disabled = true; });
card.classList.add("is-busy");
@@ -1089,12 +1377,12 @@
card.classList.add("is-busy");
status.hidden = false;
status.className = "assistant-mode-draft-status working";
status.textContent = "正在进行模拟试运行…";
status.textContent = "正在重新进行 dry-run 校验(不会下发)…";
try {
const result = await window.CognitiveAgentConsole?.tryModeDraft?.({ ...draft, actions });
if (!result || result.status !== "succeeded") throw new Error("试运行未通过,请检查动作设置。");
status.className = "assistant-mode-draft-status success";
status.textContent = "试运行通过 · 尚未保存";
status.textContent = "校验通过 · 未下发 · 尚未保存";
} catch (error) {
status.className = "assistant-mode-draft-status failed";
status.textContent = error.message || "试运行失败,请重试。";
@@ -1145,29 +1433,49 @@
const eyebrow = document.createElement("small");
eyebrow.textContent = draft.existing_mode_id
? "已载入现有功能模式"
: "已为你生成功能模式";
: "已为你生成功能模式(未下发)";
const title = document.createElement("strong");
title.textContent = `「${draft.name}」`;
headingCopy.append(eyebrow, title);
const badge = document.createElement("span");
badge.className = "assistant-mode-draft-badge";
badge.textContent = `${Number(draft.operation_count) || 0} 项动作`;
const scheduleView = modeDraftScheduleView(draft);
badge.textContent = `${scheduleView?.operationCount ?? (Number(draft.operation_count) || 0)} 项动作`;
heading.append(headingCopy, badge);
const list = document.createElement("div");
list.className = "assistant-mode-draft-list";
modeDraftRows(draft, actions).forEach((item) => {
const row = document.createElement("div");
row.className = "assistant-mode-draft-row";
const icon = document.createElement("i");
icon.dataset.lucide = item.icon;
const label = document.createElement("span");
label.textContent = item.label;
const value = document.createElement("strong");
value.textContent = item.value;
row.append(icon, label, value);
list.append(row);
});
if (scheduleView) {
card.classList.add("assistant-mode-draft-scheduled");
const summary = document.createElement("p");
summary.className = "assistant-mode-schedule-summary";
summary.textContent = scheduleView.summary;
list.append(summary);
scheduleView.phases.forEach((phase, index) => {
const section = document.createElement("section");
section.className = "assistant-mode-schedule-phase";
section.dataset.modeSchedulePhase = phase.id;
const phaseHeading = document.createElement("header");
const name = document.createElement("strong");
name.textContent = `${index + 1}. ${phase.name}`;
const timing = document.createElement("small");
timing.textContent = phase.timing;
phaseHeading.append(name, timing);
section.append(phaseHeading);
appendModeDraftRows(section, phase.rows);
list.append(section);
});
const hint = document.createElement("p");
hint.className = "assistant-mode-schedule-hint";
hint.textContent = scheduleView.stopHint;
list.append(hint);
} else {
appendModeDraftRows(list, modeDraftRows(draft, actions));
}
const previewNotice = document.createElement("p");
previewNotice.className = "assistant-mode-draft-preview-notice";
previewNotice.textContent = "创建阶段仅生成并校验控制方案,不会向 CAN 或红外外设发送指令。";
list.append(previewNotice);
const controls = document.createElement("div");
controls.className = "assistant-mode-draft-actions";
@@ -1180,7 +1488,7 @@
const trial = document.createElement("button");
trial.type = "button";
trial.className = "assistant-mode-draft-trial";
trial.textContent = "先试运行";
trial.textContent = "重新校验(不下发)";
controls.append(save, trial);
const status = document.createElement("p");
status.className = "assistant-mode-draft-status";
@@ -1233,12 +1541,36 @@
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
}
function isModeCreationPreviewRequest(message) {
const compact = String(message || "").trim().replace(/\s+/g, "");
if (!compact || /取消(?:创建|构建|生成)|(?:不要|别|禁止)(?:创建|构建|生成)/.test(compact)) {
return false;
}
return /(?:构建|创建|新建|新增|生成|添加)(?:一下)?(?:一个|个|一套|套)?[^,。!?!?]{1,64}?模式/.test(compact);
}
function validatedHardwareClientId() {
const sharedClientId = window.CognitiveAgentConsole?.getHardwareClientId?.();
let storedClientId = null;
try {
storedClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
} catch (_error) {
// The shared console identity remains authoritative when storage is unavailable.
}
return [sharedClientId, storedClientId]
.map((value) => String(value || "").trim())
.find((value) => /^hwc-[a-z0-9-]{8,80}$/.test(value)) || null;
}
function requestBody(message) {
const executionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const configuredExecutionMode = window.CognitiveAgentConsole?.getExecutionMode?.() || "dry_run";
const controlTransport = localStorage.getItem("cognitive-agent.control-transport") || "wired_can";
const hardwareClientId = localStorage.getItem("cognitive-agent.hardware-client-id");
const hardwareClientId = validatedHardwareClientId();
const vehicleId = $("#vehicleSelect")?.value || null;
const modeProfileDraftContext = requestModeDraftContext(message);
const modeConfigurationPreview = Boolean(modeProfileDraftContext)
|| isModeCreationPreviewRequest(message);
const executionMode = modeConfigurationPreview ? "dry_run" : configuredExecutionMode;
return {
message,
conversation_id: conversationId(),
@@ -1267,6 +1599,90 @@
}
}
function stopScheduleStatusPoll() {
if (!activeSchedulePoll) return;
window.clearTimeout(activeSchedulePoll.timer);
activeSchedulePoll.controller?.abort();
activeSchedulePoll = null;
}
function startScheduleStatusPoll(payload, message) {
stopScheduleStatusPoll();
const vehicleId = String(payload?.vehicle_id || payload?.vehicle?.id
|| payload?.mode_profile?.vehicle_id || $("#vehicleSelect")?.value || "");
if (!vehicleId) return;
const poll = {
vehicleId, conversationId: conversationId(), message,
sessionId: String(payload?.schedule_execution?.session_id || ""),
runId: String(payload?.schedule_execution?.run_id || ""),
modeProfileId: String(payload?.mode_profile?.id || ""),
timer: null, controller: null,
};
activeSchedulePoll = poll;
const isCurrent = () => activeSchedulePoll === poll
&& conversationId() === poll.conversationId
&& (!$("#vehicleSelect")?.value || $("#vehicleSelect").value === poll.vehicleId);
const update = async () => {
if (!isCurrent()) {
if (activeSchedulePoll === poll) stopScheduleStatusPoll();
return;
}
poll.controller = new AbortController();
try {
const response = await window.CognitiveAgentNetwork.request(
`/api/modes/schedule/status?vehicle_id=${encodeURIComponent(poll.vehicleId)}`,
{ method: "GET", signal: poll.controller.signal, timeoutMs: 8000, retries: 0 },
);
if (!isCurrent()) return;
if (response.status === 401) {
setActionStatus(message, "登录状态已失效,无法继续查询时间轴状态;请重新登录查看。", "failed");
stopScheduleStatusPoll();
return;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const status = await response.json();
if (!isCurrent()) return;
if (status.vehicle_id && String(status.vehicle_id) !== poll.vehicleId) {
throw new Error("时间轴状态车型不匹配");
}
if ((poll.runId && status.run_id && String(status.run_id) !== poll.runId)
|| (poll.sessionId && status.session_id && String(status.session_id) !== poll.sessionId)) {
setActionStatus(message, "当前时间轴已被新的运行任务替换,请查看最新模式消息。", "working");
stopScheduleStatusPoll();
return;
}
const terminal = ["completed", "failed", "cancelled", "stopped", "idle"].includes(status.status);
if (status.status === "running") {
const phase = Number.isInteger(status.phase_index) && status.phase_index >= 0
? ` · 阶段 ${status.phase_index + 1}` : "";
const repeat = Number.isInteger(status.repeat_index) && status.repeat_index >= 0
? ` · 第 ${status.repeat_index + 1} 轮` : "";
setActionStatus(message, `动态时间轴执行中${repeat}${phase}。可输入“停止动态模式”;已执行状态不自动回滚。`, "working");
} else if (terminal) {
const stopped = ["cancelled", "stopped"].includes(status.status);
const failed = status.status === "failed";
const copy = failed
? `动态时间轴执行失败:${status.error || "某个阶段未通过执行校验"}。后续阶段不再执行,已执行状态不自动回滚。`
: stopped
? "动态时间轴已停止,后续阶段不再执行;已执行状态不自动回滚。"
: status.status === "completed"
? "动态时间轴已结束,全部计划阶段已处理;请以车辆反馈确认设备实际状态。"
: "当前没有正在运行的动态时间轴。";
setActionStatus(message, copy, failed ? "failed" : "success");
window.CognitiveAgentConsole?.updateScheduleExecutionStatus?.(poll.modeProfileId, status.status);
stopScheduleStatusPoll();
return;
}
} catch (error) {
if (!isCurrent() || poll.controller?.signal.aborted) return;
setActionStatus(message, "暂时无法获取时间轴状态,正在重新查询;需要停止时可输入“停止动态模式”。", "working");
} finally {
if (isCurrent()) poll.timer = window.setTimeout(update, 2000);
}
};
poll.timer = window.setTimeout(update, 2000);
}
function applyAssistantEvent(event, message) {
if (event.type === "validated_control_commands" && event.payload) {
window.dispatchEvent(new CustomEvent(
@@ -1295,14 +1711,20 @@
if (event.type === "action.completed" || event.type === "work.completed") {
latestControlResult = event.payload || latestControlResult;
const completed = event.success !== false;
const scheduleRunning = completed
&& event.payload?.orchestration_route === "mode_schedule_execute"
&& (event.payload?.status === "running" || event.payload?.schedule_execution?.status === "running");
const defaultText = event.type === "work.completed"
? (completed ? "AI助手已完成工作" : "AI助手未能完成工作")
: (completed ? "座舱控制已完成" : "座舱控制未完成");
setActionStatus(
message,
event.display_text || defaultText,
completed ? "success" : "failed",
scheduleRunning
? "已启动动态时间轴;后续动作将分阶段执行。可输入“停止动态模式”提前结束,已执行的设备状态不会自动回滚。"
: event.display_text || defaultText,
scheduleRunning ? "working" : completed ? "success" : "failed",
);
if (scheduleRunning) startScheduleStatusPoll(event.payload, message);
if (event.payload) {
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
appendDispatchedControls(message, event.payload);
@@ -1574,8 +1996,15 @@
: beginModeExecution(profile, { command: commandText });
const message = activeHandle.message;
const status = String(result?.status || "");
const scheduleRunning = status === "running"
&& result?.schedule_execution?.status === "running";
const dryRun = result?.execution_mode === "dry_run";
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
const delegatedCount = Array.isArray(result?.deferred_requests)
? result.deferred_requests.filter(
(item) => item?.execution_backend === "n7_vehicle_transfer",
).length
: 0;
let content = `${modeName}执行未完成,请查看控制指令状态。`;
let statusText = "功能模式执行未完成";
@@ -1583,6 +2012,10 @@
if (error) {
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
statusText = "功能模式执行失败";
} else if (scheduleRunning) {
content = `已启动动态时间轴:${commandText}。后续动作将分阶段执行。`;
statusText = "动态时间轴执行中,可输入“停止动态模式”提前结束";
statusState = "working";
} else if (status === "succeeded") {
content = dryRun
? `已完成模拟编排:${commandText}。`
@@ -1596,13 +2029,21 @@
: "功能模式执行已完成";
statusState = "success";
} else if (status === "partially_succeeded") {
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
statusText = "功能模式部分完成";
content = delegatedCount > 0
? `${modeName}指令已提交,未由 PC 执行的 ${delegatedCount} 项动作将由车机继续处理。`
: waitingForFeedback
? `${modeName}指令已提交,正在等待车辆状态确认。`
: `${modeName}执行结果已更新,请查看各控制指令状态。`;
statusText = waitingForFeedback || delegatedCount > 0
? "功能模式指令已提交"
: "功能模式执行结果已更新";
statusState = waitingForFeedback || delegatedCount > 0 ? "working" : "success";
}
setMessageContent(message, content, { final: true });
setActionStatus(message, statusText, statusState);
if (result) appendDispatchedControls(message, result);
if (scheduleRunning) startScheduleStatusPoll(result, message);
persistMessages();
return activeHandle;
}
@@ -1689,9 +2130,15 @@
const target = event.target instanceof Element ? event.target : null;
if (!target?.closest("#newConversation")) return;
event.preventDefault?.();
stopScheduleStatusPoll();
resetConversation();
}, true);
document.addEventListener("change", (event) => {
if (event.target?.id === "vehicleSelect") stopScheduleStatusPoll();
});
window.addEventListener("pagehide", stopScheduleStatusPoll);
document.addEventListener("DOMContentLoaded", initialize);
window.CognitiveAgentAssistant = {
submit: submitAssistant,
+14 -8
View File
@@ -571,9 +571,20 @@
const explicitlyBlocked = executionStatus === "blocked"
|| verificationStatus === "blocked"
|| Boolean(telemetry.authorization_reason);
const executionFailed = executionStatus === "failed"
// Closed-loop evidence is authoritative. Some MQTT window executors
// retain a legacy `failed` operation status even after fresh 0x388
// feedback confirms the requested target; do not render that as a
// failed dispatch once the physical result is verified.
const verified = !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const executionFailed = !verified && (
executionStatus === "failed"
|| explicitlyBlocked
|| verificationStatus === "failed_or_blocked";
|| verificationStatus === "failed_or_blocked"
);
const missingWindowFeedback = /0x388/i.test(failureMessage) && (
/no fresh valid/i.test(failureMessage)
|| /未收到.*(?:有效|新鲜).*车窗位置/.test(failureMessage)
@@ -591,11 +602,6 @@
|| /thermal protection|热保护/i.test(failureMessage)
);
const dispatchFailed = executionFailed && !submitted && !safetyBlocked;
const verified = !executionFailed && !aborted && (
verificationStatus === "verified_success"
|| telemetry.physical_effect_confirmed === true
|| telemetry.feedback_match === true
);
const deliveryState = aborted
? "aborted"
: safetyBlocked
@@ -1325,7 +1331,7 @@
}
const levelOptions = [
["off", "关闭"], ["high", "1 档(高)"], ["low", "2 档(低)"],
["off", "关闭"], ["low", "1 档(低)"], ["high", "3 档(高)"],
];
const toggleOptions = [["off", "关闭"], ["on", "开启"]];
const massageModeOptions = [
+3 -3
View File
@@ -5,7 +5,7 @@
inset: 0;
display: grid;
place-items: center;
padding: 68px 20px 20px;
padding: 16px;
background: color-mix(in oklch, var(--background) 58%, transparent);
backdrop-filter: blur(6px) saturate(.9);
-webkit-backdrop-filter: blur(6px) saturate(.9);
@@ -17,8 +17,8 @@
}
.console-v2 .energy-monitor-page {
width: min(1180px, 100%);
height: min(664px, 100%);
width: min(1480px, 100%);
height: min(900px, 100%);
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
@@ -0,0 +1,75 @@
.console-v2 .firmware-workbench-layer {
position: fixed;
z-index: 5000;
inset: 0;
display: grid;
place-items: center;
padding: 16px;
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(1480px, 100%);
height: min(900px, 100%);
min-height: 0;
display: grid;
grid-template-rows: 76px minmax(0, 1fr);
overflow: hidden;
color: var(--foreground);
background: color-mix(in oklch, var(--card) 97%, var(--background));
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
border-radius: 22px;
box-shadow: 0 28px 70px color-mix(in oklch, var(--foreground) 22%, transparent), 0 4px 18px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 1px 0 color-mix(in oklch, var(--primary-foreground) 85%, transparent);
animation: firmware-workbench-enter 180ms ease-out both;
}
.console-v2 .firmware-workbench-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
padding: 14px 18px 13px 20px;
border-bottom: 1px solid color-mix(in oklch, var(--primary) 10%, var(--border));
background: color-mix(in oklch, var(--card) 85%, transparent);
}
.console-v2 .firmware-workbench-heading { min-width: 0; display: flex; align-items: center; gap: 12px; }
.console-v2 .firmware-workbench-heading > span {
width: 46px;
height: 46px;
flex: 0 0 46px;
display: grid;
place-items: center;
color: var(--primary);
background: color-mix(in oklch, var(--primary) 10%, var(--card));
border: 1px solid color-mix(in oklch, var(--primary) 22%, var(--border));
border-radius: 15px;
}
.console-v2 .firmware-workbench-heading svg { width: 24px; height: 24px; stroke-width: 1.8; }
.console-v2 .firmware-workbench-heading h2 { margin: 0; color: var(--heading-foreground, var(--foreground)); font-size: 20px; line-height: 1.2; letter-spacing: -.025em; }
.console-v2 #firmwareWorkbenchClose {
width: 38px;
height: 38px;
display: grid;
flex: 0 0 38px;
place-items: center;
padding: 0;
color: var(--foreground);
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
border: 1px solid color-mix(in oklch, var(--primary) 13%, var(--border));
border-radius: 12px;
cursor: pointer;
}
.console-v2 #firmwareWorkbenchClose:hover,
.console-v2 #firmwareWorkbenchClose:focus-visible { color: var(--primary); background: var(--accent); outline: 3px solid color-mix(in oklch, var(--ring) 16%, transparent); }
.console-v2 #firmwareWorkbenchClose svg { width: 18px; height: 18px; }
.console-v2 .firmware-workbench-frame { width: 100%; height: 100%; display: block; border: 0; background: transparent; }
@keyframes firmware-workbench-enter { from { opacity: 0; transform: translateY(8px) scale(.992); } to { opacity: 1; transform: none; } }
@media (max-width: 760px) {
.console-v2 .firmware-workbench-layer { padding: 54px 8px 8px; }
.console-v2 .firmware-workbench-page { height: 100%; border-radius: 17px; grid-template-rows: 64px minmax(0, 1fr); }
.console-v2 .firmware-workbench-header { padding: 9px 11px; }
.console-v2 .firmware-workbench-heading > span { width: 40px; height: 40px; flex-basis: 40px; border-radius: 12px; }
.console-v2 .firmware-workbench-heading h2 { font-size: 17px; }
}
@@ -0,0 +1,127 @@
(() => {
"use strict";
const state = { open: false, returnFocus: null, backgroundInert: [] };
function currentWorkbenchTheme() {
return document.documentElement.dataset.glassTheme === "light" ? "light" : "dark";
}
function syncWorkbenchTheme(frame) {
frame?.contentWindow?.postMessage({
type: "cognitive-agent:theme-change",
theme: currentWorkbenchTheme(),
}, window.location.origin);
}
function setBackgroundInert(inert) {
const layer = document.querySelector("#firmwareWorkbenchLayer");
if (!layer) return;
if (inert) {
state.backgroundInert = [...document.body.children]
.filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName))
.map((element) => ({ element, wasInert: element.hasAttribute("inert") }));
state.backgroundInert.forEach(({ element }) => element.toggleAttribute("inert", true));
return;
}
state.backgroundInert.forEach(({ element, wasInert }) => element.toggleAttribute("inert", wasInert));
state.backgroundInert = [];
}
function setOpen(open) {
const next = Boolean(open);
const layer = document.querySelector("#firmwareWorkbenchLayer");
const trigger = document.querySelector("#firmwareWorkbenchToggle");
if (!layer || !trigger || state.open === next) return;
state.open = next;
trigger.classList.toggle("active", next);
trigger.setAttribute("aria-pressed", String(next));
trigger.setAttribute("aria-expanded", String(next));
trigger.setAttribute("aria-label", next ? "关闭座椅固件开发工作台" : "打开座椅固件开发工作台");
trigger.title = next ? "关闭座椅固件开发工作台" : "打开座椅固件开发工作台";
if (next) {
state.returnFocus = document.activeElement;
layer.hidden = false;
layer.inert = false;
layer.dataset.open = "true";
setBackgroundInert(true);
requestAnimationFrame(() => document.querySelector("#firmwareWorkbenchClose")?.focus());
} else {
layer.dataset.open = "false";
layer.inert = true;
layer.hidden = true;
setBackgroundInert(false);
const target = state.returnFocus instanceof HTMLElement && state.returnFocus.isConnected
? state.returnFocus
: trigger;
state.returnFocus = null;
target.focus();
}
}
function mount() {
const dock = document.querySelector("#v2PanelControlDock");
if (!dock || document.querySelector("#firmwareWorkbenchToggle")) return false;
const trigger = document.createElement("button");
trigger.id = "firmwareWorkbenchToggle";
trigger.className = "firmware-workbench-toggle v2-panel-control-button";
trigger.type = "button";
trigger.setAttribute("aria-controls", "firmwareWorkbenchLayer");
trigger.setAttribute("aria-pressed", "false");
trigger.setAttribute("aria-expanded", "false");
trigger.setAttribute("aria-label", "打开座椅固件开发工作台");
trigger.title = "打开座椅固件开发工作台";
trigger.innerHTML = '<i data-lucide="blocks"></i><span class="visually-hidden">座椅固件开发工作台</span>';
const layer = document.createElement("div");
layer.id = "firmwareWorkbenchLayer";
layer.className = "firmware-workbench-layer";
layer.hidden = true;
layer.inert = true;
layer.setAttribute("role", "dialog");
layer.setAttribute("aria-modal", "true");
layer.setAttribute("aria-labelledby", "firmwareWorkbenchTitle");
layer.innerHTML = `
<section class="firmware-workbench-page">
<header class="firmware-workbench-header">
<div class="firmware-workbench-heading">
<span><i data-lucide="blocks"></i></span>
<h2 id="firmwareWorkbenchTitle">座椅固件开发工作台</h2>
</div>
<button id="firmwareWorkbenchClose" type="button" aria-label="关闭座椅固件开发工作台" title="关闭"><i data-lucide="x"></i></button>
</header>
<iframe class="firmware-workbench-frame" title="座椅固件开发工作台"></iframe>
</section>`;
dock.append(trigger);
document.body.append(layer);
const frame = document.querySelector(".firmware-workbench-frame", layer);
frame.src = `/firmware-workbench.html?embedded=1&theme=${currentWorkbenchTheme()}`;
frame.addEventListener("load", () => syncWorkbenchTheme(frame));
window.addEventListener("cognitive-agent:theme-change", () => syncWorkbenchTheme(frame));
new MutationObserver(() => syncWorkbenchTheme(frame)).observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-glass-theme"],
});
trigger.addEventListener("click", () => setOpen(!state.open));
document.querySelector("#firmwareWorkbenchClose", layer).addEventListener("click", () => setOpen(false));
layer.addEventListener("click", (event) => { if (event.target === layer) setOpen(false); });
document.addEventListener("keydown", (event) => {
if (state.open && event.key === "Escape") {
event.preventDefault();
setOpen(false);
}
});
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
return true;
}
if (!mount()) {
const observer = new MutationObserver(() => {
if (mount()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
window.setTimeout(() => observer.disconnect(), 15000);
}
})();
@@ -0,0 +1,65 @@
(() => {
"use strict";
const STORAGE_KEY = "cognitive-agent.glass-theme";
function normalizeTheme(value) {
return value === "light" ? "light" : value === "dark" ? "dark" : "";
}
function themeFromCookie() {
try {
return normalizeTheme(document.cookie
.split(";")
.map((part) => part.trim())
.find((part) => part.startsWith(`${STORAGE_KEY}=`))
?.slice(STORAGE_KEY.length + 1));
} catch (_error) {
return "";
}
}
function resolveInitialTheme() {
const requested = normalizeTheme(new URLSearchParams(window.location.search).get("theme"));
if (requested) return requested;
try {
const parentTheme = normalizeTheme(window.parent?.document?.documentElement?.dataset?.glassTheme);
if (parentTheme) return parentTheme;
} catch (_error) {
// Standalone and cross-origin rendering continue with local preferences.
}
try {
const stored = normalizeTheme(localStorage.getItem(STORAGE_KEY));
if (stored) return stored;
} catch (_error) {
// Storage may be unavailable in hardened browser contexts.
}
return themeFromCookie() || "dark";
}
function applyTheme(theme) {
const normalized = normalizeTheme(theme) || "dark";
document.documentElement.dataset.glassTheme = normalized;
document.documentElement.style.colorScheme = normalized;
document.querySelector("#firmwareWorkbenchThemeColor")
?.setAttribute("content", normalized === "dark" ? "#0f1526" : "#f3f6fb");
return normalized;
}
applyTheme(resolveInitialTheme());
window.addEventListener("storage", (event) => {
if (event.key === STORAGE_KEY) applyTheme(event.newValue);
});
window.addEventListener("message", (event) => {
if (event.origin !== window.location.origin) return;
if (window.parent !== window && event.source !== window.parent) return;
if (event.data?.type !== "cognitive-agent:theme-change") return;
applyTheme(event.data.theme);
});
window.CognitiveAgentFirmwareWorkbenchTheme = Object.freeze({
apply: applyTheme,
current: () => document.documentElement.dataset.glassTheme,
});
})();
+626
View File
@@ -0,0 +1,626 @@
: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; }
.table-actions .notify-action {
flex: 0 0 42px;
width: 42px;
min-width: 42px;
height: 34px;
margin-left: auto;
padding: 6px;
border-radius: 8px;
color: #fff;
background: var(--primary);
box-shadow: 0 4px 10px rgba(40, 105, 232, .2);
}
.table-actions .notify-action svg { width: 20px; height: 20px; stroke-width: 2.25; }
.table-actions .notify-action:hover { background: color-mix(in srgb, var(--primary) 86%, #132447); }
.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(1040px, 100%); max-height: min(820px, 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; }
}
/* The workbench is rendered in an iframe, so it owns a complete theme token
set instead of relying on custom properties from the parent document. */
:root[data-glass-theme="dark"] {
color-scheme: dark;
--page: #090e1c;
--surface: #171e31;
--surface-soft: #11182a;
--line: #303a55;
--line-strong: #44516d;
--text: #edf2ff;
--muted: #9ba9c0;
--primary: #7aa3ff;
--primary-soft: #1b2b53;
--success: #55d4a1;
--success-soft: #12372d;
--warning: #f2bd68;
--danger: #ff8993;
--shadow: 0 26px 70px rgba(0, 0, 0, .42), 0 4px 18px rgba(0, 0, 0, .24);
}
:root[data-glass-theme="dark"] body:not(.embedded) {
background:
radial-gradient(circle at 15% 8%, color-mix(in srgb, var(--primary) 18%, transparent), transparent 31%),
linear-gradient(145deg, #11182b 0%, var(--page) 100%);
}
:root[data-glass-theme="dark"] body.embedded { background: transparent; }
:root[data-glass-theme="dark"] .shell,
:root[data-glass-theme="dark"] body.embedded .shell {
background: color-mix(in srgb, var(--surface-soft) 86%, var(--page));
border-color: color-mix(in srgb, var(--line-strong) 78%, transparent);
}
:root[data-glass-theme="dark"] .topbar {
background: color-mix(in srgb, var(--surface) 94%, transparent);
}
:root[data-glass-theme="dark"] :is(.back-link, .icon-button, .button-secondary) {
color: var(--muted);
background: var(--surface);
border-color: var(--line-strong);
}
:root[data-glass-theme="dark"] :is(.back-link, .icon-button):hover {
color: var(--primary);
border-color: color-mix(in srgb, var(--primary) 50%, var(--line));
background: var(--primary-soft);
}
:root[data-glass-theme="dark"] :is(.eyebrow, .section-kicker) { color: var(--muted); }
:root[data-glass-theme="dark"] :is(.environment-chip, .server-state) {
color: var(--muted);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] .server-state span { background: color-mix(in srgb, var(--muted) 72%, transparent); }
:root[data-glass-theme="dark"] .server-state.online,
:root[data-glass-theme="dark"] .status-badge[data-status="baseline"] {
border-color: color-mix(in srgb, var(--success) 44%, var(--line));
background: var(--success-soft);
}
:root[data-glass-theme="dark"] .card {
box-shadow: 0 2px 10px rgba(0, 0, 0, .16);
}
:root[data-glass-theme="dark"] .command-card > label { color: var(--muted); }
:root[data-glass-theme="dark"] textarea:focus { background: var(--surface); }
:root[data-glass-theme="dark"] :is(select, option) { color: var(--text); background: var(--surface); }
:root[data-glass-theme="dark"] .prompt-templates button,
:root[data-glass-theme="dark"] .cap-tags span,
:root[data-glass-theme="dark"] .table-actions :is(button, a) {
color: var(--primary);
background: var(--primary-soft);
border-color: color-mix(in srgb, var(--primary) 34%, var(--line));
}
:root[data-glass-theme="dark"] .capability-brief {
border-color: color-mix(in srgb, var(--primary) 25%, var(--line));
background: color-mix(in srgb, var(--primary-soft) 44%, var(--surface-soft));
}
:root[data-glass-theme="dark"] .capability-brief strong { color: var(--text); }
:root[data-glass-theme="dark"] .capability-brief > svg { color: var(--primary); }
:root[data-glass-theme="dark"] .pipeline-stage::after { background: var(--line); }
:root[data-glass-theme="dark"] .stage-node {
color: var(--muted);
border-color: var(--line-strong);
background: var(--surface);
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="success"] .stage-node {
border-color: color-mix(in srgb, var(--success) 62%, var(--line));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="failed"] .stage-node,
:root[data-glass-theme="dark"] .pipeline-stage[data-status="blocked"] .stage-node,
:root[data-glass-theme="dark"] .release-result[data-state="failed"],
:root[data-glass-theme="dark"] .status-badge[data-status="failed"],
:root[data-glass-theme="dark"] .status-badge[data-status="blocked"] {
color: var(--danger);
border-color: color-mix(in srgb, var(--danger) 44%, var(--line));
background: color-mix(in srgb, var(--danger) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="canceled"] .stage-node {
color: var(--muted);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="pending_confirmation"] .stage-node,
:root[data-glass-theme="dark"] .status-badge[data-status="ready"],
:root[data-glass-theme="dark"] .status-badge[data-status="pending_confirmation"] {
color: var(--warning);
border-color: color-mix(in srgb, var(--warning) 44%, var(--line));
background: color-mix(in srgb, var(--warning) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .pipeline-stage[data-status="success"]::after {
background: color-mix(in srgb, var(--success) 64%, var(--line));
}
:root[data-glass-theme="dark"] .release-result[data-state="success"],
:root[data-glass-theme="dark"] .status-badge[data-status="success"],
:root[data-glass-theme="dark"] .status-badge[data-status="published"] {
border-color: color-mix(in srgb, var(--success) 44%, var(--line));
background: var(--success-soft);
}
:root[data-glass-theme="dark"] .result-icon {
color: var(--muted);
background: color-mix(in srgb, var(--surface-soft) 78%, var(--line));
}
:root[data-glass-theme="dark"] .release-result[data-state="success"] .result-icon {
color: var(--success);
background: color-mix(in srgb, var(--success) 18%, var(--surface));
}
:root[data-glass-theme="dark"] .metrics div { border-color: var(--line); }
:root[data-glass-theme="dark"] :is(.log-stream, .log-details) { background: var(--surface-soft); }
:root[data-glass-theme="dark"] .empty-state svg { color: var(--muted); }
:root[data-glass-theme="dark"] .empty-state strong { color: var(--text); }
:root[data-glass-theme="dark"] .log-entry,
:root[data-glass-theme="dark"] :is(.change-item, .test-item) { border-color: var(--line); }
:root[data-glass-theme="dark"] .log-time { color: var(--muted); }
:root[data-glass-theme="dark"] .log-dot { background: var(--muted); }
:root[data-glass-theme="dark"] .log-copy p { color: var(--muted); }
:root[data-glass-theme="dark"] .inspector-tabs { background: var(--surface); }
:root[data-glass-theme="dark"] .inspector-tabs button { color: var(--muted); }
:root[data-glass-theme="dark"] .inspector-tabs button.active {
color: var(--primary);
background: var(--surface-soft);
}
:root[data-glass-theme="dark"] :is(.build-output, .code-block) { color: var(--text); }
:root[data-glass-theme="dark"] .server-target strong { color: var(--text); }
:root[data-glass-theme="dark"] th { color: var(--muted); background: var(--surface); }
:root[data-glass-theme="dark"] td { color: var(--muted); border-color: var(--line); }
:root[data-glass-theme="dark"] :is(.software-name strong, .file-cell strong) { color: var(--text); }
:root[data-glass-theme="dark"] .table-actions .delete-action {
color: var(--danger);
background: color-mix(in srgb, var(--danger) 12%, var(--surface));
}
:root[data-glass-theme="dark"] .table-actions .notify-action {
color: #fff;
background: color-mix(in srgb, var(--primary) 82%, #07101e);
}
:root[data-glass-theme="dark"] .modal-backdrop { background: rgba(2, 5, 14, .68); }
:root[data-glass-theme="dark"] .modal { background: var(--surface); }
:root[data-glass-theme="dark"] .diff-panel p { color: var(--muted); }
:root[data-glass-theme="dark"] .diff-panel.added {
border-color: color-mix(in srgb, var(--success) 42%, var(--line));
}
+144
View File
@@ -0,0 +1,144 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#0f1526" id="firmwareWorkbenchThemeColor" />
<title>座椅固件开发工作台 · 511 工程</title>
<link rel="icon" href="/icons/app-icon-192.png" />
<script src="/firmware-workbench-theme.js?v=1"></script>
<link rel="stylesheet" href="/firmware-workbench.css?v=7" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/firmware-workbench.js?v=4" defer></script>
</head>
<body>
<div class="shell">
<header class="topbar">
<div class="topbar-main">
<a class="back-link" href="/" aria-label="返回智能座舱">
<i data-lucide="arrow-left"></i>
</a>
<div class="product-mark" aria-hidden="true"><i data-lucide="blocks"></i></div>
<h1>座椅固件开发工作台</h1>
</div>
<div class="topbar-status">
<span class="server-state" id="serverState"><span></span>正在连接发布服务</span>
</div>
</header>
<main>
<section class="hero-grid" aria-label="固件开发任务">
<article class="card command-card">
<div class="card-heading">
<div>
<h2>开发指令</h2>
</div>
<label class="baseline-picker" for="baselineSelect">
<i data-lucide="git-branch"></i>
<span>固件基线</span>
<select id="baselineSelect" aria-label="选择固件基线">
<option value="time_only">时间控制</option>
</select>
</label>
</div>
<label for="instructionInput">描述需要扩展的副驾座椅能力</label>
<textarea id="instructionInput" rows="5" maxlength="2000">将副驾座椅固件从仅时间控制扩展为时间+位置参数控制,并增加座椅通风和座椅加热功能;完成编译、测试验证、固件打包,并发布到升级服务器。</textarea>
<div class="prompt-templates" aria-label="常用开发指令">
<button type="button" data-prompt="增加支持座椅位置参数控制的能力">+ 位置控制</button>
<button type="button" data-prompt="增加座椅通风功能">+ 座椅通风</button>
<button type="button" data-prompt="增加座椅加热功能">+ 座椅加热</button>
</div>
<div class="capability-brief" aria-label="固件能力升级范围">
<div><span>当前基线</span><strong id="baselineCapabilities">时间控制</strong></div>
<i data-lucide="chevrons-right"></i>
<div class="capability-target"><span>目标固件能力</span><strong id="targetCapabilities">时间控制+位置控制+座椅通风+座椅加热</strong></div>
</div>
<div class="command-footer">
<div class="button-row">
<button class="button button-secondary" id="cancelTaskButton" type="button" disabled>
<i data-lucide="square"></i>取消任务
</button>
<button class="button button-primary" id="startTaskButton" type="button">
<i data-lucide="play"></i>开始生成
</button>
</div>
</div>
</article>
</section>
<section class="card pipeline-card" aria-label="固件交付流水线">
<ol class="pipeline" id="pipeline" aria-label="固件交付阶段"></ol>
<div class="release-result" id="releaseResult" data-state="idle">
<span class="result-icon"><i data-lucide="cloud-upload"></i></span>
<div><small>当前交付状态</small><strong>尚未开始</strong></div>
</div>
<div class="status-legend" aria-label="阶段状态图例">
<span data-status="waiting">等待</span><span data-status="running">运行中</span>
<span data-status="pending_confirmation">待确认</span><span data-status="success">成功</span>
<span data-status="failed">失败</span><span data-status="canceled">取消</span>
<span data-status="blocked">阻塞</span>
</div>
</section>
<section class="execution-grid">
<article class="card log-card" aria-labelledby="logTitle">
<div class="card-heading">
<div>
<h2 id="logTitle">任务执行日志</h2>
</div>
<div class="log-actions">
<span class="status-badge" id="taskStatusBadge" data-status="waiting">等待</span>
<button class="icon-button" id="retryTaskButton" type="button" title="重试失败任务" disabled><i data-lucide="refresh-cw"></i></button>
<button class="icon-button" id="scrollLogButton" type="button" title="滚动到最新日志"><i data-lucide="arrow-down-to-line"></i></button>
</div>
</div>
<dl class="metrics" id="taskMetrics">
<div><dt>开始时间</dt><dd>—</dd></div><div><dt>结束时间</dt><dd>—</dd></div>
<div><dt>实际耗时</dt><dd>—</dd></div><div><dt>文件变更</dt><dd>—</dd></div>
</dl>
<div class="log-workspace">
<div class="log-stream" id="logStream" role="log" aria-live="polite">
<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>
</div>
<aside class="log-details" aria-label="日志附属详情">
<div class="inspector-tabs" role="tablist">
<button class="active" type="button" role="tab" data-inspector-tab="changes">文件变更</button>
<button type="button" role="tab" data-inspector-tab="build">编译输出</button>
<button type="button" role="tab" data-inspector-tab="tests">测试验证</button>
</div>
<div class="inspector-content" id="inspectorContent">
<div class="empty-inline">暂无任务数据</div>
</div>
</aside>
</div>
</article>
</section>
<section class="card software-card" aria-labelledby="softwareTitle">
<div class="card-heading software-heading">
<div>
<h2 id="softwareTitle">固件版本与软件产物</h2>
</div>
<div class="server-target"><i data-lucide="server"></i><span>发布目标</span><strong id="otaServerName">车机升级服务器</strong></div>
</div>
<div class="table-wrap">
<table>
<thead><tr><th>软件 / 版本</th><th>能力</th><th>构建时间</th><th>固件文件 / 校验</th><th>状态</th><th>操作</th></tr></thead>
<tbody id="artifactTableBody"><tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr></tbody>
</table>
</div>
</section>
</main>
</div>
<div class="modal-backdrop" id="detailModal" hidden>
<section class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<header><h2 id="modalTitle">固件详情</h2><button class="icon-button" id="closeModalButton" type="button" aria-label="关闭"><i data-lucide="x"></i></button></header>
<div class="modal-body" id="modalBody"></div>
<footer><button class="button button-secondary" id="modalCloseButton" type="button">关闭</button></footer>
</section>
</div>
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
</body>
</html>
+460
View File
@@ -0,0 +1,460 @@
(() => {
"use strict";
if (window.self !== window.top || new URLSearchParams(window.location.search).has("embedded")) {
document.body.classList.add("embedded");
}
const STATUS_LABELS = {
waiting: "等待",
running: "运行中",
pending_confirmation: "待确认",
success: "成功",
failed: "失败",
canceled: "取消",
blocked: "阻塞",
published: "已发布",
ready: "待发布",
baseline: "基线固件",
};
const STAGE_ICONS = {
instruction: "file-input",
requirements: "list-checks",
baseline: "git-compare-arrows",
codegen: "code-2",
build: "hammer",
test: "shield-check",
package: "package-check",
publish: "cloud-upload",
};
const ACTIVE_STATES = new Set(["waiting", "running", "pending_confirmation"]);
const CAPABILITY_ORDER = ["时间控制", "位置控制", "座椅通风", "座椅加热"];
const FALLBACK_BASELINES = {
time_only: ["时间控制"],
};
const FALLBACK_PIPELINE = [
["instruction", "指令输入"], ["requirements", "需求解析"],
["baseline", "基线检查"], ["codegen", "代码生成"],
["build", "编译构建"], ["test", "测试验证"],
["package", "固件打包"], ["publish", "发布服务器"],
].map(([id, name]) => ({ id, name, status: "waiting", summary: "等待执行" }));
const ui = {
instruction: document.querySelector("#instructionInput"),
baseline: document.querySelector("#baselineSelect"),
baselineCapabilities: document.querySelector("#baselineCapabilities"),
targetCapabilities: document.querySelector("#targetCapabilities"),
start: document.querySelector("#startTaskButton"),
cancel: document.querySelector("#cancelTaskButton"),
retry: document.querySelector("#retryTaskButton"),
server: document.querySelector("#serverState"),
serverName: document.querySelector("#otaServerName"),
release: document.querySelector("#releaseResult"),
pipeline: document.querySelector("#pipeline"),
logs: document.querySelector("#logStream"),
scrollLogs: document.querySelector("#scrollLogButton"),
status: document.querySelector("#taskStatusBadge"),
metrics: document.querySelector("#taskMetrics"),
inspector: document.querySelector("#inspectorContent"),
artifacts: document.querySelector("#artifactTableBody"),
modal: document.querySelector("#detailModal"),
modalTitle: document.querySelector("#modalTitle"),
modalBody: document.querySelector("#modalBody"),
modalClose: document.querySelector("#closeModalButton"),
modalCloseFooter: document.querySelector("#modalCloseButton"),
toasts: document.querySelector("#toastRegion"),
};
const model = {
snapshot: { tasks: [], artifacts: [], releases: [], pipeline: FALLBACK_PIPELINE },
selectedTaskId: null,
inspectorTab: "changes",
pending: false,
pollTimer: null,
lastLogCount: 0,
};
function escapeHtml(value) {
return String(value ?? "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#039;");
}
async function api(path, options = {}) {
const response = await fetch(path, {
credentials: "same-origin",
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
...options,
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`);
return payload;
}
function selectedTask() {
return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId)
|| model.snapshot.tasks[0]
|| null;
}
function baselineCapabilities() {
const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value);
return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only;
}
function parseInstructionCapabilities(instruction) {
const lowered = String(instruction || "").toLowerCase();
const aliases = {
"时间控制": ["时间", "定时", "timed"],
"位置控制": ["位置", "position"],
"座椅通风": ["通风", "vent"],
"座椅加热": ["加热", "heater", "heat"],
};
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
}
function updateCapabilityPreview(task = null) {
const baseline = baselineCapabilities();
const taskMatchesDraft = task
&& task.instruction === ui.instruction.value.trim()
&& (task.baseline_id || "time_only") === ui.baseline.value;
const target = taskMatchesDraft
? task.target_capabilities
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
ui.baselineCapabilities.textContent = baseline.join("+");
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
}
function formatDate(value) {
if (!value) return "—";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value;
return new Intl.DateTimeFormat("zh-CN", {
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
hour12: false,
}).format(date).replaceAll("/", "-");
}
function formatClock(value) {
if (!value) return "--:--:--";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "--:--:--";
return new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
}).format(date);
}
function formatDuration(milliseconds) {
if (milliseconds === null || milliseconds === undefined) return "—";
if (milliseconds < 1000) return `${milliseconds} 毫秒`;
const seconds = milliseconds / 1000;
return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`;
}
function formatSize(bytes) {
const value = Number(bytes || 0);
if (value < 1024) return `${value} 字节`;
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`;
return `${(value / 1024 / 1024).toFixed(2)} 兆字节`;
}
function toast(message, type = "info") {
const element = document.createElement("div");
element.className = `toast ${type}`;
element.textContent = message;
ui.toasts.append(element);
window.setTimeout(() => element.remove(), 4200);
}
function refreshIcons() {
if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } });
}
function renderPipeline(task) {
const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE;
ui.pipeline.innerHTML = stages.map((stage) => `
<li class="pipeline-stage" data-status="${escapeHtml(stage.status || "waiting")}" title="${escapeHtml(stage.summary || "")}">
<span class="stage-node"><i data-lucide="${STAGE_ICONS[stage.id] || "circle"}"></i></span>
<strong class="stage-name">${escapeHtml(stage.name)}</strong>
<span class="stage-state">${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}</span>
</li>`).join("");
}
function renderRelease(task) {
let icon = "cloud-upload";
let label = "尚未开始";
let state = "idle";
if (task) {
label = task.message || STATUS_LABELS[task.status] || task.status;
state = task.status;
if (task.status === "success") { icon = "circle-check-big"; state = "success"; }
else if (task.status === "running") icon = "loader-circle";
else if (task.status === "failed") icon = "circle-x";
else if (task.status === "canceled") icon = "ban";
}
ui.release.dataset.state = state;
ui.release.innerHTML = `<span class="result-icon"><i data-lucide="${icon}"></i></span><div><small>当前交付状态</small><strong>${escapeHtml(label)}</strong></div>`;
}
function renderLogs(task) {
const logs = task?.logs || [];
if (!logs.length) {
ui.logs.innerHTML = `<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>`;
model.lastLogCount = 0;
return;
}
const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80;
ui.logs.innerHTML = logs.map((entry) => `
<article class="log-entry" data-level="${escapeHtml(entry.level)}">
<time class="log-time">${escapeHtml(formatClock(entry.time))}</time>
<span class="log-dot"></span>
<div class="log-copy"><strong>${escapeHtml(entry.title)}</strong><p>${escapeHtml(entry.message)}</p></div>
</article>`).join("");
if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight;
model.lastLogCount = logs.length;
}
function renderInspector(task) {
const status = task?.status || "waiting";
ui.status.dataset.status = status;
ui.status.textContent = STATUS_LABELS[status] || status;
const fileCount = task?.file_changes?.length || 0;
ui.metrics.innerHTML = `
<div><dt>开始时间</dt><dd>${escapeHtml(formatDate(task?.started_at))}</dd></div>
<div><dt>结束时间</dt><dd>${escapeHtml(formatDate(task?.finished_at))}</dd></div>
<div><dt>实际耗时</dt><dd>${escapeHtml(formatDuration(task?.duration_ms))}</dd></div>
<div><dt>文件变更</dt><dd>${fileCount ? `${fileCount} 个文件` : "—"}</dd></div>`;
document.querySelectorAll("[data-inspector-tab]").forEach((button) => {
button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab);
});
if (!task) {
ui.inspector.innerHTML = `<div class="empty-inline">暂无任务数据</div>`;
return;
}
if (model.inspectorTab === "build") {
ui.inspector.innerHTML = task.build_output?.length
? `<pre class="build-output">${escapeHtml(task.build_output.join("\n"))}</pre>`
: `<div class="empty-inline">尚无编译输出</div>`;
return;
}
if (model.inspectorTab === "tests") {
ui.inspector.innerHTML = task.test_results?.length
? task.test_results.map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">测试尚未执行</div>`;
return;
}
ui.inspector.innerHTML = task.file_changes?.length
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
: `<div class="empty-inline">尚无文件变更</div>`;
}
function renderArtifacts() {
const artifacts = model.snapshot.artifacts || [];
if (!artifacts.length) {
ui.artifacts.innerHTML = `<tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr>`;
return;
}
ui.artifacts.innerHTML = artifacts.map((artifact) => `
<tr>
<td><div class="software-name"><strong>${escapeHtml(artifact.software_name)}</strong><span>${escapeHtml(artifact.version)}</span></div></td>
<td><div class="cap-tags">${(artifact.capabilities || []).map((capability) => `<span>${escapeHtml(capability)}</span>`).join("")}</div></td>
<td>${escapeHtml(formatDate(artifact.build_time))}</td>
<td><div class="file-cell"><strong>${escapeHtml(artifact.firmware_filename)}</strong><span>${escapeHtml(formatSize(artifact.size_bytes))} · <span class="hash-value" title="${escapeHtml(artifact.sha256)}">${escapeHtml(artifact.sha256)}</span></span></div></td>
<td><span class="status-badge" data-status="${escapeHtml(artifact.status)}">${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</span></td>
<td><div class="table-actions">
<button type="button" title="查看详情" data-artifact-action="detail" data-artifact-id="${artifact.id}">详情</button>
<button type="button" title="查看能力差异" data-artifact-action="capabilities" data-artifact-id="${artifact.id}">能力</button>
<button type="button" title="查看代码差异" data-artifact-action="code" data-artifact-id="${artifact.id}">代码</button>
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
${artifact.status === "published" ? `<button class="notify-action" type="button" title="通知车端" aria-label="通知车端" data-artifact-action="notify" data-artifact-id="${artifact.id}"><i data-lucide="bell"></i></button>` : ""}
</div></td>
</tr>`).join("");
}
function render() {
const task = selectedTask();
if (task) model.selectedTaskId = task.id;
renderPipeline(task);
renderRelease(task);
renderLogs(task);
renderInspector(task);
renderArtifacts();
updateCapabilityPreview(task);
const active = task && ACTIVE_STATES.has(task.status);
ui.start.disabled = model.pending || Boolean(active);
ui.cancel.disabled = model.pending || !active;
ui.retry.disabled = model.pending || !task || !["failed", "canceled", "blocked"].includes(task.status);
ui.start.innerHTML = model.pending ? `<i data-lucide="loader-circle"></i>正在提交` : `<i data-lucide="play"></i>开始生成`;
refreshIcons();
}
async function refreshState({ quiet = false } = {}) {
try {
model.snapshot = await api("/api/firmware-workbench/state");
if (!model.selectedTaskId && model.snapshot.tasks?.length) model.selectedTaskId = model.snapshot.tasks[0].id;
ui.server.classList.add("online");
ui.server.innerHTML = `<span></span>发布服务已连接`;
ui.serverName.textContent = model.snapshot.ota_server?.name || "车机升级服务器";
render();
} catch (error) {
ui.server.classList.remove("online");
ui.server.innerHTML = `<span></span>发布服务连接失败`;
if (!quiet) toast(error.message, "error");
} finally {
schedulePoll();
}
}
function schedulePoll() {
window.clearTimeout(model.pollTimer);
const active = model.snapshot.tasks?.some((task) => ACTIVE_STATES.has(task.status));
model.pollTimer = window.setTimeout(() => refreshState({ quiet: true }), active ? 700 : 5000);
}
async function startTask() {
const instruction = ui.instruction.value.trim();
if (!instruction) { toast("请输入开发指令。", "error"); ui.instruction.focus(); return; }
model.pending = true;
render();
try {
const result = await api("/api/firmware-workbench/tasks", {
method: "POST", body: JSON.stringify({ instruction, baseline_id: ui.baseline.value }),
});
model.selectedTaskId = result.task.id;
toast("开发任务已创建,流水线开始执行。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
async function taskAction(action) {
const task = selectedTask();
if (!task) return;
model.pending = true;
render();
try {
await api(`/api/firmware-workbench/tasks/${encodeURIComponent(task.id)}/${action}`, { method: "POST", body: "{}" });
toast(action === "cancel" ? "取消请求已提交。" : "任务已重新进入流水线。", "success");
await refreshState({ quiet: true });
} catch (error) {
toast(error.message, "error");
} finally {
model.pending = false;
render();
}
}
function appendPrompt(text) {
const current = ui.instruction.value.trim();
if (current.includes(text)) return;
ui.instruction.value = current ? `${current.replace(/[。;;]?$/, "")};${text}。` : `${text}。`;
ui.instruction.focus();
updateCapabilityPreview();
}
function showModal(title, content) {
ui.modalTitle.textContent = title;
ui.modalBody.innerHTML = content;
ui.modal.hidden = false;
document.body.style.overflow = "hidden";
refreshIcons();
ui.modalClose.focus();
}
function closeModal() {
ui.modal.hidden = true;
document.body.style.overflow = "";
}
async function artifactAction(button) {
const artifact = model.snapshot.artifacts.find((item) => item.id === button.dataset.artifactId);
if (!artifact) return;
const action = button.dataset.artifactAction;
if (action === "delete") {
if (!window.confirm(`确认删除“${artifact.firmware_filename}”及其服务器发布记录吗?此操作不可恢复。`)) return;
button.disabled = true;
try {
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}`, { method: "DELETE" });
toast(result.message, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "publish") {
button.disabled = true;
try {
const release = await api(`/api/firmware-workbench/artifacts/${artifact.id}/publish`, { method: "POST", body: "{}" });
toast(`${release.message}(${release.id})`, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "notify") {
button.disabled = true;
try {
const result = await api(`/api/firmware-workbench/artifacts/${artifact.id}/notify`, { method: "POST", body: "{}" });
toast(`已向车端发送 ${result.published_count || result.burst_count || 3} 帧通知。`, "success");
await refreshState({ quiet: true });
} catch (error) { toast(error.message, "error"); }
finally { button.disabled = false; }
return;
}
if (action === "detail") {
showModal("固件产物详情", `<div class="detail-grid">
<div class="detail-field"><span>软件名称</span><strong>${escapeHtml(artifact.software_name)}</strong></div>
<div class="detail-field"><span>版本</span><strong>${escapeHtml(artifact.version)}</strong></div>
<div class="detail-field"><span>固件包</span><strong>${escapeHtml(artifact.firmware_filename)}</strong></div>
<div class="detail-field"><span>构建时间</span><strong>${escapeHtml(formatDate(artifact.build_time))}</strong></div>
<div class="detail-field"><span>固件包大小</span><strong>${escapeHtml(formatSize(artifact.size_bytes))}</strong></div>
<div class="detail-field"><span>发布状态</span><strong>${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</strong></div>
<div class="detail-field"><span>SHA-256(固件包)</span><strong>${escapeHtml(artifact.sha256)}</strong></div>
<div class="detail-field"><span>SHA-256(HEX 镜像)</span><strong>${escapeHtml(artifact.image_sha256)}</strong></div>
</div>`);
} else if (action === "capabilities") {
const diff = artifact.capability_diff || {};
showModal("能力差异", `<div class="diff-columns">
<div class="diff-panel"><h3>固件基线</h3>${(artifact.baseline_capabilities || []).map((item) => `<p>• ${escapeHtml(item)}</p>`).join("")}</div>
<div class="diff-panel added"><h3>目标固件变化</h3>${(diff.added || []).map((item) => `<p>+ ${escapeHtml(item)}</p>`).join("")}${(diff.retained || []).map((item) => `<p>✓ 保留${escapeHtml(item)}</p>`).join("") || "<p>无新增能力</p>"}</div>
</div>`);
} else if (action === "code") {
showModal("代码差异", `<pre class="code-block">${escapeHtml(artifact.code_diff || "暂无代码差异记录")}</pre>`);
} else if (action === "validation") {
showModal("验证记录", (artifact.validation_records || []).map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)} · ${escapeHtml(formatDate(test.finished_at))}</span></div></div>`).join("") || `<div class="empty-inline">暂无验证记录</div>`);
}
}
ui.start.addEventListener("click", startTask);
ui.instruction.addEventListener("input", () => updateCapabilityPreview());
ui.baseline.addEventListener("change", () => updateCapabilityPreview());
ui.cancel.addEventListener("click", () => taskAction("cancel"));
ui.retry.addEventListener("click", () => taskAction("retry"));
ui.scrollLogs.addEventListener("click", () => { ui.logs.scrollTop = ui.logs.scrollHeight; });
document.querySelectorAll("[data-prompt]").forEach((button) => button.addEventListener("click", () => appendPrompt(button.dataset.prompt)));
document.querySelectorAll("[data-inspector-tab]").forEach((button) => button.addEventListener("click", () => { model.inspectorTab = button.dataset.inspectorTab; renderInspector(selectedTask()); refreshIcons(); }));
ui.artifacts.addEventListener("click", (event) => {
const button = event.target.closest("[data-artifact-action]");
if (button) artifactAction(button);
});
ui.modalClose.addEventListener("click", closeModal);
ui.modalCloseFooter.addEventListener("click", closeModal);
ui.modal.addEventListener("click", (event) => { if (event.target === ui.modal) closeModal(); });
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !ui.modal.hidden) closeModal(); });
renderPipeline(null);
renderRelease(null);
updateCapabilityPreview();
refreshIcons();
refreshState();
})();
+6 -4
View File
@@ -33,19 +33,21 @@
document.documentElement.style.colorScheme = theme;
})();
</script>
<link rel="stylesheet" href="/firmware-workbench-entry.css?v=2" />
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
<script src="/network.js?v=android-pad-2" defer></script>
<script src="/app.js?v=dual-seat-145-n7-dispatch" defer></script>
<script src="/app.js?v=dual-seat-147-video-media" defer></script>
<script src="/speech-input.js?v=sensevoice-13" defer></script>
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
<script src="/pwa.js?v=android-pad-3" defer></script>
<script src="/console-v2.js?v=local-first-77-performance" defer></script>
<script src="/self-extension.js?v=pet-settings-53-performance" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-34" defer></script>
<script src="/console-v2.js?v=local-first-79-cinema-status" defer></script>
<script src="/self-extension.js?v=nfc-persistent-57" defer></script>
<script src="/assistant-chat.js?v=codex-assistant-39-video-media" defer></script>
<script src="/energy-monitor.js?v=energy-monitor-5-performance" defer></script>
<script src="/firmware-workbench-entry.js?v=3" defer></script>
</head>
<body class="console-v2">
<div id="app" aria-busy="true"></div>
+7 -7
View File
@@ -1493,7 +1493,7 @@ button.v2-extension-capability.preview {
/* Dialogs ---------------------------------------------------------------- */
.v2-extension-dialog {
width: min(650px, calc(100vw - 40px));
width: min(780px, calc(100vw - 32px));
max-height: calc(100dvh - 40px);
margin: auto;
padding: 0;
@@ -2307,7 +2307,7 @@ form > .v2-extension-primary {
/* GSCU1B TCP runtime controls ------------------------------------------- */
.v2-gscu-tcp-dialog {
width: min(720px, calc(100vw - 30px));
width: min(960px, calc(100vw - 30px));
}
.v2-gscu-status {
@@ -2934,9 +2934,9 @@ form > .v2-extension-primary {
}
.v2-nfc-dialog {
width: min(900px, calc(100vw - 28px));
max-width: 900px;
max-height: min(860px, calc(100dvh - 28px));
width: min(1180px, calc(100vw - 28px));
max-width: 1180px;
max-height: min(900px, calc(100dvh - 28px));
padding: 0;
overflow: auto;
color: #203d3d;
@@ -4596,8 +4596,8 @@ form > .v2-extension-primary {
}
.v2-pet-settings-dialog {
width: min(760px, calc(100vw - 32px));
height: min(760px, calc(100dvh - 32px));
width: min(980px, calc(100vw - 32px));
height: min(860px, calc(100dvh - 32px));
max-height: calc(100dvh - 32px);
overflow: hidden;
}
+240 -41
View File
@@ -373,6 +373,17 @@
card: null,
legacyProfiles: [],
};
let extensionToySnapshot = {
revision: -1,
state: "stopped",
topic: "extension/toy",
burst_published_count: 0,
last_error: null,
};
let peripheralCatalogRevision = null;
let peripheralCatalogSyncQueue = Promise.resolve();
let durablePeripheralCatalog = [];
let durablePeripheralCatalogHydrated = false;
let petCardSnapshot = {
revision: -1,
cards: [],
@@ -1964,10 +1975,40 @@
}).filter((item) => item.device_id && item.card_uid && validSlot(item.slot));
}
function applyDurablePeripheralCatalog(
catalog,
revision = null,
{ render = true } = {},
) {
const nextCatalog = (Array.isArray(catalog) ? catalog : [])
.filter((item) => item && typeof item === "object"
&& item.id && item.device_id && normalizeCardUid(item.card_uid)
&& validSlot(item.slot))
.map((item) => ({ ...item, card_uid: normalizeCardUid(item.card_uid) }));
const changed = JSON.stringify(durablePeripheralCatalog) !== JSON.stringify(nextCatalog)
|| !durablePeripheralCatalogHydrated;
durablePeripheralCatalog = nextCatalog;
durablePeripheralCatalogHydrated = true;
if (revision != null && String(revision).trim()) {
peripheralCatalogRevision = String(revision).trim();
}
if (changed) unifiedExtensionDirty = true;
if (render && changed) renderUnifiedExtensionView?.({ force: true });
return changed;
}
async function hydratePeripheralCatalog() {
const payload = await api("peripherals");
const catalog = Array.isArray(payload.catalog) ? payload.catalog : [];
if (!catalog.length) return 0;
applyDurablePeripheralCatalog(
catalog,
payload.catalog_revision,
{ render: false },
);
if (!catalog.length) {
renderAll();
return 0;
}
const restoredDeviceIds = new Set();
catalog.forEach((item) => {
if (!item || typeof item !== "object" || !item.id || !validSlot(item.slot)) return;
@@ -3754,6 +3795,12 @@
${deviceGroups.length ? `<div class="v2-nfc-existing"><span>继续关联红外设备 ID(可选)</span><div><select id="extensionNfcExistingDevice" data-nfc-device-select>${deviceGroups.map((group) => `<option value="${escapeHtml(group.deviceId)}">${escapeHtml(group.deviceId)} — ${escapeHtml(group.objectNames.join(" / ") || "无对象描述")} — ${group.commands.length} 条命令</option>`).join("")}</select><button class="v2-extension-primary" type="button" data-nfc-bind-existing>关联设备 ID</button></div><small>宠物消息与红外设备授权相互独立,不会覆盖宠物卡片内容。</small></div>` : ""}`;
} else {
const group = deviceGroups.find((item) => item.deviceId === linkedDevice.id);
const alternativeDeviceGroups = deviceGroups.filter(
(item) => item.deviceId !== linkedDevice.id
&& !normalizeCardUid(
state.devices.find((device) => device.id === item.deviceId)?.cardUid,
),
);
const capabilities = configuredCapabilities(linkedDevice.id);
const enabledCount = capabilities.filter((capability) => capability.enabled !== false).length;
const allEnabled = capabilities.length > 0 && enabledCount === capabilities.length;
@@ -3766,6 +3813,7 @@
${renderNfcPetBinding(linkedPet)}
<div class="v2-nfc-capability-heading"><div><strong>允许此 NFC 执行的命令</strong></div><span>${enabledCount}/${capabilities.length} 已选择</span></div>
${capabilities.length ? `<label class="v2-nfc-select-all"><input type="checkbox" data-nfc-select-all ${allEnabled ? "checked" : ""} /><span><strong>全选</strong><small>统一允许或禁止当前设备 ID 下的全部命令</small></span></label><div class="v2-nfc-command-list">${capabilities.map((capability) => `<label class="v2-nfc-command-option"><input type="checkbox" data-nfc-capability-enabled="${escapeHtml(capability.id)}" ${capability.enabled !== false ? "checked" : ""} /><span><strong>${escapeHtml(capability.name)}</strong><small>IR ${String(capability.slot).padStart(3, "0")} · ${escapeHtml((capability.aliases || []).join("、"))}</small></span></label>`).join("")}</div>` : `<div class="v2-nfc-no-devices"><i data-lucide="triangle-alert"></i><div><strong>该设备 ID 暂无有效命令</strong><span>槽位可能已删除或改绑到其他设备 ID,请重新检查红外设置。</span></div></div>`}
${alternativeDeviceGroups.length ? `<div class="v2-nfc-existing v2-nfc-rebind"><span>将当前 NFC 改绑到新设备 ID</span><div><select id="extensionNfcExistingDevice" data-nfc-device-select>${alternativeDeviceGroups.map((item) => `<option value="${escapeHtml(item.deviceId)}">${escapeHtml(item.deviceId)} — ${escapeHtml(item.objectNames.join(" / ") || "无对象描述")} — ${item.commands.length} 条命令</option>`).join("")}</select><button class="v2-extension-primary" type="button" data-nfc-bind-existing>改绑设备 ID</button></div><small>改绑成功后,旧设备 ID 的 NFC 授权会解除,新设备的全部有效命令会默认启用。</small></div>` : ""}
<div class="v2-nfc-recognized-actions"><span>复选框状态会持久化为自然语言执行白名单。</span><button type="button" data-nfc-unbind>解绑 NFC</button></div>`;
}
nfcDialogFingerprint = renderFingerprint;
@@ -3788,19 +3836,36 @@
document.body.append(dialog);
}
async function syncPeripheralCatalog(device = null) {
try {
await api("display-sync", {
async function syncPeripheralCatalog(device = null, { throwOnError = false } = {}) {
const nextCatalog = peripheralCatalog();
const operation = peripheralCatalogSyncQueue
.catch(() => null)
.then(() => api("display-sync", {
method: "POST",
body: JSON.stringify({ peripherals: peripheralCatalog(), device }),
});
body: JSON.stringify({
peripherals: nextCatalog,
device,
catalog_revision: peripheralCatalogRevision,
}),
}));
peripheralCatalogSyncQueue = operation.catch(() => null);
try {
const result = await operation;
applyDurablePeripheralCatalog(
nextCatalog,
result.catalog_revision,
);
return result;
} catch (error) {
console.warn("外设能力同步失败", error);
if (throwOnError) throw error;
return null;
}
}
function bindCardToDevice(device, cardUid) {
async function bindCardToDevice(device, cardUid) {
if (!device || !cardUid) return;
const previousBindings = state.devices.map((item) => [item.id, item.cardUid || null]);
const normalizedUid = normalizeCardUid(cardUid);
state.devices.forEach((item) => {
if (item.id !== device.id && normalizeCardUid(item.cardUid) === normalizedUid) {
@@ -3812,62 +3877,167 @@
state.selectedDeviceByType[device.type] = device.id;
saveState();
renderAll();
void syncPeripheralCatalog(device);
try {
await syncPeripheralCatalog(device, { throwOnError: true });
} catch (error) {
previousBindings.forEach(([id, cardUidValue]) => {
const item = state.devices.find((candidate) => candidate.id === id);
if (item) item.cardUid = cardUidValue;
});
saveState();
renderAll();
throw error;
}
}
function bindCurrentNfcToExisting() {
async function bindCurrentNfcToExisting() {
const deviceId = $("#extensionNfcExistingDevice")?.value;
reconcileDevicesAndCapabilitiesFromSlots();
const device = state.devices.find((item) => item.id === deviceId);
const cardUid = nfcSnapshot.card?.uid;
if (!device || !cardUid) return;
configuredCapabilities(device.id).forEach((capability) => {
if (!cardUid) {
showToast("当前没有可用于关联的 NFC 卡片。", "error");
return;
}
if (!device) {
showToast("未找到所选设备 ID,请刷新红外槽位后重试。", "error");
return;
}
const capabilities = configuredCapabilities(device.id);
if (!capabilities.length) {
showToast(`设备 ID“${device.id}”没有可授权的有效红外命令。`, "error");
return;
}
const previousDevice = linkedDeviceForCard(cardUid);
const previousEnabled = capabilities.map((capability) => [capability.id, capability.enabled]);
capabilities.forEach((capability) => {
capability.enabled = true;
});
bindCardToDevice(device, cardUid);
try {
await bindCardToDevice(device, cardUid);
} catch (error) {
previousEnabled.forEach(([id, enabled]) => {
const capability = capabilities.find((item) => item.id === id);
if (capability) capability.enabled = enabled;
});
saveState();
renderAll();
renderNfcDialog();
showToast(`NFC 设备授权保存失败:${error.message}`, "error");
return;
}
renderNfcDialog();
showToast(`NFC 已关联设备 ID“${device.id}”,全部命令已默认选中。`, "success");
showToast(
previousDevice && previousDevice.id !== device.id
? `NFC 已从设备 ID“${previousDevice.id}”改绑到“${device.id}”,新设备命令已全部启用。`
: `NFC 已关联设备 ID“${device.id}”,全部命令已默认选中。`,
"success",
);
}
function unbindCurrentNfc() {
async function unbindCurrentNfc() {
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
if (!device) return;
const previousUid = device.cardUid;
device.cardUid = null;
saveState();
renderAll();
void syncPeripheralCatalog(device);
try {
await syncPeripheralCatalog(device, { throwOnError: true });
} catch (error) {
device.cardUid = previousUid;
saveState();
renderAll();
renderNfcDialog();
showToast(`解除 NFC 绑定失败:${error.message}`, "error");
return;
}
renderNfcDialog();
showToast(`已解除设备 ID“${device.id}”的 NFC 绑定。`);
}
function updateNfcCapabilityAuthorization(capabilityId, enabled) {
async function updateNfcCapabilityAuthorization(capabilityId, enabled) {
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
const capability = configuredCapabilities(device?.id).find(
(item) => item.id === capabilityId,
);
if (!device || !capability) return;
const previousEnabled = capability.enabled;
capability.enabled = enabled;
saveState();
renderAll();
renderNfcDialog();
void syncPeripheralCatalog(device);
try {
await syncPeripheralCatalog(device, { throwOnError: true });
} catch (error) {
capability.enabled = previousEnabled;
saveState();
renderAll();
renderNfcDialog();
showToast(`NFC 命令授权保存失败:${error.message}`, "error");
}
}
function updateAllNfcCapabilityAuthorizations(enabled) {
async function updateAllNfcCapabilityAuthorizations(enabled) {
const device = linkedDeviceForCard(nfcSnapshot.card?.uid);
if (!device) return;
configuredCapabilities(device.id).forEach((capability) => {
const capabilities = configuredCapabilities(device.id);
const previousEnabled = capabilities.map((capability) => [capability.id, capability.enabled]);
capabilities.forEach((capability) => {
capability.enabled = enabled;
});
saveState();
renderAll();
renderNfcDialog();
void syncPeripheralCatalog(device);
try {
await syncPeripheralCatalog(device, { throwOnError: true });
} catch (error) {
previousEnabled.forEach(([id, previous]) => {
const capability = capabilities.find((item) => item.id === id);
if (capability) capability.enabled = previous;
});
saveState();
renderAll();
renderNfcDialog();
showToast(`NFC 命令授权保存失败:${error.message}`, "error");
}
}
function applyExtensionToySnapshot(value, { announce = true } = {}) {
if (!value || typeof value !== "object") return false;
const revision = Number(value.revision ?? -1);
if (revision === extensionToySnapshot.revision) return false;
extensionToySnapshot = {
revision,
state: String(value.state || "stopped"),
topic: String(value.topic || "extension/toy"),
burst_published_count: Number(value.burst_published_count || 0),
active_extension_names: Array.isArray(value.active_extension_names)
? value.active_extension_names.map((item) => String(item || "")).filter(Boolean)
: [],
last_delivery_status: value.last_delivery_status || null,
last_error: value.last_error || null,
};
if (announce && extensionToySnapshot.state === "burst_complete") {
showToast(
`已向 ${extensionToySnapshot.topic} 发送 ${extensionToySnapshot.burst_published_count} 帧设备能力消息。`,
"success",
);
} else if (announce && extensionToySnapshot.state === "burst_failed") {
showToast(
`${extensionToySnapshot.topic} 发送失败:${extensionToySnapshot.last_error || "MQTT 未连接"}`,
"error",
);
}
return true;
}
function applyNfcSnapshot(value, { announce = true } = {}) {
if (!value || typeof value !== "object") return;
if (value.pet_cards) applyPetCardSnapshot(value.pet_cards);
if (value.extension_toy) {
applyExtensionToySnapshot(value.extension_toy, { announce });
}
const revision = Number(value.revision ?? -1);
if (revision === nfcSnapshot.revision) return;
if (Array.isArray(value.legacy_profiles) && value.legacy_profiles.length) {
@@ -3893,8 +4063,8 @@
if (announce) {
showToast(
pet
? `已识别宠物“${pet.pet_name}”和设备 ID“${device.id}”。`
: `已识别设备 ID“${device.id}”。`,
? `已识别宠物“${pet.pet_name}”和设备 ID“${device.id}”,正在准备 extension/toy 消息。`
: `已识别设备 ID“${device.id}”,正在准备 extension/toy 消息。`,
"success",
);
}
@@ -3958,8 +4128,8 @@
return;
}
if (event.target.closest("[data-close-nfc-dialog]")) closeDialog("extensionNfcDialog");
if (event.target.closest("[data-nfc-bind-existing]")) bindCurrentNfcToExisting();
if (event.target.closest("[data-nfc-unbind]")) unbindCurrentNfc();
if (event.target.closest("[data-nfc-bind-existing]")) void bindCurrentNfcToExisting();
if (event.target.closest("[data-nfc-unbind]")) void unbindCurrentNfc();
if (event.target.closest("[data-nfc-bind-pet]")) void bindCurrentNfcToPet();
const unbindPet = event.target.closest("[data-nfc-unbind-pet]");
if (unbindPet) void unbindPetCard(unbindPet.dataset.nfcUnbindPet);
@@ -3969,14 +4139,14 @@
if (petSelect) selectedPetCardId = petSelect.value;
const capability = event.target.closest("[data-nfc-capability-enabled]");
if (capability) {
updateNfcCapabilityAuthorization(
void updateNfcCapabilityAuthorization(
capability.dataset.nfcCapabilityEnabled,
capability.checked,
);
return;
}
const selectAll = event.target.closest("[data-nfc-select-all]");
if (selectAll) updateAllNfcCapabilityAuthorizations(selectAll.checked);
if (selectAll) void updateAllNfcCapabilityAuthorizations(selectAll.checked);
});
$("#extensionContinueLearn").addEventListener("click", () => {
try {
@@ -4034,6 +4204,18 @@
status: item.status,
enabled: item.enabled,
})),
durableCatalogHydrated: durablePeripheralCatalogHydrated,
durableCatalog: durablePeripheralCatalog.map((item) => ({
id: item.id,
deviceId: item.device_id,
deviceName: item.device_name,
deviceType: item.device_type,
typeLabel: item.type_label,
action: item.action,
cardUid: item.card_uid,
status: item.status,
enabled: item.enabled,
})),
lastAction: snapshot?.extension?.last_action ? {
command: snapshot.extension.last_action.command,
type: snapshot.extension.last_action.type,
@@ -4114,27 +4296,28 @@
const sharedCatalog = Array.isArray(snapshot?.extension?.catalog)
? snapshot.extension.catalog
: [];
const mergedCatalog = [...sharedCatalog, ...peripheralCatalog()]
const fallbackCatalog = [...sharedCatalog, ...peripheralCatalog()]
.filter((item, index, catalog) => {
const identity = item.id || `${item.device_id}:${item.action}`;
return catalog.findIndex((candidate) => (
candidate.id || `${candidate.device_id}:${candidate.action}`
) === identity) === index;
});
const persistentCatalog = (
durablePeripheralCatalogHydrated
? durablePeripheralCatalog
: fallbackCatalog
).filter((item) => (
normalizeCardUid(item.card_uid)
&& item.status === "learned"
));
const nfcReader = nfcSnapshot.reader || {};
const nfcCard = nfcSnapshot.card;
const sensedCardUid = normalizeCardUid(nfcCard?.uid);
const nfcDevice = linkedDeviceForCard(sensedCardUid);
const nfcPet = linkedPetForCard(sensedCardUid);
const activeCatalog = sensedCardUid
? mergedCatalog.filter((item) => (
normalizeCardUid(item.card_uid) === sensedCardUid
&& item.status === "learned"
&& item.enabled !== false
))
: [];
const grouped = new Map();
activeCatalog.forEach((item) => {
persistentCatalog.forEach((item) => {
const deviceId = item.device_id || "external-device";
if (!grouped.has(deviceId)) grouped.set(deviceId, {
id: deviceId,
@@ -4143,7 +4326,10 @@
cardUid: item.card_uid || null,
actions: [],
});
grouped.get(deviceId).actions.push(item.action || item.name || "已学习能力");
const actionName = item.action || item.name || "已学习能力";
grouped.get(deviceId).actions.push(
item.enabled === false ? `${actionName}(已停用)` : actionName,
);
});
const devices = [...grouped.values()];
const lastAction = snapshot?.extension?.last_action;
@@ -4159,8 +4345,10 @@
: petCardSnapshot.publisher?.state === "burst_failed"
? "宠物消息发送失败"
: "已感应宠物 NFC"
: devices.length
: nfcDevice
? "已感应 NFC 外设"
: devices.length
? `${devices.length} 个外设已绑定`
: sensedCardUid
? "当前 NFC 暂无已授权命令"
: nfcReader.online
@@ -4226,11 +4414,11 @@
<div>${device.actions.slice(0, 8).map((action) => `<span>${escapeHtml(action)}</span>`).join("")}</div>
</article>`).join("") : nfcPet ? "" : `
<div class="v2-extension-empty">
<i data-lucide="${sensedCardUid ? "badge-alert" : "scan-line"}"></i><div><strong>${sensedCardUid ? "当前 NFC 暂无已授权命令" : "当前未感应到外设"}</strong><span>${sensedCardUid ? "请检查设备 ID 绑定及命令复选框" : "将已绑定设备 ID 的 NFC 卡靠近读卡器后显示命令"}</span></div>
<i data-lucide="link-2-off"></i><div><strong>当前没有已绑定外设</strong><span>外设绑定 NFC 后将持续显示,解除绑定后移除</span></div>
</div>`}
</div>
<footer class="v2-extension-linkage" data-active="${String(Boolean(visibleLastAction))}">
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "等待 NFC 外设感应")}</strong></div>
<i data-lucide="workflow"></i><div><small>最近联动</small><strong>${escapeHtml(visibleLastAction?.command || visibleLastAction?.type || "暂无外设联动")}</strong></div>
<span>${escapeHtml(visibleLastAction ? (lastResult?.status || lastResult?.message || "STANDBY") : "STANDBY")}</span>
</footer>`;
unifiedExtensionFingerprint = renderFingerprint;
@@ -4242,6 +4430,17 @@
window.addEventListener("cognitive-agent:display-sync", (event) => {
snapshot = event.detail || null;
const extensionSnapshot = snapshot?.extension;
if (
Array.isArray(extensionSnapshot?.catalog)
&& extensionSnapshot?.result?.catalog_revision
) {
applyDurablePeripheralCatalog(
extensionSnapshot.catalog,
extensionSnapshot.result.catalog_revision,
{ render: false },
);
}
if (snapshot?.extension_health?.nfc) {
applyNfcSnapshot(snapshot.extension_health.nfc, { announce: false });
}
@@ -4267,8 +4466,8 @@
void hydratePeripheralCatalog()
.catch((error) => {
console.warn("恢复后端红外能力目录失败,将保留浏览器本地目录。", error);
})
.finally(() => syncPeripheralCatalog());
return syncPeripheralCatalog();
});
return true;
}
+16 -10
View File
@@ -1,28 +1,34 @@
const CACHE_VERSION = "cognitive-agent-pad-v321-n7-dispatch";
const CACHE_VERSION = "cognitive-agent-pad-v334-video-media";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
const APP_SHELL = [
"/",
"/index.html",
"/styles.css?v=dual-seat-23",
"/styles.css?v=dual-seat-24-dialog-size",
"/network.js?v=android-pad-2",
"/app.js?v=dual-seat-145-n7-dispatch",
"/app.js?v=dual-seat-147-video-media",
"/gesture-control.js?v=android-pad-9",
"/pwa.js?v=android-pad-3",
"/console-v2.css?v=delivery-state-69",
"/console-v2.js?v=local-first-77-performance",
"/self-extension.css?v=pet-settings-52",
"/self-extension.js?v=pet-settings-53-performance",
"/console-v2.js?v=local-first-79-cinema-status",
"/self-extension.css?v=pet-settings-54-dialog-size",
"/self-extension.js?v=nfc-persistent-57",
"/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-34",
"/energy-monitor.css?v=energy-monitor-4",
"/assistant-chat.css?v=codex-assistant-30",
"/assistant-chat.js?v=codex-assistant-39-video-media",
"/energy-monitor.css?v=energy-monitor-5-dialog-size",
"/energy-monitor.js?v=energy-monitor-5-performance",
"/ui-consistency.css?v=spatial-ui-8",
"/codepen-glass-theme.css?v=creative-cloud-6-performance",
"/vue-ui.js?v=vue-presentation-3",
"/vue-ui.js?v=vue-presentation-4",
"/firmware-workbench-entry.css?v=2",
"/firmware-workbench-entry.js?v=3",
"/firmware-workbench.html",
"/firmware-workbench-theme.js?v=1",
"/firmware-workbench.css?v=7",
"/firmware-workbench.js?v=4",
"/assets/pets/self-extension-cat.png?v=3",
"/assets/pets/self-extension-cat-entrance.png?v=3",
"/manifest.webmanifest",
+4 -3
View File
@@ -395,8 +395,9 @@ body.mode-editor-open {
}
.mode-gallery {
width: min(1080px, calc(100vw - 32px));
max-height: min(680px, calc(100dvh - 32px));
width: min(1320px, calc(100vw - 32px));
height: min(860px, calc(100dvh - 32px));
max-height: min(860px, calc(100dvh - 32px));
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto;
gap: 12px;
@@ -863,7 +864,7 @@ body.mode-editor-open {
}
.mode-editor {
width: min(760px, calc(100vw - 28px));
width: min(1040px, calc(100vw - 28px));
height: min(900px, calc(100dvh - 28px));
max-height: calc(100dvh - 28px);
display: grid;
+85 -2
View File
@@ -1,5 +1,13 @@
import { cpSync, copyFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { createHash } from "node:crypto";
import {
cpSync,
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync,
} from "node:fs";
import { basename, dirname, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import vue from "@vitejs/plugin-vue";
@@ -29,16 +37,90 @@ const runtimeFiles = [
"speech-input.js",
"assistant-chat.js",
"energy-monitor.js",
"firmware-workbench-entry.js",
"firmware-workbench-theme.js",
"firmware-workbench.js",
"firmware-workbench.html",
"styles.css",
"console-v2.css",
"self-extension.css",
"speech-input.css",
"assistant-chat.css",
"energy-monitor.css",
"firmware-workbench-entry.css",
"firmware-workbench.css",
"ui-consistency.css",
"codepen-glass-theme.css",
"vue-ui.js"
];
const fingerprintedRuntimeFiles = runtimeFiles.filter((file) => (
/\.(?:css|js)$/.test(file)
&& !["service-worker.js", "speech-audio-worklet.js"].includes(file)
));
const rewrittenHtmlFiles = [
"index.html",
"login.html",
"hardware-control.html",
"firmware-workbench.html",
];
function contentHash(value) {
return createHash("sha256").update(value).digest("hex").slice(0, 12);
}
function hashedRuntimeName(file, hash) {
const extension = extname(file);
return `${basename(file, extension)}.${hash}${extension}`;
}
function rewriteRuntimeReferences(value, manifest) {
let rewritten = value;
for (const [source, target] of Object.entries(manifest)) {
const escaped = source.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
rewritten = rewritten.replace(
new RegExp(`/${escaped}(?:\\?[^\\s\"'<>)]*)?`, "g"),
`/${target}`,
);
}
return rewritten;
}
function fingerprintRuntimeAssets() {
const manifest = {};
for (const file of fingerprintedRuntimeFiles) {
const source = resolve(outputRoot, file);
if (!existsSync(source)) continue;
const contents = readFileSync(source);
const hashedName = hashedRuntimeName(file, contentHash(contents));
writeFileSync(resolve(outputRoot, hashedName), contents);
manifest[file] = hashedName;
}
for (const file of rewrittenHtmlFiles) {
const target = resolve(outputRoot, file);
if (!existsSync(target)) continue;
const source = readFileSync(target, "utf8");
writeFileSync(target, rewriteRuntimeReferences(source, manifest), "utf8");
}
const serviceWorkerPath = resolve(outputRoot, "service-worker.js");
const buildHash = contentHash(JSON.stringify(manifest));
if (existsSync(serviceWorkerPath)) {
const serviceWorker = rewriteRuntimeReferences(
readFileSync(serviceWorkerPath, "utf8"),
manifest,
).replace(
/const CACHE_VERSION = "[^"]+";/,
`const CACHE_VERSION = "cognitive-agent-pad-build-${buildHash}";`,
);
writeFileSync(serviceWorkerPath, serviceWorker, "utf8");
}
writeFileSync(
resolve(outputRoot, "runtime-assets.json"),
`${JSON.stringify({ build_hash: buildHash, assets: manifest }, null, 2)}\n`,
"utf8",
);
}
function copyRuntimeAssets() {
return {
@@ -58,6 +140,7 @@ function copyRuntimeAssets() {
mkdirSync(dirname(target), { recursive: true });
copyFileSync(source, target);
}
fingerprintRuntimeAssets();
}
};
}
+4 -3
View File
@@ -111,7 +111,7 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
}
function createCommandMount(container) {
const state = reactive({ commands: [], open: true });
const state = reactive({ commands: [], open: true, heading: "下发控制指令" });
const app = createApp({
name: "N7DispatchedControlsCard",
setup() {
@@ -131,7 +131,7 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
h("i", { "data-lucide": "list-tree" }),
]),
h("b", null, "下发控制指令"),
h("b", null, state.heading),
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
h("i", {
class: "assistant-dispatched-controls-chevron",
@@ -148,12 +148,13 @@ import { Fragment, createApp, h, nextTick, reactive } from "vue";
return mount;
}
function renderDispatchedControls(container, commands) {
function renderDispatchedControls(container, commands, options = {}) {
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
return false;
}
const mount = commandMounts.get(container) || createCommandMount(container);
replaceReactiveArray(mount.state.commands, commands);
mount.state.heading = text(options.heading, "下发控制指令");
container.dataset.vueSurface = "dispatched-controls";
refreshIcons();
return true;
@@ -2,12 +2,13 @@ id: leisure_lounger
name: 休闲躺椅模式
aliases:
- 休闲躺椅
description: 腾势 N7 副驾休闲躺椅姿态,并联动当前已接入的空调、天幕和氛围灯能力。
description: 腾势 N7 主驾放松按摩与副驾休闲躺椅姿态,并联动当前已接入的空调、天幕和氛围灯能力;驻车使用,不改变主驾驾驶姿态。
icon: sofa
scope:
vehicle_ids:
- denza_n7
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用已接入的主驾放松按摩,让主副驾按各自能力联动休闲;主驾没有绝对位置反馈,不自动移动其座椅。}
- domain: seat
capability: seat.set_longitudinal_position
target_scope: passenger
@@ -3,12 +3,13 @@ name: 冥想空间模式
aliases:
- 冥想空间
- 冥想
description: 腾势 N7 冥想空间姿态,并联动当前已接入的恒温、低风、内循环、遮阳帘和冥想氛围灯能力。
description: 腾势 N7 主驾放松按摩与副驾冥想空间姿态,并联动当前已接入的恒温、低风、内循环、遮阳帘和冥想氛围灯能力;驻车使用,不改变主驾驾驶姿态。
icon: leaf
scope:
vehicle_ids:
- denza_n7
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用当前工程已接入的主驾放松按摩辅助驻车冥想;不假定副驾具有按摩能力,不移动主驾座椅。}
- domain: seat
capability: seat.set_longitudinal_position
target_scope: passenger
+2 -1
View File
@@ -2,12 +2,13 @@ id: nap
name: 小憩模式
aliases:
- 小憩
description: 腾势 N7 副驾小憩姿态,并联动当前已接入的静音恒温、遮阳帘和氛围灯关闭能力。
description: 腾势 N7 主驾低档座椅通风与副驾小憩姿态,并联动当前已接入的静音恒温、遮阳帘和氛围灯关闭能力;驻车使用,不改变主驾驾驶姿态。
icon: moon
scope:
vehicle_ids:
- denza_n7
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 主驾使用低档座椅通风保持驻车静坐舒适,不自动移动主驾或开启按摩。}
- domain: seat
capability: seat.set_longitudinal_position
target_scope: passenger
+3 -1
View File
@@ -2,12 +2,14 @@ id: office
name: 办公模式
aliases:
- 办公
description: 腾势 N7 副驾办公姿态,并联动当前已接入的恒温、中低风、内循环、遮阳帘和冷白氛围灯能力。
description: 腾势 N7 主副驾低档座椅通风与副驾办公姿态,并联动当前已接入的恒温、中低风、内循环、遮阳帘和冷白氛围灯能力;不改变主驾驾驶姿态。
icon: briefcase
scope:
vehicle_ids:
- denza_n7
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾原生 CAN 低档通风,改善驻车办公时久坐闷热;不移动主驾座椅。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾独立接口低档通风,与办公支撑姿态联动。}
- domain: seat
capability: seat.set_longitudinal_position
target_scope: passenger
@@ -0,0 +1,26 @@
id: boarding-warmup
name: 上车预热模式
aliases: [上车预热的功能模式, 上车预热功能模式, 上车暖舱模式, 分阶段预热模式, 动态预热模式, 空调预热模式]
description: 分阶段上车预热;前2分钟以28℃和4档风量快速暖舱,再用24℃和1档风量舒适保持5分钟,并分别编排主副驾加热。总计7分钟,不移动座椅位置;时间轴结束后保持最后设置,不承诺无人值守预约启动。
icon: thermometer
schedule:
schema: cognitive-agent-mode-schedule/v1
repeat_count: 1
phases:
- id: warmup
name: 快速暖舱
duration_ms: 120000
operations:
- {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: 28}, reason: 初始预热目标。}
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 4}, reason: 初始阶段较高风量。}
- {domain: seat, capability: seat.set_driver_heating_native, target_scope: driver, operation: set, parameters: {state: 'on', level: 2}, reason: 主驾低档加热接口2为低档。}
- {domain: seat, capability: seat.set_heating, target_scope: passenger, operation: set, parameters: {state: 'on', level: 2}, reason: 副驾使用独立接口2档加热。}
- id: comfort
name: 舒适恒温
duration_ms: 300000
operations:
- {domain: climate, capability: climate.set_temperature, target_scope: vehicle, operation: set, parameters: {temperature_c: 24}, reason: 预热结束后降低目标温度。}
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 降低风噪。}
- {domain: seat, capability: seat.set_driver_heating_native, target_scope: driver, operation: set, parameters: {state: 'on', level: 2}, reason: 主驾维持低档。}
- {domain: seat, capability: seat.set_heating, target_scope: passenger, operation: set, parameters: {state: 'on', level: 1}, reason: 副驾降低为1档。}
+5 -1
View File
@@ -1,9 +1,13 @@
id: camping
name: 露营模式
aliases: [户外露营模式, 营地模式]
description: 使用低风外循环、打开遮阳帘并启用露营氛围;不自动开窗。
description: 为驻车露营组合主副驾低档通风、副驾靠背和腿托支撑、低风外循环、开阔天幕与露营氛围;不移动主驾座椅,不自动开窗。
icon: tent-tree
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾原生 CAN 的低档通风,减少驻车久坐闷热。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾独立接口的低档通风。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 45}, reason: 为驻车露营提供适度舒展的副驾靠背支撑。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 35}, 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: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 使用安静低风量。}
+5 -1
View File
@@ -1,9 +1,13 @@
id: cinema
name: 观影模式
aliases: [影院模式, 家庭影院模式, 电影模式, 看电影模式]
description: 关闭直射光和车内主照明,使用低风恒温与影院氛围灯;不包含屏幕或音响控制。
description: 为驻车观影组合主驾放松按摩、副驾舒展靠背和腿托、低档通风、遮光与低风恒温;不改变主驾驾驶姿态,不包含屏幕或音响控制。
icon: film
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用主驾已接入的放松按摩,缓解驻车观影时的久坐不适;不移动主驾座椅。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾低档通风减少久坐闷热。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 55}, reason: 为驻车观影提供适度后仰的副驾靠背支撑。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 45}, 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: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 使用低风量降低噪声。}
+19
View File
@@ -0,0 +1,19 @@
id: lay_flat
name: 躺平模式
aliases:
- 躺平休息模式
- 平躺休息模式
description: 驻车休息预览:默认仅联动副驾靠背与腿托,配合恒温、柔和氛围光、关闭阅读灯与四扇车窗;指定主驾时仅使用其已接入的按摩能力,不调整驾驶姿态。所有动作需确认后保存或试运行。
icon: armchair
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, 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: 关闭右后车窗,降低外部噪声。}
@@ -1,9 +1,11 @@
id: long-trip-comfort
name: 长途舒适模式
aliases: [长途乘车模式, 高速舒适模式]
description: 为长途乘坐提供副驾恒温、柔和风量、外循环与适度遮光。
description: 为长途乘坐提供主副驾低档座椅通风、副驾恒温、柔和风量、外循环与适度遮光,不改变座椅驾驶姿态。
icon: route
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾原生 CAN 的低档通风;该接口 2 为低档。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾独立接口的低档通风;该接口 1 为低档。}
- {domain: climate, capability: climate.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启空调。}
- {domain: climate, capability: climate.set_temperature, target_scope: passenger, operation: set, parameters: {temperature_c: 24}, reason: 保持副驾舒适温度。}
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 2}, reason: 使用中低风量。}
+3 -1
View File
@@ -3,9 +3,11 @@ name: 夜间静享模式
aliases:
- 夜间模式
- 静夜模式
description: 使用低亮冷色氛围、关闭主照明与阅读灯,并以低风恒温保持安静。
description: 使用主副驾低档座椅通风、低亮冷色氛围、关闭主照明与阅读灯,并以低风恒温保持安静;不改变座椅姿态。
icon: moon-star
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾低档通风,降低夜间久坐闷热且不启动姿态调整或按摩。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 副驾使用低档通风,保持座椅当前姿态。}
- {domain: ambient_light, capability: ambient_light.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启低亮氛围灯。}
- {domain: ambient_light, capability: ambient_light.set_illumination, target_scope: vehicle, operation: set, parameters: {color_hex: '#102040', brightness_pct: 10}, reason: 使用低亮深蓝氛围。}
- {domain: interior_light, capability: interior_light.set_indoor_power, target_scope: vehicle, operation: set, parameters: {state: "off"}, reason: 关闭室内主照明。}
+5 -1
View File
@@ -4,9 +4,13 @@ aliases:
- 阅读空间模式
- 看书模式
- 读书模式
description: 为座舱阅读提供暖色照明、低风恒温和适度遮光。
description: 为驻车阅读提供主副驾低档座椅通风、副驾靠背与腿托支撑、暖色照明、低风恒温和适度遮光;不改变主驾驾驶姿态。
icon: book-open
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 主驾采用已接入原生 CAN 的低档通风,减少久坐闷热;不移动主驾座椅。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 副驾使用独立接口的低档通风。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 22}, reason: 使用偏直立的副驾阅读靠背支撑,避免大幅躺倒。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 21}, 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: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 使用安静低风量。}
+3 -1
View File
@@ -1,9 +1,11 @@
id: reception
name: 接待模式
aliases: [贵宾模式, 迎宾接待模式]
description: 使用柔和暖光、车内照明、恒温和半开遮阳帘营造接待环境。
description: 使用主副驾低档座椅通风、柔和暖光、车内照明、恒温和半开遮阳帘营造接待环境;不自动移动有人入座的座椅。
icon: handshake
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾低档通风营造舒适接待环境,保持当前座椅位置。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 副驾使用低档通风,不在迎宾入座时自动调整姿态。}
- {domain: ambient_light, capability: ambient_light.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启氛围灯。}
- {domain: ambient_light, capability: ambient_light.set_illumination, target_scope: vehicle, operation: set, parameters: {color_hex: '#FFE2B8', brightness_pct: 50}, reason: 使用柔和暖色氛围光。}
- {domain: interior_light, capability: interior_light.set_indoor_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启室内主照明。}
+3 -2
View File
@@ -1,9 +1,10 @@
id: relaxation
name: 放松模式
aliases: [休闲模式, 舒缓模式, 轻松模式, 放空模式]
description: 使用舒展的副驾姿态、安静恒温、适度遮光和低亮暖光,营造适合放松休息的座舱环境。
aliases: [休闲模式, 舒缓模式, 轻松模式, 放空模式, 坐着放松模式, 乘坐放松模式, 座椅放松模式, 座位放松模式]
description: 使用主驾放松按摩、舒展的副驾姿态、安静恒温、适度遮光和低亮暖光,营造适合驻车放松休息的座舱环境;不改变主驾驾驶姿态。
icon: sofa
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用已验证的主驾放松按摩模式;主驾没有绝对位置反馈,不自动移动主驾座椅。}
- {domain: seat, capability: seat.set_longitudinal_position, target_scope: passenger, operation: set, parameters: {position_pct: 51}, reason: 将副驾移动到兼顾腿部空间与安全的休闲位置。}
- {domain: seat, capability: seat.set_cushion_tilt, target_scope: passenger, operation: set, parameters: {position_pct: 30}, reason: 调整坐盆角度以提供稳定支撑。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 66}, reason: 将副驾靠背调整到舒展的休闲角度。}
+3 -1
View File
@@ -1,9 +1,11 @@
id: romantic
name: 浪漫模式
aliases: [约会模式, 浪漫氛围模式]
description: 使用浪漫氛围灯并关闭车内主照明与阅读灯,同时关闭遮阳帘。
description: 使用主副驾低档座椅通风与浪漫氛围灯,关闭车内主照明、阅读灯和遮阳帘;保持当前座椅姿态。
icon: heart
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾低档通风减少久坐闷热,不调整驾驶姿态。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 使用副驾低档通风,保持用户当前的座椅姿态。}
- {domain: ambient_light, capability: ambient_light.set_power, target_scope: vehicle, operation: set, parameters: {state: "on"}, reason: 开启氛围灯。}
- {domain: ambient_light, capability: ambient_light.set_mode, target_scope: vehicle, operation: set, parameters: {mode: romantic}, reason: 使用工程已接入的浪漫氛围。}
- {domain: interior_light, capability: interior_light.set_indoor_power, target_scope: vehicle, operation: set, parameters: {state: "off"}, reason: 关闭室内主照明。}
+14
View File
@@ -0,0 +1,14 @@
id: seat-comfort
name: 座椅舒适模式
aliases: [座位舒适模式, 乘坐舒适模式, 坐着舒服模式, 坐着舒适模式, 座椅舒服模式, 座位舒服模式]
description: 为主副驾组合低档通风、主驾放松按摩和副驾适度靠背及腿托支撑;按两座实际能力分别编排,主驾没有绝对位置反馈,不自动移动主驾座椅。适合驻车调整并预览后使用。
icon: armchair
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 主驾通风使用独立 CAN 接口的低档 2,减少背部闷热。}
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 使用主驾已验证的放松按摩模式,不假定副驾具有按摩能力。}
- {domain: seat, capability: seat.set_ventilation, target_scope: passenger, operation: set, parameters: {state: "on", level: 1}, reason: 副驾通风使用独立接口的低档 1。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 45}, reason: 副驾靠背使用可反馈的适度舒展目标。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 35}, 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: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 1}, reason: 降低风噪和直吹打扰。}
+23
View File
@@ -0,0 +1,23 @@
id: seat-dynamic
name: 座椅动态舒缓模式
aliases: [座椅动态模式, 座椅的动态模式, 动态座椅模式, 座椅循环模式, 座椅交替放松模式, 动态放松模式]
description: 驻车使用的有界双座舒缓时间轴;主驾在放松和舒缓按摩间切换,副驾靠背与腿托小幅交替,30秒一个阶段、重复5次,总计5分钟。不自动移动主驾位置;停止后不再进入后续阶段。
icon: activity
schedule:
schema: cognitive-agent-mode-schedule/v1
repeat_count: 5
phases:
- id: relax
name: 舒展支撑
duration_ms: 30000
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: relax}, reason: 主驾使用原生放松按摩,不改变驾驶姿态。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 45}, reason: 驻车时副驾适度舒展。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 25}, reason: 适度支撑腿部。}
- id: support
name: 轻柔回落
duration_ms: 30000
operations:
- {domain: seat, capability: seat.set_driver_massage_native, target_scope: driver, operation: set, parameters: {action: mode, mode: slack}, reason: 主驾切换舒缓按摩。}
- {domain: seat, capability: seat.set_backrest_recline, target_scope: passenger, operation: set, parameters: {position_pct: 40}, reason: 副驾小幅回到支撑姿态。}
- {domain: seat, capability: seat.set_legrest_height, target_scope: passenger, operation: set, parameters: {position_pct: 20}, reason: 腿托小幅回落,不使用无界轴运动。}
+2 -1
View File
@@ -1,9 +1,10 @@
id: summer-cooling
name: 夏季清凉模式
aliases: [清凉模式, 酷夏模式]
description: 使用内循环、适中冷风、关闭遮阳帘并开启当前已接入的副驾座椅通风。
description: 使用内循环、适中冷风、关闭遮阳帘并分别开启主驾低档和副驾中档座椅通风;不改变座椅姿态。
icon: snowflake
operations:
- {domain: seat, capability: seat.set_driver_ventilation_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用主驾原生 CAN 的低档通风;该接口 2 为低档,不与副驾档位混用。}
- {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: 23}, reason: 设置清凉但不过低的温度。}
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 3}, reason: 使用适中风量快速降温。}
+2 -1
View File
@@ -1,9 +1,10 @@
id: winter-warmth
name: 冬季温暖模式
aliases: [暖冬模式, 冬日模式]
description: 使用内循环、暖风、关闭遮阳帘并开启当前已接入的副驾座椅加热。
description: 使用内循环、暖风、关闭遮阳帘并分别开启主驾低档和副驾中档座椅加热;不改变座椅姿态。
icon: flame
operations:
- {domain: seat, capability: seat.set_driver_heating_native, target_scope: driver, operation: set, parameters: {state: "on", level: 2}, reason: 使用已接入的主驾原生 CAN 低档加热;该接口 2 为低档,不与副驾档位混用。}
- {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: 26}, reason: 设置温暖舒适温度。}
- {domain: climate, capability: climate.set_fan_level, target_scope: vehicle, operation: set, parameters: {level: 2}, reason: 使用中低风量。}
+46
View File
@@ -0,0 +1,46 @@
# 功能模式的座椅联动与时间轴
## 行为
新建或重新构建功能模式时,未指定座位默认只编排副驾座椅;明确“主驾”时只编排主驾座椅;只有明确要求主驾和副驾时才组合双座。该规则同时覆盖已登记模板、工程预设复制、未登记模型组合,以及时间轴每一个阶段。其他设备的独立区域不改变座椅选择,例如“主驾空调”并不代表选择主驾座椅。
模板保留主副驾能力作为可选来源。构建时服务端先按所选座位投影,再逐动作校验,不能把副驾靠背、腿托的位置能力重标为主驾。模型规划的能力目录也按所选座位收窄,越界方案整体拒绝。已经保存的用户模式及其后续参数编辑仍以原有配置为准,不因本次默认规则变更静默删除座椅动作。
常用阅读、观影、露营、夜间静享、接待、浪漫、冬季温暖、夏季清凉模板,以及办公、小憩、冥想、休闲躺椅预设,包含本工程支持的主驾和副驾舒适动作。主驾优先使用加热、通风、按摩,不凭空增加没有协议支持的位置动作。除明确座椅场景外,不随意改变主驾坐姿。
已有用户保存的配置优先,不被模板升级静默覆盖。要采用新版组合,可删除不再需要的旧自定义副本后重新构建,或明确修改已有模式。除霜、新风等专项模式不强行增加无关的座椅动作。
“构建一个座椅的动态模式”和“构建一个上车预热的功能模式”命中本地模板,无需模型推理。其他带时间变化的需求由结构化场景编排产生分阶段计划,每个阶段再次经过能力、参数、语义、安全和 CAN 编译校验;任意阶段失败则整份草稿拒绝,不生成静态替代品。
## 模板默认值
- 座椅动态舒缓:30 秒舒缓、30 秒支撑,重复 5 次,总计 5 分钟。默认副驾靠背与腿托小幅改变;明确主驾时改用主驾支持的按摩切换,不移动主驾姿态。仅应在车辆安全停稳后使用。
- 上车预热:升温阶段 2 分钟,空调 28℃、风量 4;舒适保持阶段 5 分钟,空调 24℃、风量 1,同时编排所选座位的加热(默认副驾)。总计 7 分钟。
预览卡片展示每阶段的目标与时长,默认值属于可审核的建议。生成和试运行都不下发实际车辆控制;保存后进入模式才开始计时。“上车预热”目前是模式名称,不表示已接入自动开门触发。
## 存储与执行
`cognitive-agent-mode-schedule/v1` 保存 `repeat_count` 与有序 `phases`。每阶段有唯一 ID、名称、`duration_ms` 和绝对目标 `operations`。同一能力在不同阶段的不同值不能合并或去重。
约束:2–8 阶段;每阶段 1–12 项动作,1 秒–15 分钟;1–20 轮,含重复的总时长不超过 1 小时。禁止无限循环、相对累加动作和未经校验的底层指令。
真实执行由服务端计时。每阶段重新编译并检查权限/硬件租约,任意阶段失败即停止后续阶段。同车只运行一条时间轴,新时间轴替换旧时间轴;同车阶段回调互斥。不同车辆独立。
输入“停止动态模式”可取消后续阶段。已下发的目标不自动回滚,当前已进入执行器的操作可能继续完成。模拟停止不会改变真实任务。服务关闭时取消时间轴,重启不会自动续跑;网页关闭不会隐式修改服务端已启动任务。
`GET /api/modes/schedule/status?vehicle_id=denza_n7` 只读返回运行 ID、会话、阶段和终态,不暴露底层指令或 CAN 数据。助手区分已启动、模拟完成、真实完成、失败和取消,不把刚启动显示为全部完成。
## 验证
核心时钟/并发/取消:`tests/unit/test_mode_schedule.py`。
真实能力链创建、试运行、保存、重载、执行与防篡改:`tests/integration/test_mode_schedule_flow.py`。
座椅模板覆盖:`tests/integration/test_registered_scene_seat_coverage.py`。
新建座位选择、模型目录约束、非座椅分区隔离、保存绑定及越界拒绝:`tests/integration/test_mode_creation_seat_policy.py`。词汇与路由:`tests/unit/test_mode_creation_seat_policy.py`、`tests/unit/test_mode_creation_seat_routing.py`。
时间轴卡片与状态轮询:`tests/unit/mode_schedule_frontend.test.cjs`;隔离服务的真实浏览器:`tests/e2e/test_mode_schedule_browser.py`。
自动化测试使用模拟执行、临时用户目录并阻断硬件访问,不代表完成实车 CAN 验收。
@@ -305,8 +305,20 @@ class SeatServiceIntegrationTests(unittest.TestCase):
)
interpret.assert_called_once_with(
"播放音乐", context, self.service.capability_context
"播放音乐",
{
**context,
"natural_language_routing": {
"original_utterance": "空调温度设置为25度,播放音乐",
"parsed_clauses": ["空调温度设置为25度"],
"resolved_clauses": ["空调温度设置为25度"],
"unresolved_clauses": ["播放音乐"],
"cloud_input_utterance": "播放音乐",
},
},
self.service.capability_context,
)
self.assertNotIn("natural_language_routing", context)
self.assertEqual(
provider, f"local_rules+openai:{self.service.openai.model}"
)
@@ -0,0 +1,79 @@
/***************************************************************************//**
* \file main_cm0plus.c
*
* CYT4BFBCJE rev_d CM0+ entry point for the passenger-seat timed-only ECU.
*******************************************************************************/
#include "cy_project.h"
#include "cy_device_headers.h"
#include "bsp_seat_backrest_heater.h"
#include "bsp_seat_fan.h"
#include "motor_control.h"
#include "safety_manager.h"
#include "seat_move_config.h"
#include "seat_timed_application.h"
volatile uint32_t g_seatTimedResetReason = 0u;
volatile uint32_t g_seatTimedInitResult = MOTOR_API_FAILURE;
static void SeatTimed_HoldSafeOff(void)
{
for(;;)
{
Motor_ForceSafeOff();
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
SafetyManager_KickWatchdog();
Cy_SysTick_DelayInUs(SEAT_MOVE_TASK_PERIOD_US);
}
}
int main(void)
{
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
SystemInit();
Motor_ForceSafeOff();
if(!BspSeatBackrestHeater_ForceOff() || !BspSeatFan_ForceOff())
{
SeatTimed_HoldSafeOff();
}
g_seatTimedResetReason = Cy_SysReset_GetResetReason();
Cy_SysReset_ClearAllResetReasons();
SafetyManager_Init(g_seatTimedResetReason);
if(SafetyManager_IsInhibited())
{
SeatTimed_HoldSafeOff();
}
__enable_irq();
g_seatTimedInitResult = Motor_Init();
if(g_seatTimedInitResult != MOTOR_API_SUCCESS)
{
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
SeatTimed_HoldSafeOff();
}
Motor_SetWatchdogServiceEnabled(true);
if(!SeatTimedApplication_Init())
{
SeatTimed_HoldSafeOff();
}
for(;;)
{
SeatTimedApplication_Service();
if(SafetyManager_IsInhibited())
{
Motor_ForceSafeOff();
(void)BspSeatBackrestHeater_ForceOff();
(void)BspSeatFan_ForceOff();
}
SafetyManager_KickWatchdog();
Cy_SysTick_DelayInUs(SEAT_MOVE_TASK_PERIOD_US);
}
}
/* [] END OF FILE */
@@ -0,0 +1,332 @@
#include "safety_manager.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "motor_control.h"
#include "seat_motor_manager.h"
#include "seat_move_config.h"
#if defined(CoreDebug) && defined(CoreDebug_DHCSR_C_DEBUGEN_Msk)
#define SAFETY_DEBUG_DETACH_SUPPORTED (1u)
#else
#define SAFETY_DEBUG_DETACH_SUPPORTED (0u)
#endif
static volatile safety_fault_t latchedFault = SAFETY_FAULT_NONE;
static bool watchdogReset = false;
static bool watchdogInitialized = false;
static bool hilWatchdogResetArmed = false;
static bool hilWatchdogRecovered = false;
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
static bool debuggerWasAttached = false;
#endif
static bool SafetyManager_DebuggerAttached(void)
{
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
return ((CoreDebug->DHCSR & CoreDebug_DHCSR_C_DEBUGEN_Msk) != 0ul);
#else
return false;
#endif
}
static uint32_t SafetyManager_ReadDebugMarker(void)
{
return BACKUP->unBREG[SEAT_MOVE_DEBUG_SESSION_BREG_INDEX].u32Register;
}
static bool SafetyManager_WriteDebugMarker(uint32_t marker)
{
BACKUP->unBREG[SEAT_MOVE_DEBUG_SESSION_BREG_INDEX].u32Register = marker;
__DSB();
return (SafetyManager_ReadDebugMarker() == marker);
}
static bool SafetyManager_WaitForWatchdogState(bool enabled)
{
uint32_t remaining = 200ul;
uint32_t expected = enabled ? 1ul : 0ul;
/* WDT ENABLE is an asynchronous request bit; ENABLED is the synchronized
* LF-clock state. Configuration writes made before disable completes are
* ignored by the hardware. */
while((WDT->unCTL.stcField.u1ENABLED != expected) && (remaining > 0ul))
{
Cy_SysTick_DelayInUs(1u);
remaining--;
}
return (WDT->unCTL.stcField.u1ENABLED == expected);
}
static void SafetyManager_ClearHilWatchdogCredential(void)
{
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = 0u;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = 0u;
__DSB();
}
void SafetyManager_Init(uint32_t resetReason)
{
const uint32_t watchdogMask = CY_SYSRESET_WDT |
CY_SYSRESET_MCWDT0 |
CY_SYSRESET_MCWDT1 |
CY_SYSRESET_MCWDT2 |
CY_SYSRESET_MCWDT3;
bool watchdogStopped;
bool debuggerAttached;
uint32_t marker;
uint32_t hilMagic;
uint32_t hilToken;
uint32_t hilCheck;
bool authorizedHilReset;
bool markerOk = true;
/* A watchdog instance is retained across its own reset on this device.
* Stop it before reading credentials or initializing peripherals; the
* 100 ms bench timeout otherwise permits a reset loop. */
Cy_WDT_Disable();
watchdogStopped = SafetyManager_WaitForWatchdogState(false);
debuggerAttached = SafetyManager_DebuggerAttached();
marker = SafetyManager_ReadDebugMarker();
hilMagic = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register;
hilToken = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register;
hilCheck = BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register;
authorizedHilReset =
(hilMagic == SEAT_MOVE_HIL_WDT_MARKER) &&
(hilCheck == (hilMagic ^ hilToken ^ 0xFFFFFFFFu)) &&
(((resetReason & watchdogMask) != 0ul) ||
((resetReason & CY_SYSRESET_SOFT) != 0ul));
latchedFault = SAFETY_FAULT_NONE;
watchdogInitialized = false;
hilWatchdogResetArmed = false;
watchdogReset = ((resetReason & watchdogMask) != 0ul) ||
authorizedHilReset;
hilWatchdogRecovered = authorizedHilReset;
if((resetReason & CY_SYSRESET_PORVDDD) != 0ul)
{
markerOk = SafetyManager_WriteDebugMarker(0u);
marker = SafetyManager_ReadDebugMarker();
SafetyManager_ClearHilWatchdogCredential();
hilWatchdogRecovered = false;
}
else if(hilWatchdogRecovered)
{
/* Consume before CAN comes online: transport retries cannot turn one
* authenticated reset into a second physical reset. */
SafetyManager_ClearHilWatchdogCredential();
}
else if(hilMagic == SEAT_MOVE_HIL_WDT_MARKER)
{
/* XRES/programming without a valid reset reason is not completion. */
SafetyManager_ClearHilWatchdogCredential();
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
debuggerWasAttached = debuggerAttached;
#endif
if(!markerOk || (marker == SEAT_MOVE_DEBUG_EXIT_MAGIC) ||
((marker == SEAT_MOVE_DEBUG_ACTIVE_MAGIC) && !debuggerAttached))
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
else if(watchdogReset && !hilWatchdogRecovered)
{
latchedFault = SAFETY_FAULT_BOOT_RECOVERY;
}
else if(debuggerAttached &&
!SafetyManager_WriteDebugMarker(SEAT_MOVE_DEBUG_ACTIVE_MAGIC))
{
latchedFault = SAFETY_FAULT_DEBUG_DETACH;
}
/* The system-WDT configuration register is retained across some reset
* paths and AUTO_SERVICE is not reset by Cy_WDT_Init(). If that bit is
* left enabled, stopping the two software service paths can never prove a
* real watchdog reset because hardware services the counter at WARN_LIMIT.
* Configure only while stopped, and make software the sole WDT owner. */
if(!watchdogStopped)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Init();
Cy_WDT_Unlock();
Cy_WDT_SetUpperLimit(SEAT_MOVE_WDT_TIMEOUT_TICKS);
Cy_WDT_SetUpperAction(CY_WDT_LOW_UPP_ACTION_RESET);
Cy_WDT_SetWarnAction(CY_WDT_WARN_ACTION_NONE);
Cy_WDT_SetAutoService(CY_WDT_DISABLE);
Cy_WDT_SetDebugRun(CY_WDT_ENABLE);
Cy_WDT_Lock();
if((WDT->unUPPER_LIMIT.stcField.u32UPPER_LIMIT !=
SEAT_MOVE_WDT_TIMEOUT_TICKS) ||
(WDT->unCONFIG.stcField.u1UPPER_ACTION !=
(uint32_t)CY_WDT_LOW_UPP_ACTION_RESET) ||
(WDT->unCONFIG.stcField.u1AUTO_SERVICE !=
(uint32_t)CY_WDT_DISABLE))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_Enable();
if(!SafetyManager_WaitForWatchdogState(true))
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
Cy_WDT_ClearWatchdog();
Cy_SysTick_DelayInUs(1000u);
if(Cy_WDT_GetCount() == 0ul)
{
latchedFault = SAFETY_FAULT_DRIVER;
return;
}
watchdogInitialized = true;
SafetyManager_KickWatchdog();
}
void SafetyManager_KickWatchdog(void)
{
if((false != watchdogInitialized) && !hilWatchdogResetArmed)
{
Cy_WDT_ClearWatchdog();
}
}
bool SafetyManager_IsWatchdogInitialized(void)
{
return watchdogInitialized;
}
void SafetyManager_LatchFault(safety_fault_t fault)
{
if((fault != SAFETY_FAULT_NONE) &&
(latchedFault == SAFETY_FAULT_NONE))
{
latchedFault = fault;
Motor_LatchSafetyInhibitFromIsr(0u);
Motor_StopAll();
SeatMotorManager_StopAll(0ul);
}
}
void SafetyManager_Service(void)
{
bool debuggerAttached = SafetyManager_DebuggerAttached();
uint32_t marker = SafetyManager_ReadDebugMarker();
if(marker == SEAT_MOVE_DEBUG_EXIT_MAGIC)
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
}
if(Motor_IsDriverFaultLatched())
{
SafetyManager_LatchFault(SAFETY_FAULT_DRIVER);
}
else
{
/* No newly observed driver fault. */
}
#if (SAFETY_DEBUG_DETACH_SUPPORTED == 1u)
if(debuggerAttached)
{
if((marker != SEAT_MOVE_DEBUG_ACTIVE_MAGIC) &&
!SafetyManager_WriteDebugMarker(SEAT_MOVE_DEBUG_ACTIVE_MAGIC))
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
}
debuggerWasAttached = true;
}
else if(debuggerWasAttached)
{
SafetyManager_LatchFault(SAFETY_FAULT_DEBUG_DETACH);
debuggerWasAttached = false;
}
#else
(void)debuggerAttached;
#endif
SafetyManager_KickWatchdog();
}
bool SafetyManager_IsInhibited(void)
{
return (latchedFault != SAFETY_FAULT_NONE);
}
safety_fault_t SafetyManager_GetFault(void)
{
return latchedFault;
}
bool SafetyManager_WasWatchdogReset(void)
{
return watchdogReset;
}
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token)
{
uint32_t check;
if(!watchdogInitialized || hilWatchdogResetArmed)
{
return false;
}
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register =
SEAT_MOVE_HIL_WDT_MARKER;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX].u32Register = token;
check = SEAT_MOVE_HIL_WDT_MARKER ^ (uint32_t)token ^ 0xFFFFFFFFu;
BACKUP->unBREG[SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register = check;
__DSB();
if((BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX].u32Register !=
SEAT_MOVE_HIL_WDT_MARKER) ||
(BACKUP->unBREG[
SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX].u32Register != check))
{
SafetyManager_ClearHilWatchdogCredential();
return false;
}
/* Long-running motor calibration/coast loops normally service WDT
* directly so they remain safe even while the application loop is busy.
* The armed HIL case must suspend both service paths or a real watchdog
* reset can never occur. Startup re-enables this after the reset. */
Motor_SetWatchdogServiceEnabled(false);
Cy_WDT_ClearWatchdog();
hilWatchdogResetArmed = true;
return true;
}
void SafetyManager_CommitHilWatchdogReset(void)
{
if(!hilWatchdogResetArmed)
{
return;
}
/* This bench's connected CMSIS-DAP can suppress the WDT reset output even
* after C-SPY detaches. First prove physical WDT expiry, then use the
* reference project's authenticated processor-reset fallback. */
while(Cy_WDT_GetCount() < SEAT_MOVE_WDT_TIMEOUT_TICKS)
{
__NOP();
}
__DSB();
NVIC_SystemReset();
for(;;)
{
__NOP();
}
}
bool SafetyManager_WasHilWatchdogRecovered(void)
{
return hilWatchdogRecovered;
}
@@ -0,0 +1,30 @@
#ifndef SAFETY_MANAGER_H
#define SAFETY_MANAGER_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
SAFETY_FAULT_NONE = 0,
SAFETY_FAULT_DRIVER = 1,
SAFETY_FAULT_CAN_BUS_OFF = 2,
SAFETY_FAULT_CONFIG_STORE = 3,
SAFETY_FAULT_BOOT_RECOVERY = 4,
SAFETY_FAULT_DEBUG_DETACH = 5,
SAFETY_FAULT_CAN_INIT = 6
} safety_fault_t;
void SafetyManager_Init(uint32_t resetReason);
void SafetyManager_Service(void);
void SafetyManager_LatchFault(safety_fault_t fault);
void SafetyManager_KickWatchdog(void);
bool SafetyManager_IsWatchdogInitialized(void);
bool SafetyManager_IsInhibited(void);
safety_fault_t SafetyManager_GetFault(void);
bool SafetyManager_WasWatchdogReset(void);
bool SafetyManager_TriggerHilWatchdogReset(uint16_t token);
void SafetyManager_CommitHilWatchdogReset(void);
bool SafetyManager_WasHilWatchdogRecovered(void);
#endif /* SAFETY_MANAGER_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,154 @@
#ifndef SEAT_MOTOR_MANAGER_H
#define SEAT_MOTOR_MANAGER_H
#include <stdbool.h>
#include <stdint.h>
#include "motor_control.h"
typedef enum
{
SEAT_MOTOR_TASK_IDLE = 0,
SEAT_MOTOR_TASK_WAIT_START,
SEAT_MOTOR_TASK_POSITION,
SEAT_MOTOR_TASK_TIMED,
SEAT_MOTOR_TASK_RETRY_DELAY,
SEAT_MOTOR_TASK_RANGE_CALIBRATION,
SEAT_MOTOR_TASK_SPEED_CAL_WARMUP,
SEAT_MOTOR_TASK_SPEED_CAL_MEASURE,
SEAT_MOTOR_TASK_SPEED_CAL_SETTLE,
SEAT_MOTOR_TASK_COMPLETE,
SEAT_MOTOR_TASK_FAILED
} seat_motor_task_state_t;
typedef enum
{
SEAT_MOTOR_RESULT_NONE = 0,
SEAT_MOTOR_RESULT_RUNNING,
SEAT_MOTOR_RESULT_OK,
SEAT_MOTOR_RESULT_STOPPED,
SEAT_MOTOR_RESULT_STALL,
SEAT_MOTOR_RESULT_ENDPOINT,
SEAT_MOTOR_RESULT_TIMEOUT,
SEAT_MOTOR_RESULT_DRIVER_FAULT,
SEAT_MOTOR_RESULT_INVALID,
SEAT_MOTOR_RESULT_CALIBRATION_FAILED
} seat_motor_result_t;
typedef struct
{
seat_motor_task_state_t taskState;
seat_motor_result_t result;
motor_event_t lastMotorEvent;
motor_direction_t direction;
uint32_t commandSequence;
uint32_t valueX100;
uint32_t hallCount;
uint32_t maxHallCount;
uint32_t maxSpeedPpsX100;
uint32_t hallSpeedPpsX100;
uint32_t positionPercentX100;
uint32_t positionMmX100;
uint32_t horizontalMmX100;
uint32_t verticalMmX100;
uint32_t angleDegX100;
uint8_t dutyPercent;
uint8_t retryCount;
bool active;
bool retried;
bool positionValid;
} seat_motor_status_t;
void SeatMotorManager_Init(void);
void SeatMotorManager_Service(void);
void SeatMotorManager_Stop(motor_mode_t mode, uint32_t commandSequence);
void SeatMotorManager_StopAll(uint32_t commandSequence);
bool SeatMotorManager_IsAnyActive(void);
bool SeatMotorManager_GetStatus(motor_mode_t mode,
seat_motor_status_t *status);
/* Six standardized, non-blocking motion primitives. Physical values are x100. */
bool SeatMotor_MoveAbsoluteDuty(motor_mode_t mode,
uint32_t targetValueX100,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteNormalized(motor_mode_t mode,
uint32_t targetPercentX100,
uint32_t timeoutMs,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveRelativeNormalized(motor_mode_t mode,
int32_t deltaPercentX100,
uint32_t timeoutMs,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveRelativeDuty(motor_mode_t mode,
int32_t deltaValueX100,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveTimedDuty(motor_mode_t mode,
motor_direction_t direction,
uint32_t durationMs,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteSpeed(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
bool SeatMotor_MoveRelativeSpeed(motor_mode_t mode,
int32_t deltaValueX100,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
bool SeatMotor_MoveTimedSpeed(motor_mode_t mode,
motor_direction_t direction,
uint32_t durationMs,
uint32_t speedValuePerSecondX100,
uint32_t commandSequence);
/* Position command with an explicit requested arrival time. */
bool SeatMotor_MoveAbsoluteInTime(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t commandSequence);
/* Keep the same speed plan while allowing a bounded scheduler/settling grace
* before the manager reports TIMEOUT. */
bool SeatMotor_MoveAbsoluteInTimeWithGrace(motor_mode_t mode,
uint32_t targetValueX100,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Hall-coordinate variants used by firmware-owned synchronized trajectories.
* The target is checked against the live calibrated MaxHall before conversion
* to the motor's physical value. */
bool SeatMotor_MoveAbsoluteHallDuty(motor_mode_t mode,
uint32_t targetHallCount,
uint8_t dutyPercent,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTime(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t commandSequence);
bool SeatMotor_MoveAbsoluteHallInTimeWithGrace(motor_mode_t mode,
uint32_t targetHallCount,
uint32_t durationMs,
uint32_t deadlineGraceMs,
uint32_t commandSequence);
/* Range calibration is interruptible by Motor_RequestAbortAllFromIsr(). */
bool SeatMotor_CalibrateRange(motor_mode_t mode,
uint32_t legTimeoutMs,
uint32_t commandSequence);
/* Re-establish zero at the known near endpoint while retaining the confirmed
* default MaxHall. This performs one near-seek only, not a full-range test. */
bool SeatMotor_HomeKnownRange(motor_mode_t mode,
uint32_t legTimeoutMs,
uint32_t commandSequence);
bool SeatMotor_StartMaximumSpeedCalibration(motor_mode_t mode,
uint32_t commandSequence);
bool SeatMotorManager_ClearFault(motor_mode_t mode);
/* Optional endpoint-based position recovery primitive. */
bool SeatMotor_RecoverPosition(motor_mode_t mode, uint32_t savedHallCount);
#endif /* SEAT_MOTOR_MANAGER_H */
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
#ifndef SEAT_TIMED_APPLICATION_H
#define SEAT_TIMED_APPLICATION_H
#include <stdbool.h>
#include <stdint.h>
bool SeatTimedApplication_Init(void);
void SeatTimedApplication_Service(void);
void SeatTimedApplication_StopAll(uint16_t sequence);
#endif /* SEAT_TIMED_APPLICATION_H */
@@ -0,0 +1,687 @@
#include "bsp_seat_backrest_heater.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_backrest_heater_config.h"
static bool BspSeatBackrestHeater_ConfigClock(void);
static uint32_t BspSeatBackrestHeater_DutyToCompare(uint32_t dutyPercent);
static void BspSeatBackrestHeater_SetPinGpioLow(void);
static bool BspSeatBackrestHeaterCan_ConfigClock(void);
static bool BspSeatBackrestHeaterCan_ConfigPins(void);
static void BspSeatBackrestHeaterCan_IrqHandler(void);
static void BspSeatBackrestHeaterCan_RxCallback(
bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
static uint32_t s_heaterDutyPercent = 0u;
volatile uint32_t g_bspSeatBackrestHeaterStartFailStage = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatBackrestHeaterCanRxCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanTxCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastRxId = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastTxId = 0u;
volatile uint32_t g_bspSeatBackrestHeaterCanLastTxStatus =
(uint32_t)CY_CANFD_SUCCESS;
static volatile bsp_seat_backrest_heater_can_frame_t
s_canRxQueue[SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_canRxQueueHead = 0u;
static volatile uint8_t s_canRxQueueTail = 0u;
static const cy_stc_id_filter_t s_canStandardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID,
SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID,
SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID,
SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID,
SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_BACKREST_HEATER_CAN_IRQN,
.intIdx = SEAT_BACKREST_HEATER_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatBackrestHeaterCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = NULL,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_canStandardIdFilters) /
sizeof(s_canStandardIdFilters[0]),
.sidFilter = s_canStandardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements =
SEAT_BACKREST_HEATER_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT,
};
static const cy_stc_tcpwm_pwm_config_t s_heaterPwmConfig =
{
.pwmMode = CY_TCPWM_PWM_MODE_PWM,
.clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1,
.debug_pause = false,
.deadTime = 0u,
.deadTimeComp = 0u,
.runMode = CY_TCPWM_PWM_CONTINUOUS,
.countDirection = CY_TCPWM_COUNTER_COUNT_UP,
.cc0MatchMode = CY_TCPWM_PWM_TR_CTRL2_CLEAR,
.overflowMode = CY_TCPWM_PWM_TR_CTRL2_SET,
.underflowMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.cc1MatchMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.period = SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u,
.period_buff = 0u,
.enablePeriodSwap = false,
.enableLineSelSwap = false,
.compare0 = 0u,
.compare0_buff = 0u,
.enableCompare0Swap = false,
.compare1 = 0u,
.compare1_buff = 0u,
.enableCompare1Swap = false,
.interruptSources = CY_TCPWM_INT_NONE,
.invertPWMOut = 0u,
.invertPWMOutN = 0u,
.killMode = CY_TCPWM_PWM_STOP_ON_KILL,
.switchInputMode = CY_TCPWM_INPUT_LEVEL,
.switchInput = 0u,
.reloadInputMode = CY_TCPWM_INPUT_LEVEL,
.reloadInput = 0u,
.startInputMode = CY_TCPWM_INPUT_LEVEL,
.startInput = 0u,
.kill0InputMode = CY_TCPWM_INPUT_LEVEL,
.kill0Input = 0u,
.kill1InputMode = CY_TCPWM_INPUT_LEVEL,
.kill1Input = 0u,
.countInputMode = CY_TCPWM_INPUT_LEVEL,
.countInput = 1u,
.immediateKill = true,
.pwmOnDisable = CY_TCPWM_PWM_OUT_MODE_LOW,
.trigger0EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
.trigger1EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
};
bool BspSeatBackrestHeater_ForceOff(void)
{
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
cy_en_gpio_status_t status;
status = Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
&pinConfig);
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
s_heaterDutyPercent = 0u;
return ((status == CY_GPIO_SUCCESS) &&
(Cy_GPIO_GetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN) ==
SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM));
}
bool BspSeatBackrestHeater_InitPwm(void)
{
if(!BspSeatBackrestHeater_ConfigClock())
{
return false;
}
Cy_Tcpwm_Pwm_Disable(SEAT_BACKREST_HEATER_PWM_TCPWM);
if(Cy_Tcpwm_Pwm_Init(SEAT_BACKREST_HEATER_PWM_TCPWM,
&s_heaterPwmConfig) != CY_RET_SUCCESS)
{
return false;
}
return ((Cy_Tcpwm_Pwm_GetPeriod(SEAT_BACKREST_HEATER_PWM_TCPWM) ==
(SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u)) &&
(Cy_Tcpwm_Pwm_GetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM) == 0u));
}
bool BspSeatBackrestHeater_Start(uint32_t dutyPercent)
{
g_bspSeatBackrestHeaterStartFailStage = 0u;
if(!BspSeatBackrestHeater_SetDutyPercent(dutyPercent))
{
g_bspSeatBackrestHeaterStartFailStage = 1u;
return false;
}
/* P16.7 remains GPIO low until the PWM counter is confirmed running. */
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
Cy_Tcpwm_Pwm_SetCounter(SEAT_BACKREST_HEATER_PWM_TCPWM,
SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS - 1u);
Cy_Tcpwm_Pwm_Enable(SEAT_BACKREST_HEATER_PWM_TCPWM);
Cy_Tcpwm_TriggerStart(SEAT_BACKREST_HEATER_PWM_TCPWM);
Cy_SysTick_DelayInUs(2u);
if((Cy_Tcpwm_Pwm_GetStatus(SEAT_BACKREST_HEATER_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) == 0u)
{
g_bspSeatBackrestHeaterStartFailStage = 2u;
BspSeatBackrestHeater_FailSafeOff();
return false;
}
Cy_GPIO_SetDriveSel(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_SetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
SEAT_BACKREST_HEATER_PWM_HSIOM);
if(Cy_GPIO_GetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN) !=
SEAT_BACKREST_HEATER_PWM_HSIOM)
{
g_bspSeatBackrestHeaterStartFailStage = 3u;
BspSeatBackrestHeater_FailSafeOff();
return false;
}
return true;
}
bool BspSeatBackrestHeater_SetDutyPercent(uint32_t dutyPercent)
{
uint32_t compare;
/* A true OFF uses FailSafeOff, avoiding a possible zero-compare pulse. */
if((dutyPercent == 0u) || (dutyPercent > 100u))
{
return false;
}
compare = BspSeatBackrestHeater_DutyToCompare(dutyPercent);
Cy_Tcpwm_Pwm_SetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM, compare);
if(Cy_Tcpwm_Pwm_GetCompare0(SEAT_BACKREST_HEATER_PWM_TCPWM) != compare)
{
return false;
}
s_heaterDutyPercent = dutyPercent;
return true;
}
void BspSeatBackrestHeater_FailSafeOff(void)
{
Cy_Tcpwm_Pwm_Disable(SEAT_BACKREST_HEATER_PWM_TCPWM);
BspSeatBackrestHeater_SetPinGpioLow();
s_heaterDutyPercent = 0u;
}
bool BspSeatBackrestHeater_IsRunning(void)
{
return ((Cy_Tcpwm_Pwm_GetStatus(SEAT_BACKREST_HEATER_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) != 0u);
}
uint32_t BspSeatBackrestHeater_GetDutyPercent(void)
{
return s_heaterDutyPercent;
}
bool BspSeatBackrestHeaterCan_Init(void)
{
cy_en_canfd_status_t canStatus;
if(!BspSeatBackrestHeaterCan_ConfigClock() ||
!BspSeatBackrestHeaterCan_ConfigPins())
{
g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatBackrestHeaterCanInitStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatBackrestHeaterCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_BACKREST_HEATER_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_BACKREST_HEATER_CAN_TYPE, &s_canConfig);
g_bspSeatBackrestHeaterCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatBackrestHeaterCan_Read(
bsp_seat_backrest_heater_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_canRxQueueTail;
if(tail == s_canRxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_canRxQueue[tail].identifier;
frame->data0 = s_canRxQueue[tail].data0;
frame->data1 = s_canRxQueue[tail].data1;
frame->dlc = s_canRxQueue[tail].dlc;
tail++;
if(tail >= SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_canRxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatBackrestHeaterCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1)
{
cy_stc_canfd_msg_t message;
uint8_t bufferIndex;
uint8_t dataIndex;
if(identifier > 0x7FFu)
{
g_bspSeatBackrestHeaterCanLastTxStatus =
(uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_BACKREST_HEATER_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(dataIndex = 2u;
dataIndex < CY_CANFD_MESSAGE_DATA_BUFFER_SIZE;
dataIndex++)
{
message.dataConfig.data[dataIndex] = 0u;
}
for(bufferIndex = 0u;
bufferIndex < SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_BACKREST_HEATER_CAN_TYPE,
bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatBackrestHeaterCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_BACKREST_HEATER_CAN_TYPE,
bufferIndex,
&message);
g_bspSeatBackrestHeaterCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatBackrestHeaterCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatBackrestHeaterCanTxBusyCount++;
return false;
}
static bool BspSeatBackrestHeaterCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_BACKREST_HEATER_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_BACKREST_HEATER_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_BACKREST_HEATER_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_BACKREST_HEATER_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(
SEAT_BACKREST_HEATER_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(
clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatBackrestHeaterCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_BACKREST_HEATER_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_BACKREST_HEATER_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t transceiverConfig =
{
.outVal = SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_RX_PORT,
SEAT_BACKREST_HEATER_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_TX_PORT,
SEAT_BACKREST_HEATER_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN,
&transceiverConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT,
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN) ==
SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatBackrestHeaterCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_BACKREST_HEATER_CAN_TYPE);
}
static void BspSeatBackrestHeaterCan_RxCallback(
bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatBackrestHeaterCanRxCount++;
g_bspSeatBackrestHeaterCanLastRxId = message->idConfig.identifier;
head = s_canRxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_canRxQueueTail)
{
g_bspSeatBackrestHeaterCanRxQueueOverflowCount++;
return;
}
s_canRxQueue[head].identifier = message->idConfig.identifier;
s_canRxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_canRxQueue[head].data0 = message->dataConfig.data[0];
s_canRxQueue[head].data1 = message->dataConfig.data[1];
s_canRxQueueHead = nextHead;
}
static bool BspSeatBackrestHeater_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ) ||
((periClockHz % SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ;
if((divider == 0u) || (divider > 65536u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_BACKREST_HEATER_PWM_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_BACKREST_HEATER_PWM_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static uint32_t BspSeatBackrestHeater_DutyToCompare(uint32_t dutyPercent)
{
return (SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS * dutyPercent) / 100u;
}
static void BspSeatBackrestHeater_SetPinGpioLow(void)
{
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
Cy_GPIO_SetHSIOM(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM);
Cy_GPIO_SetDriveSel(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_Write(SEAT_BACKREST_HEATER_PWM_PORT,
SEAT_BACKREST_HEATER_PWM_PIN,
0u);
}
@@ -0,0 +1,62 @@
#ifndef BSP_SEAT_BACKREST_HEATER_H
#define BSP_SEAT_BACKREST_HEATER_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_backrest_heater_can_frame_t;
/* Force the BTS7010 IN path to GPIO low before PWM initialization. */
bool BspSeatBackrestHeater_ForceOff(void);
/* Configure TCPWM1 counter 65 while P16.7 remains a strong GPIO low. */
bool BspSeatBackrestHeater_InitPwm(void);
/* Start a non-zero PWM duty, then connect P16.7 to TCPWM line 65. */
bool BspSeatBackrestHeater_Start(uint32_t dutyPercent);
/* Change a running non-zero duty. Use FailSafeOff for a true 0% output. */
bool BspSeatBackrestHeater_SetDutyPercent(uint32_t dutyPercent);
/* Disable TCPWM and restore P16.7 to strong GPIO low. */
void BspSeatBackrestHeater_FailSafeOff(void);
bool BspSeatBackrestHeater_IsRunning(void);
uint32_t BspSeatBackrestHeater_GetDutyPercent(void);
/*
* Start-path diagnostic for bench/HIL fault isolation.
* 0 = no start failure, 1 = duty/compare write, 2 = counter did not run,
* 3 = P16.7 HSIOM switch/readback.
*/
extern volatile uint32_t g_bspSeatBackrestHeaterStartFailStage;
/* Initialize the proven CAN4/TJA1044 board interface at 500 kbit/s. */
bool BspSeatBackrestHeaterCan_Init(void);
/* Pop one received standard Classic CAN frame from the interrupt-safe queue. */
bool BspSeatBackrestHeaterCan_Read(
bsp_seat_backrest_heater_can_frame_t *frame);
/* Send one 8-byte standard Classic CAN frame. */
bool BspSeatBackrestHeaterCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1);
/* Watch-window diagnostics for CAN hardware integration. */
extern volatile uint32_t g_bspSeatBackrestHeaterCanInitStatus;
extern volatile uint32_t g_bspSeatBackrestHeaterCanRxCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanTxCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanTxBusyCount;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastRxId;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastTxId;
extern volatile uint32_t g_bspSeatBackrestHeaterCanLastTxStatus;
#endif /* BSP_SEAT_BACKREST_HEATER_H */
@@ -0,0 +1,332 @@
#include "bsp_seat_current_adc.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_current_adc_config.h"
#define SEAT_CURRENT_ADC_DIV_ROUND_UP(a, b) (((a) + (b) - 1u) / (b))
typedef struct
{
volatile stc_GPIO_PRT_t *gpioPort;
uint32_t gpioPin;
en_hsiom_sel_t gpioHsiom;
volatile stc_PASS_SAR_CH_t *adcChannel;
cy_en_adc_pin_address_t pinAddress;
} bsp_seat_current_adc_profile_t;
static const bsp_seat_current_adc_profile_t s_profiles[] =
{
{
SEAT_CURRENT_M0_PORT,
SEAT_CURRENT_M0_PIN,
SEAT_CURRENT_M0_GPIO_HSIOM,
SEAT_CURRENT_M0_CHANNEL,
SEAT_CURRENT_M0_PIN_ADDRESS,
},
{
SEAT_CURRENT_M1_PORT,
SEAT_CURRENT_M1_PIN,
SEAT_CURRENT_M1_GPIO_HSIOM,
SEAT_CURRENT_M1_CHANNEL,
SEAT_CURRENT_M1_PIN_ADDRESS,
},
{
SEAT_CURRENT_M2_PORT,
SEAT_CURRENT_M2_PIN,
SEAT_CURRENT_M2_GPIO_HSIOM,
SEAT_CURRENT_M2_CHANNEL,
SEAT_CURRENT_M2_PIN_ADDRESS,
},
{
SEAT_CURRENT_M3_PORT,
SEAT_CURRENT_M3_PIN,
SEAT_CURRENT_M3_GPIO_HSIOM,
SEAT_CURRENT_M3_CHANNEL,
SEAT_CURRENT_M3_PIN_ADDRESS,
},
{
SEAT_CURRENT_HEATER_PORT,
SEAT_CURRENT_HEATER_PIN,
SEAT_CURRENT_HEATER_GPIO_HSIOM,
SEAT_CURRENT_HEATER_CHANNEL,
SEAT_CURRENT_HEATER_PIN_ADDRESS,
},
{
SEAT_CURRENT_FAN_PORT,
SEAT_CURRENT_FAN_PIN,
SEAT_CURRENT_FAN_GPIO_HSIOM,
SEAT_CURRENT_FAN_CHANNEL,
SEAT_CURRENT_FAN_PIN_ADDRESS,
},
};
typedef char bsp_seat_current_adc_profile_count_must_match[
((sizeof(s_profiles) / sizeof(s_profiles[0])) ==
(uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT) ? 1 : -1];
static bool s_initialized = false;
static bool s_sampleActive = false;
static uint16_t s_lastRaw[BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT];
static bool s_lastRawValid[BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT];
static bool BspSeatCurrentAdc_ConfigClock(uint32_t *actualClockHz);
static bool BspSeatCurrentAdc_WaitDone(
volatile stc_PASS_SAR_CH_t *channel);
static void BspSeatCurrentAdc_ClearStatus(
volatile stc_PASS_SAR_CH_t *channel);
bool BspSeatCurrentAdc_Init(void)
{
uint32_t actualClockHz;
uint32_t samplingCycles;
uint32_t index;
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_ANALOG,
.hsiom = HSIOM_SEL_GPIO,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_adc_config_t adcConfig =
{
.preconditionTime = 0u,
.powerupTime = 0u,
.enableIdlePowerDown = false,
.msbStretchMode = CY_ADC_MSB_STRETCH_MODE_1CYCLE,
.enableHalfLsbConv = false,
.sarMuxEnable = true,
.adcEnable = true,
.sarIpEnable = true,
};
cy_stc_adc_channel_config_t channelConfig =
{
.triggerSelection = CY_ADC_TRIGGER_OFF,
.channelPriority = 0u,
.preenptionType = CY_ADC_PREEMPTION_FINISH_RESUME,
.isGroupEnd = true,
.doneLevel = CY_ADC_DONE_LEVEL_PULSE,
.pinAddress = CY_ADC_PIN_ADDRESS_AN0,
.portAddress = SEAT_CURRENT_ADC_PORT_ADDRESS,
.extMuxSelect = 0u,
.extMuxEnable = true,
.preconditionMode = CY_ADC_PRECONDITION_MODE_OFF,
.overlapDiagMode = CY_ADC_OVERLAP_DIAG_MODE_OFF,
.sampleTime = 0u,
.calibrationValueSelect = CY_ADC_CALIBRATION_VALUE_REGULAR,
.postProcessingMode = CY_ADC_POST_PROCESSING_MODE_NONE,
.resultAlignment = CY_ADC_RESULT_ALIGNMENT_RIGHT,
.signExtention = CY_ADC_SIGN_EXTENTION_UNSIGNED,
.averageCount = 0u,
.rightShift = 0u,
.rangeDetectionMode = CY_ADC_RANGE_DETECTION_MODE_INSIDE_RANGE,
.rangeDetectionLoThreshold = 0u,
.rangeDetectionHiThreshold = SEAT_CURRENT_ADC_FULL_SCALE,
.mask.grpDone = true,
.mask.grpCancelled = false,
.mask.grpOverflow = false,
.mask.chRange = false,
.mask.chPulse = false,
.mask.chOverflow = false,
};
if(s_initialized)
{
return true;
}
for(index = 0u;
index < (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT;
index++)
{
pinConfig.hsiom = s_profiles[index].gpioHsiom;
if(Cy_GPIO_Pin_Init(s_profiles[index].gpioPort,
s_profiles[index].gpioPin,
&pinConfig) != CY_GPIO_SUCCESS)
{
return false;
}
s_lastRaw[index] = 0u;
s_lastRawValid[index] = false;
}
if(!BspSeatCurrentAdc_ConfigClock(&actualClockHz))
{
return false;
}
samplingCycles = (uint32_t)SEAT_CURRENT_ADC_DIV_ROUND_UP(
(SEAT_CURRENT_ADC_SAMPLE_TIME_MIN_NS * (uint64_t)actualClockHz),
1000000000ull);
if(samplingCycles == 0u)
{
samplingCycles = 1u;
}
channelConfig.sampleTime = (uint16_t)samplingCycles;
Cy_Adc_DeInit(SEAT_CURRENT_ADC_INSTANCE);
if(Cy_Adc_Init(SEAT_CURRENT_ADC_INSTANCE,
&adcConfig) != CY_ADC_SUCCESS)
{
return false;
}
for(index = 0u;
index < (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT;
index++)
{
channelConfig.pinAddress = s_profiles[index].pinAddress;
if(Cy_Adc_Channel_Init(s_profiles[index].adcChannel,
&channelConfig) != CY_ADC_SUCCESS)
{
Cy_Adc_DeInit(SEAT_CURRENT_ADC_INSTANCE);
return false;
}
Cy_Adc_Channel_Enable(s_profiles[index].adcChannel);
}
s_initialized = true;
return true;
}
bool BspSeatCurrentAdc_Sample(bsp_seat_current_adc_signal_t signal,
uint16_t *raw)
{
uint32_t index = (uint32_t)signal;
uint16_t sample = 0u;
cy_stc_adc_ch_status_t resultStatus = { false };
volatile stc_PASS_SAR_CH_t *channel;
bool success = false;
if(!s_initialized || s_sampleActive ||
(index >= (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT))
{
return false;
}
s_sampleActive = true;
channel = s_profiles[index].adcChannel;
BspSeatCurrentAdc_ClearStatus(channel);
Cy_Adc_Channel_SoftwareTrigger(channel);
if(BspSeatCurrentAdc_WaitDone(channel) &&
(Cy_Adc_Channel_GetResult(channel,
&sample,
&resultStatus) == CY_ADC_SUCCESS) &&
resultStatus.valid)
{
sample = (uint16_t)(sample & SEAT_CURRENT_ADC_FULL_SCALE);
s_lastRaw[index] = sample;
s_lastRawValid[index] = true;
if(raw != NULL)
{
*raw = sample;
}
success = true;
}
BspSeatCurrentAdc_ClearStatus(channel);
s_sampleActive = false;
return success;
}
bool BspSeatCurrentAdc_GetLastRaw(bsp_seat_current_adc_signal_t signal,
uint16_t *raw)
{
uint32_t index = (uint32_t)signal;
if((raw == NULL) ||
(index >= (uint32_t)BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT) ||
!s_lastRawValid[index])
{
return false;
}
*raw = s_lastRaw[index];
return true;
}
static bool BspSeatCurrentAdc_ConfigClock(uint32_t *actualClockHz)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup = Cy_SysClk_GetClockGroup(SEAT_CURRENT_ADC_PCLK);
if((actualClockHz == NULL) ||
(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS))
{
return false;
}
divider = SEAT_CURRENT_ADC_DIV_ROUND_UP(
periClockHz, SEAT_CURRENT_ADC_OPERATION_FREQ_MAX_HZ);
if((divider == 0u) || (divider > 65536u))
{
return false;
}
if(Cy_SysClk_PeriphDisableDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(
SEAT_CURRENT_ADC_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphEnableDivider(
clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_CURRENT_ADC_DIVIDER_NUMBER) != CY_SYSCLK_SUCCESS)
{
return false;
}
*actualClockHz = periClockHz / divider;
return true;
}
static bool BspSeatCurrentAdc_WaitDone(
volatile stc_PASS_SAR_CH_t *channel)
{
uint32_t timeout = SEAT_CURRENT_ADC_POLL_TIMEOUT;
cy_stc_adc_interrupt_source_t status = { false };
do
{
(void)Cy_Adc_Channel_GetInterruptStatus(channel, &status);
if(status.grpDone)
{
return true;
}
timeout--;
} while(timeout > 0u);
return false;
}
static void BspSeatCurrentAdc_ClearStatus(
volatile stc_PASS_SAR_CH_t *channel)
{
cy_stc_adc_interrupt_source_t clearSource =
{
.grpDone = true,
.grpCancelled = true,
.grpOverflow = true,
.chRange = false,
.chPulse = false,
.chOverflow = true,
};
(void)Cy_Adc_Channel_ClearInterruptStatus(channel, &clearSource);
}
@@ -0,0 +1,27 @@
#ifndef BSP_SEAT_CURRENT_ADC_H
#define BSP_SEAT_CURRENT_ADC_H
#include <stdbool.h>
#include <stdint.h>
typedef enum
{
BSP_SEAT_CURRENT_ADC_MOTOR0 = 0,
BSP_SEAT_CURRENT_ADC_MOTOR1,
BSP_SEAT_CURRENT_ADC_MOTOR2,
BSP_SEAT_CURRENT_ADC_MOTOR3,
BSP_SEAT_CURRENT_ADC_HEATER,
BSP_SEAT_CURRENT_ADC_FAN,
BSP_SEAT_CURRENT_ADC_SIGNAL_COUNT
} bsp_seat_current_adc_signal_t;
/* Initializes PASS0 SAR1 once and enables every shared seat-current channel. */
bool BspSeatCurrentAdc_Init(void);
/* Main-loop polling API. It must not be called from an ISR. */
bool BspSeatCurrentAdc_Sample(bsp_seat_current_adc_signal_t signal,
uint16_t *raw);
bool BspSeatCurrentAdc_GetLastRaw(bsp_seat_current_adc_signal_t signal,
uint16_t *raw);
#endif /* BSP_SEAT_CURRENT_ADC_H */
@@ -0,0 +1,659 @@
#include "bsp_seat_fan.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "seat_fan_config.h"
static bool BspSeatFan_ConfigClock(void);
static uint32_t BspSeatFan_DutyToCompare(uint32_t dutyPercent);
static void BspSeatFan_SetPinGpioLow(volatile stc_GPIO_PRT_t *port,
uint32_t pin,
en_hsiom_sel_t gpioHsiom);
static bool BspSeatCan_ConfigClock(void);
static bool BspSeatCan_ConfigPins(void);
static void BspSeatCan_IrqHandler(void);
static void BspSeatCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
volatile uint32_t g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatCanRxCount = 0u;
volatile uint32_t g_bspSeatCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatCanTxCount = 0u;
volatile uint32_t g_bspSeatCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatCanLastRxId = 0u;
volatile uint32_t g_bspSeatCanLastTxId = 0u;
volatile uint32_t g_bspSeatCanLastTxStatus = (uint32_t)CY_CANFD_SUCCESS;
static volatile bsp_seat_can_frame_t s_canRxQueue[SEAT_FAN_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_canRxQueueHead = 0u;
static volatile uint8_t s_canRxQueueTail = 0u;
volatile uint32_t g_bspSeatFanStartFailStage = 0u;
static const cy_stc_id_filter_t s_canStandardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_CONTROL_29E_ID,
SEAT_FAN_CAN_CONTROL_29E_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_CONFIG_2AA_ID,
SEAT_FAN_CAN_CONFIG_2AA_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_FAN_CAN_SWITCH_42A_ID,
SEAT_FAN_CAN_SWITCH_42A_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_FAN_CAN_IRQN,
.intIdx = SEAT_FAN_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = NULL,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_canStandardIdFilters) / sizeof(s_canStandardIdFilters[0]),
.sidFilter = s_canStandardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = SEAT_FAN_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_FAN_CAN_TX_BUFFER_COUNT,
};
static const cy_stc_tcpwm_pwm_config_t s_seatFanPwmConfig =
{
.pwmMode = CY_TCPWM_PWM_MODE_PWM,
.clockPrescaler = CY_TCPWM_PRESCALER_DIVBY_1,
.debug_pause = false,
.deadTime = 0u,
.deadTimeComp = 0u,
.runMode = CY_TCPWM_PWM_CONTINUOUS,
.countDirection = CY_TCPWM_COUNTER_COUNT_UP,
.cc0MatchMode = CY_TCPWM_PWM_TR_CTRL2_CLEAR,
.overflowMode = CY_TCPWM_PWM_TR_CTRL2_SET,
.underflowMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.cc1MatchMode = CY_TCPWM_PWM_TR_CTRL2_NO_CHANGE,
.period = SEAT_FAN_PWM_PERIOD_TICKS - 1u,
.period_buff = 0u,
.enablePeriodSwap = false,
.enableLineSelSwap = false,
.compare0 = SEAT_FAN_PWM_PERIOD_TICKS,
.compare0_buff = 0u,
.enableCompare0Swap = false,
.compare1 = 0u,
.compare1_buff = 0u,
.enableCompare1Swap = false,
.interruptSources = CY_TCPWM_INT_NONE,
.invertPWMOut = 0u,
.invertPWMOutN = 0u,
.killMode = CY_TCPWM_PWM_STOP_ON_KILL,
.switchInputMode = CY_TCPWM_INPUT_LEVEL,
.switchInput = 0u,
.reloadInputMode = CY_TCPWM_INPUT_LEVEL,
.reloadInput = 0u,
.startInputMode = CY_TCPWM_INPUT_LEVEL,
.startInput = 0u,
.kill0InputMode = CY_TCPWM_INPUT_LEVEL,
.kill0Input = 0u,
.kill1InputMode = CY_TCPWM_INPUT_LEVEL,
.kill1Input = 0u,
.countInputMode = CY_TCPWM_INPUT_LEVEL,
.countInput = 1u,
.immediateKill = true,
.pwmOnDisable = CY_TCPWM_PWM_OUT_MODE_LOW,
.trigger0EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
.trigger1EventCfg = CY_TCPWM_COUNTER_OVERFLOW,
};
bool BspSeatFan_ForceOff(void)
{
cy_stc_gpio_pin_config_t pinConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_FAN_PWM_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
cy_en_gpio_status_t pwmPinStatus;
cy_en_gpio_status_t unusedPinStatus;
pwmPinStatus = Cy_GPIO_Pin_Init(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
&pinConfig);
pinConfig.hsiom = SEAT_FAN_UNUSED_GPIO_HSIOM;
unusedPinStatus = Cy_GPIO_Pin_Init(SEAT_FAN_UNUSED_PORT,
SEAT_FAN_UNUSED_PIN,
&pinConfig);
/* Write the output latches again after configuration for an explicit safe state. */
Cy_GPIO_Write(SEAT_FAN_PWM_PORT, SEAT_FAN_PWM_PIN, 0u);
Cy_GPIO_Write(SEAT_FAN_UNUSED_PORT, SEAT_FAN_UNUSED_PIN, 0u);
return ((pwmPinStatus == CY_GPIO_SUCCESS) &&
(unusedPinStatus == CY_GPIO_SUCCESS));
}
bool BspSeatFan_InitPwm(void)
{
if(!BspSeatFan_ConfigClock())
{
return false;
}
Cy_Tcpwm_Pwm_Disable(SEAT_FAN_PWM_TCPWM);
if(Cy_Tcpwm_Pwm_Init(SEAT_FAN_PWM_TCPWM,
&s_seatFanPwmConfig) != CY_RET_SUCCESS)
{
return false;
}
return ((Cy_Tcpwm_Pwm_GetPeriod(SEAT_FAN_PWM_TCPWM) ==
(SEAT_FAN_PWM_PERIOD_TICKS - 1u)) &&
(Cy_Tcpwm_Pwm_GetCompare0(SEAT_FAN_PWM_TCPWM) ==
SEAT_FAN_PWM_PERIOD_TICKS));
}
bool BspSeatFan_Start(uint32_t dutyPercent)
{
g_bspSeatFanStartFailStage = 0u;
if(!BspSeatFan_SetDutyPercent(dutyPercent))
{
g_bspSeatFanStartFailStage = 1u;
return false;
}
/*
* P11.1 is still GPIO low here. Starting from PERIOD makes the first
* counter clock generate overflow immediately, which asserts LINE high
* for the 100% startup setting before the pin is switched to TCPWM.
*/
Cy_GPIO_Write(SEAT_FAN_PWM_PORT, SEAT_FAN_PWM_PIN, 0u);
Cy_Tcpwm_Pwm_SetCounter(SEAT_FAN_PWM_TCPWM,
SEAT_FAN_PWM_PERIOD_TICKS - 1u);
Cy_Tcpwm_Pwm_Enable(SEAT_FAN_PWM_TCPWM);
Cy_Tcpwm_TriggerStart(SEAT_FAN_PWM_TCPWM);
/* Keep the pin at GPIO low until the 2 MHz counter has started and overflowed. */
Cy_SysTick_DelayInUs(2u);
if((Cy_Tcpwm_Pwm_GetStatus(SEAT_FAN_PWM_TCPWM) &
TCPWM_GRP_CNT_STATUS_RUNNING_Msk) == 0u)
{
g_bspSeatFanStartFailStage = 2u;
BspSeatFan_FailSafeOff();
return false;
}
Cy_GPIO_SetDriveSel(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_SetHSIOM(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
SEAT_FAN_PWM_HSIOM);
if(Cy_GPIO_GetHSIOM(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN) != SEAT_FAN_PWM_HSIOM)
{
g_bspSeatFanStartFailStage = 3u;
BspSeatFan_FailSafeOff();
return false;
}
return true;
}
bool BspSeatFan_SetDutyPercent(uint32_t dutyPercent)
{
uint32_t compare;
if(dutyPercent > 100u)
{
return false;
}
compare = BspSeatFan_DutyToCompare(dutyPercent);
Cy_Tcpwm_Pwm_SetCompare0(SEAT_FAN_PWM_TCPWM, compare);
return (Cy_Tcpwm_Pwm_GetCompare0(SEAT_FAN_PWM_TCPWM) == compare);
}
void BspSeatFan_FailSafeOff(void)
{
Cy_Tcpwm_Pwm_Disable(SEAT_FAN_PWM_TCPWM);
BspSeatFan_SetPinGpioLow(SEAT_FAN_PWM_PORT,
SEAT_FAN_PWM_PIN,
SEAT_FAN_PWM_GPIO_HSIOM);
BspSeatFan_SetPinGpioLow(SEAT_FAN_UNUSED_PORT,
SEAT_FAN_UNUSED_PIN,
SEAT_FAN_UNUSED_GPIO_HSIOM);
}
bool BspSeatCan_Init(void)
{
cy_en_canfd_status_t canStatus;
if(!BspSeatCan_ConfigClock() || !BspSeatCan_ConfigPins())
{
g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_FAN_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_FAN_CAN_TYPE, &s_canConfig);
g_bspSeatCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatCan_Read(bsp_seat_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_canRxQueueTail;
if(tail == s_canRxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_canRxQueue[tail].identifier;
frame->data0 = s_canRxQueue[tail].data0;
frame->data1 = s_canRxQueue[tail].data1;
frame->dlc = s_canRxQueue[tail].dlc;
tail++;
if(tail >= SEAT_FAN_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_canRxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1)
{
cy_stc_canfd_msg_t message;
uint8_t bufferIndex;
uint8_t dataIndex;
if(identifier > 0x7FFu)
{
g_bspSeatCanLastTxStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_FAN_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(dataIndex = 2u;
dataIndex < CY_CANFD_MESSAGE_DATA_BUFFER_SIZE;
dataIndex++)
{
message.dataConfig.data[dataIndex] = 0u;
}
for(bufferIndex = 0u;
bufferIndex < SEAT_FAN_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_FAN_CAN_TYPE, bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_FAN_CAN_TYPE,
bufferIndex,
&message);
g_bspSeatCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatCanTxBusyCount++;
return false;
}
static bool BspSeatCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_FAN_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_FAN_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_FAN_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_FAN_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_FAN_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_FAN_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_FAN_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_FAN_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t transceiverConfig =
{
.outVal = SEAT_FAN_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_FAN_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_RX_PORT,
SEAT_FAN_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_TX_PORT,
SEAT_FAN_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN,
&transceiverConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN,
SEAT_FAN_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_FAN_CAN_TRANSCEIVER_PORT,
SEAT_FAN_CAN_TRANSCEIVER_PIN) ==
SEAT_FAN_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_FAN_CAN_TYPE);
}
static void BspSeatCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatCanRxCount++;
g_bspSeatCanLastRxId = message->idConfig.identifier;
head = s_canRxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_FAN_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_canRxQueueTail)
{
g_bspSeatCanRxQueueOverflowCount++;
return;
}
s_canRxQueue[head].identifier = message->idConfig.identifier;
s_canRxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_canRxQueue[head].data0 = message->dataConfig.data[0];
s_canRxQueue[head].data1 = message->dataConfig.data[1];
s_canRxQueueHead = nextHead;
}
static bool BspSeatFan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_FAN_COUNTER_CLOCK_HZ) ||
((periClockHz % SEAT_FAN_COUNTER_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_FAN_COUNTER_CLOCK_HZ;
if((divider == 0u) || (divider > 65536u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_FAN_PWM_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_FAN_PWM_PCLK,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_16_BIT,
SEAT_FAN_PWM_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static uint32_t BspSeatFan_DutyToCompare(uint32_t dutyPercent)
{
return (SEAT_FAN_PWM_PERIOD_TICKS * dutyPercent) / 100u;
}
static void BspSeatFan_SetPinGpioLow(volatile stc_GPIO_PRT_t *port,
uint32_t pin,
en_hsiom_sel_t gpioHsiom)
{
/* Set the latch low before changing HSIOM to avoid a transient high. */
Cy_GPIO_Write(port, pin, 0u);
Cy_GPIO_SetHSIOM(port, pin, gpioHsiom);
Cy_GPIO_SetDriveSel(port, pin, CY_GPIO_DRIVE_FULL);
Cy_GPIO_SetDrivemode(port, pin, CY_GPIO_DM_STRONG_IN_OFF);
Cy_GPIO_Write(port, pin, 0u);
}
@@ -0,0 +1,52 @@
#ifndef BSP_SEAT_FAN_H
#define BSP_SEAT_FAN_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_can_frame_t;
/* Configure P11.1 and P11.2 as strong GPIO outputs at logic low. */
bool BspSeatFan_ForceOff(void);
/* Configure the peripheral clock and PWM0-60 while P11.1 remains GPIO low. */
bool BspSeatFan_InitPwm(void);
/* Start PWM and only then connect P11.1 to the TCPWM line output. */
bool BspSeatFan_Start(uint32_t dutyPercent);
/* Change the active PWM compare value. Valid range is 0 through 100 percent. */
bool BspSeatFan_SetDutyPercent(uint32_t dutyPercent);
/* Disable TCPWM and restore both known fan-control pins to GPIO low. */
void BspSeatFan_FailSafeOff(void);
/* Read-only HIL diagnostic: 0=none, 1=compare, 2=counter, 3=HSIOM. */
extern volatile uint32_t g_bspSeatFanStartFailStage;
/* Initialize the proven CAN4/TJA1044 board interface at 500 kbit/s. */
bool BspSeatCan_Init(void);
/* Pop one received standard Classic CAN frame from the interrupt-safe queue. */
bool BspSeatCan_Read(bsp_seat_can_frame_t *frame);
/* Send one 8-byte standard Classic CAN frame. */
bool BspSeatCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1);
/* Watch-window diagnostics for hardware integration. */
extern volatile uint32_t g_bspSeatCanInitStatus;
extern volatile uint32_t g_bspSeatCanRxCount;
extern volatile uint32_t g_bspSeatCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatCanTxCount;
extern volatile uint32_t g_bspSeatCanTxBusyCount;
extern volatile uint32_t g_bspSeatCanLastRxId;
extern volatile uint32_t g_bspSeatCanLastTxId;
extern volatile uint32_t g_bspSeatCanLastTxStatus;
#endif /* BSP_SEAT_FAN_H */
@@ -0,0 +1,400 @@
#include "bsp_seat_move_can.h"
#include "cy_project.h"
#include "cy_device_headers.h"
#include "motor_control.h"
#include "seat_move_config.h"
static bool BspSeatMoveCan_ConfigClock(void);
static bool BspSeatMoveCan_ConfigPins(void);
static void BspSeatMoveCan_IrqHandler(void);
static void BspSeatMoveCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message);
static void BspSeatMoveCan_ErrorCallback(cy_en_canfd_bus_error_t error);
volatile uint32_t g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatMoveCanRxCount = 0u;
volatile uint32_t g_bspSeatMoveCanRxQueueOverflowCount = 0u;
volatile uint32_t g_bspSeatMoveCanTxCount = 0u;
volatile uint32_t g_bspSeatMoveCanTxBusyCount = 0u;
volatile uint32_t g_bspSeatMoveCanLastRxId = 0u;
volatile uint32_t g_bspSeatMoveCanLastTxId = 0u;
volatile uint32_t g_bspSeatMoveCanLastTxStatus = (uint32_t)CY_CANFD_SUCCESS;
volatile uint32_t g_bspSeatMoveCanBusOff = 0u;
static volatile bsp_seat_move_can_frame_t
s_rxQueue[SEAT_MOVE_CAN_RX_QUEUE_DEPTH];
static volatile uint8_t s_rxQueueHead = 0u;
static volatile uint8_t s_rxQueueTail = 0u;
static const cy_stc_id_filter_t s_standardIdFilters[] =
{
CANFD_CONFIG_STD_ID_FILTER_RANGE(
SEAT_MOVE_CAN_COMMAND_ID,
SEAT_MOVE_CAN_COMMAND_ID,
CY_CANFD_ID_FILTER_ELEMNT_CONFIG_STORE_RXFIFO0),
};
static cy_stc_sysint_irq_t s_canIrqConfig =
{
.sysIntSrc = SEAT_MOVE_CAN_IRQN,
.intIdx = SEAT_MOVE_CAN_CPU_IRQ,
.isEnabled = true,
};
static cy_stc_canfd_config_t s_canConfig =
{
.txCallback = NULL,
.rxCallback = BspSeatMoveCan_RxCallback,
.rxFifoWithTopCallback = NULL,
.statusCallback = NULL,
.errorCallback = BspSeatMoveCan_ErrorCallback,
.canFDMode = false,
.bitrate =
{
/* 40 MHz / (10 * (1 + 5 + 2)) = 500 kbit/s. */
.prescaler = 10u - 1u,
.timeSegment1 = 5u - 1u,
.timeSegment2 = 2u - 1u,
.syncJumpWidth = 2u - 1u,
},
.fastBitrate =
{
.prescaler = 0u,
.timeSegment1 = 0u,
.timeSegment2 = 0u,
.syncJumpWidth = 0u,
},
.tdcConfig =
{
.tdcEnabled = false,
.tdcOffset = 0u,
.tdcFilterWindow = 0u,
},
.sidFilterConfig =
{
.numberOfSIDFilters =
sizeof(s_standardIdFilters) / sizeof(s_standardIdFilters[0]),
.sidFilter = s_standardIdFilters,
},
.extidFilterConfig =
{
.numberOfEXTIDFilters = 0u,
.extidFilter = NULL,
.extIDANDMask = 0x1FFFFFFFu,
},
.globalFilterConfig =
{
.nonMatchingFramesStandard = CY_CANFD_REJECT_NON_MATCHING,
.nonMatchingFramesExtended = CY_CANFD_REJECT_NON_MATCHING,
.rejectRemoteFramesStandard = true,
.rejectRemoteFramesExtended = true,
},
.rxBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo1DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0DataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.txBufferDataSize = CY_CANFD_BUFFER_DATA_SIZE_8,
.rxFifo0Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = SEAT_MOVE_CAN_RX_FIFO0_ELEMENTS,
.topPointerLogicEnabled = false,
},
.rxFifo1Config =
{
.mode = CY_CANFD_FIFO_MODE_BLOCKING,
.watermark = 0u,
.numberOfFifoElements = 0u,
.topPointerLogicEnabled = false,
},
.noOfRxBuffers = 0u,
.noOfTxBuffers = SEAT_MOVE_CAN_TX_BUFFER_COUNT,
};
bool BspSeatMoveCan_Init(void)
{
cy_en_canfd_status_t canStatus;
s_rxQueueHead = 0u;
s_rxQueueTail = 0u;
g_bspSeatMoveCanBusOff = 0u;
if(!BspSeatMoveCan_ConfigClock() || !BspSeatMoveCan_ConfigPins())
{
g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
if(Cy_SysInt_InitIRQ(&s_canIrqConfig) != CY_SYSINT_SUCCESS)
{
g_bspSeatMoveCanInitStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
Cy_SysInt_SetSystemIrqVector(s_canIrqConfig.sysIntSrc,
BspSeatMoveCan_IrqHandler);
NVIC_SetPriority(s_canIrqConfig.intIdx, 0u);
NVIC_ClearPendingIRQ(s_canIrqConfig.intIdx);
(void)Cy_CANFD_DeInit(SEAT_MOVE_CAN_TYPE);
canStatus = Cy_CANFD_Init(SEAT_MOVE_CAN_TYPE, &s_canConfig);
g_bspSeatMoveCanInitStatus = (uint32_t)canStatus;
if(canStatus != CY_CANFD_SUCCESS)
{
return false;
}
NVIC_EnableIRQ(s_canIrqConfig.intIdx);
return true;
}
bool BspSeatMoveCan_Read(bsp_seat_move_can_frame_t *frame)
{
uint32_t interruptState;
uint8_t tail;
if(frame == NULL)
{
return false;
}
interruptState = Cy_SysLib_EnterCriticalSection();
tail = s_rxQueueTail;
if(tail == s_rxQueueHead)
{
Cy_SysLib_ExitCriticalSection(interruptState);
return false;
}
frame->identifier = s_rxQueue[tail].identifier;
frame->data0 = s_rxQueue[tail].data0;
frame->data1 = s_rxQueue[tail].data1;
frame->dlc = s_rxQueue[tail].dlc;
tail++;
if(tail >= SEAT_MOVE_CAN_RX_QUEUE_DEPTH)
{
tail = 0u;
}
s_rxQueueTail = tail;
Cy_SysLib_ExitCriticalSection(interruptState);
return true;
}
bool BspSeatMoveCan_Send(uint32_t identifier,
uint32_t data0,
uint32_t data1)
{
cy_stc_canfd_msg_t message = { 0 };
uint8_t bufferIndex;
if((identifier > 0x7FFu) || (g_bspSeatMoveCanBusOff != 0u))
{
g_bspSeatMoveCanLastTxStatus = (uint32_t)CY_CANFD_BAD_PARAM;
return false;
}
message.canFDFormat = false;
message.idConfig.extended = false;
message.idConfig.identifier = identifier;
message.dataConfig.dataLengthCode = SEAT_MOVE_CAN_DLC;
message.dataConfig.data[0] = data0;
message.dataConfig.data[1] = data1;
for(bufferIndex = 0u;
bufferIndex < SEAT_MOVE_CAN_TX_BUFFER_COUNT;
bufferIndex++)
{
cy_en_canfd_tx_buffer_status_t bufferStatus =
Cy_CANFD_GetTxBufferStatus(SEAT_MOVE_CAN_TYPE, bufferIndex);
if((bufferStatus != CY_CANFD_TX_BUFFER_PENDING) &&
(bufferStatus != CY_CANFD_TX_BUFFER_CANCEL_REQUESTED))
{
cy_en_canfd_status_t sendStatus;
g_bspSeatMoveCanLastTxId = identifier;
sendStatus = Cy_CANFD_UpdateAndTransmitMsgBuffer(
SEAT_MOVE_CAN_TYPE, bufferIndex, &message);
g_bspSeatMoveCanLastTxStatus = (uint32_t)sendStatus;
if(sendStatus == CY_CANFD_SUCCESS)
{
g_bspSeatMoveCanTxCount++;
return true;
}
return false;
}
}
g_bspSeatMoveCanTxBusyCount++;
return false;
}
static bool BspSeatMoveCan_ConfigClock(void)
{
uint32_t periClockHz = 0u;
uint32_t divider;
uint32_t clockGroup;
if(Cy_SysClk_GetClkPeriFrequency(&periClockHz) != CY_SYSCLK_SUCCESS)
{
return false;
}
if((periClockHz < SEAT_MOVE_CAN_CLOCK_HZ) ||
((periClockHz % SEAT_MOVE_CAN_CLOCK_HZ) != 0u))
{
return false;
}
divider = periClockHz / SEAT_MOVE_CAN_CLOCK_HZ;
if((divider == 0u) || (divider > 256u))
{
return false;
}
clockGroup = Cy_SysClk_GetClockGroup(SEAT_MOVE_CAN_PCLK);
if(Cy_SysClk_PeriphDisableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphAssignDivider(SEAT_MOVE_CAN_PCLK,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) !=
CY_SYSCLK_SUCCESS)
{
return false;
}
if(Cy_SysClk_PeriphSetDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER,
divider - 1u) != CY_SYSCLK_SUCCESS)
{
return false;
}
return (Cy_SysClk_PeriphEnableDivider(clockGroup,
CY_SYSCLK_DIV_8_BIT,
SEAT_MOVE_CAN_DIVIDER_NUMBER) ==
CY_SYSCLK_SUCCESS);
}
static bool BspSeatMoveCan_ConfigPins(void)
{
cy_stc_gpio_pin_config_t rxConfig =
{
.outVal = 0u,
.driveMode = CY_GPIO_DM_HIGHZ,
.hsiom = SEAT_MOVE_CAN_RX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t txConfig =
{
.outVal = 1u,
.driveMode = CY_GPIO_DM_STRONG,
.hsiom = SEAT_MOVE_CAN_TX_MUX,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = 0u,
};
cy_stc_gpio_pin_config_t enableConfig =
{
.outVal = SEAT_MOVE_CAN_TRANSCEIVER_ENABLE,
.driveMode = CY_GPIO_DM_STRONG_IN_OFF,
.hsiom = SEAT_MOVE_CAN_TRANSCEIVER_GPIO_HSIOM,
.intEdge = 0u,
.intMask = 0u,
.vtrip = 0u,
.slewRate = 0u,
.driveSel = CY_GPIO_DRIVE_FULL,
};
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_RX_PORT,
SEAT_MOVE_CAN_RX_PIN,
&rxConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_TX_PORT,
SEAT_MOVE_CAN_TX_PIN,
&txConfig) != CY_GPIO_SUCCESS)
{
return false;
}
if(Cy_GPIO_Pin_Init(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN,
&enableConfig) != CY_GPIO_SUCCESS)
{
return false;
}
Cy_GPIO_Write(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN,
SEAT_MOVE_CAN_TRANSCEIVER_ENABLE);
Cy_SysTick_DelayInUs(1000u);
return (Cy_GPIO_ReadOut(SEAT_MOVE_CAN_TRANSCEIVER_PORT,
SEAT_MOVE_CAN_TRANSCEIVER_PIN) ==
SEAT_MOVE_CAN_TRANSCEIVER_ENABLE);
}
static void BspSeatMoveCan_IrqHandler(void)
{
Cy_CANFD_IrqHandler(SEAT_MOVE_CAN_TYPE);
}
static void BspSeatMoveCan_RxCallback(bool rxFifoMessage,
uint8_t rxBufferOrFifo,
cy_stc_canfd_msg_t *message)
{
uint8_t head;
uint8_t nextHead;
(void)rxFifoMessage;
(void)rxBufferOrFifo;
if((message == NULL) || message->idConfig.extended || message->canFDFormat)
{
return;
}
g_bspSeatMoveCanRxCount++;
g_bspSeatMoveCanLastRxId = message->idConfig.identifier;
if((message->idConfig.identifier == SEAT_MOVE_CAN_COMMAND_ID) &&
(message->dataConfig.dataLengthCode == SEAT_MOVE_CAN_DLC) &&
(((uint8_t)(message->dataConfig.data[0] & 0xFFu)) == 0x09u) &&
(((uint8_t)((message->dataConfig.data[0] >> 8u) & 0xFFu)) == 0x10u) &&
(message->dataConfig.data[1] == 0u))
{
Motor_RequestAbortAllFromIsr(
(uint16_t)(message->dataConfig.data[0] >> 16u));
}
head = s_rxQueueHead;
nextHead = head + 1u;
if(nextHead >= SEAT_MOVE_CAN_RX_QUEUE_DEPTH)
{
nextHead = 0u;
}
if(nextHead == s_rxQueueTail)
{
g_bspSeatMoveCanRxQueueOverflowCount++;
return;
}
s_rxQueue[head].identifier = message->idConfig.identifier;
s_rxQueue[head].dlc = message->dataConfig.dataLengthCode;
s_rxQueue[head].data0 = message->dataConfig.data[0];
s_rxQueue[head].data1 = message->dataConfig.data[1];
s_rxQueueHead = nextHead;
}
static void BspSeatMoveCan_ErrorCallback(cy_en_canfd_bus_error_t error)
{
if(error == CY_CANFD_BUSOFF)
{
g_bspSeatMoveCanBusOff = 1u;
}
}
@@ -0,0 +1,29 @@
#ifndef BSP_SEAT_MOVE_CAN_H
#define BSP_SEAT_MOVE_CAN_H
#include <stdbool.h>
#include <stdint.h>
typedef struct
{
uint32_t identifier;
uint32_t data0;
uint32_t data1;
uint8_t dlc;
} bsp_seat_move_can_frame_t;
bool BspSeatMoveCan_Init(void);
bool BspSeatMoveCan_Read(bsp_seat_move_can_frame_t *frame);
bool BspSeatMoveCan_Send(uint32_t identifier, uint32_t data0, uint32_t data1);
extern volatile uint32_t g_bspSeatMoveCanInitStatus;
extern volatile uint32_t g_bspSeatMoveCanRxCount;
extern volatile uint32_t g_bspSeatMoveCanRxQueueOverflowCount;
extern volatile uint32_t g_bspSeatMoveCanTxCount;
extern volatile uint32_t g_bspSeatMoveCanTxBusyCount;
extern volatile uint32_t g_bspSeatMoveCanLastRxId;
extern volatile uint32_t g_bspSeatMoveCanLastTxId;
extern volatile uint32_t g_bspSeatMoveCanLastTxStatus;
extern volatile uint32_t g_bspSeatMoveCanBusOff;
#endif /* BSP_SEAT_MOVE_CAN_H */
@@ -0,0 +1,155 @@
#ifndef SEAT_BACKREST_HEATER_CONFIG_H
#define SEAT_BACKREST_HEATER_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* Schematic-confirmed backrest-heater path:
* P16.7 / KBJRD -> R126 (1 kOhm) -> BTS7010-1EPA IN -> backrest heater.
* The rev_d SDK maps P16.7 to TCPWM1 line 65.
*/
#define SEAT_BACKREST_HEATER_PWM_PORT GPIO_PRT16
#define SEAT_BACKREST_HEATER_PWM_PIN (7u)
#define SEAT_BACKREST_HEATER_PWM_GPIO_HSIOM P16_7_GPIO
#define SEAT_BACKREST_HEATER_PWM_HSIOM P16_7_TCPWM1_LINE65
#define SEAT_BACKREST_HEATER_PWM_TCPWM TCPWM1_GRP0_CNT65
#define SEAT_BACKREST_HEATER_PWM_PCLK PCLK_TCPWM1_CLOCKS65
#define SEAT_BACKREST_HEATER_PWM_DIVIDER_NUMBER (3u)
#define SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ (2000000u)
#define SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ (100u)
#define SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS \
(SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ / \
SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ)
/* Three open-loop power levels. Tune only after thermal validation. */
#define SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT (30u)
#define SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT (60u)
#define SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT (90u)
#define SEAT_BACKREST_HEATER_DEFAULT_LEVEL (0u)
#define SEAT_BACKREST_HEATER_TASK_PERIOD_US (1000u)
/*
* Do not sample at the 10 ms PWM period. Equal periods phase-lock the ADC
* to one PWM point and can report zero current continuously for a healthy
* low-duty heater. Seven milliseconds walks through every PWM phase while
* remaining slow enough for the existing SAR ADC service.
*/
#define SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS (7u)
#define SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS (3u)
#define SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS (600000u)
/*
* Vehicle CAN4 wiring proven by D:\wcl\can and Seat_Fan_demo:
* P3.7 RX, P3.6 TX, P3.1 high enables the board TJA1044 circuit.
*/
#define SEAT_BACKREST_HEATER_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_BACKREST_HEATER_CAN_RX_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_RX_PIN (7u)
#define SEAT_BACKREST_HEATER_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_BACKREST_HEATER_CAN_TX_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_TX_PIN (6u)
#define SEAT_BACKREST_HEATER_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_BACKREST_HEATER_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_BACKREST_HEATER_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_BACKREST_HEATER_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_BACKREST_HEATER_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_BACKREST_HEATER_CAN_DIVIDER_NUMBER (0u)
#define SEAT_BACKREST_HEATER_CAN_CLOCK_HZ (40000000u)
#define SEAT_BACKREST_HEATER_CAN_TX_BUFFER_COUNT (4u)
#define SEAT_BACKREST_HEATER_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH (8u)
/* Classic CAN matrix messages, 11-bit IDs, 8-byte payloads, Intel bit order. */
#define SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID (0x29Eu)
#define SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID (0x2AAu)
#define SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID (0x3CEu)
#define SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID (0x42Au)
#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID (0x4FAu)
#define SEAT_BACKREST_HEATER_CAN_DLC (8u)
#define SEAT_BACKREST_HEATER_CAN_FEEDBACK_PERIOD_MS (100u)
#define SEAT_BACKREST_HEATER_CAN_ACCEPT_42A_COMMAND (1u)
#define SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR (2u)
/* Safety watchdog and retained C-SPY session latch, derived from Seat_Fan_demo. */
#define SEAT_BACKREST_HEATER_WDT_TIMEOUT_TICKS (3200u)
#define SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX (3u)
#define SEAT_BACKREST_HEATER_DEBUG_ACTIVE_MAGIC (0x48444247u) /* "HDBG" */
#define SEAT_BACKREST_HEATER_DEBUG_EXIT_MAGIC (0x484F4646u) /* "HOFF" */
/*
* Schematic-confirmed diagnostic current-sense path:
* KBJRD-AD -> P12.2 / ADC[1]_6. The BTS7010 IS pin uses a 1.2 kOhm
* sense resistor and the schematic annotates K=5000. The calculated current
* is for bench visibility only; production thresholds require calibration.
*/
#define SEAT_BACKREST_HEATER_SENSE_ENABLE (1u)
#define SEAT_BACKREST_HEATER_SENSE_PORT GPIO_PRT12
#define SEAT_BACKREST_HEATER_SENSE_PIN (2u)
#define SEAT_BACKREST_HEATER_SENSE_GPIO_HSIOM P12_2_GPIO
#define SEAT_BACKREST_HEATER_SENSE_RESISTOR_OHM (1200u)
#define SEAT_BACKREST_HEATER_SENSE_KILIS (5000u)
/* BTS7010 control facts confirmed on the power-driver schematic page. */
#define SEAT_BACKREST_HEATER_BTS7010_IN_ACTIVE_HIGH (1u)
#define SEAT_BACKREST_HEATER_BTS7010_DEN_MCU_CONTROLLED (0u)
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat_Controller_demo requires CYT4BFBCJE rev_d headers."
#endif
#if ((SEAT_BACKREST_HEATER_COUNTER_CLOCK_HZ % \
SEAT_BACKREST_HEATER_PWM_FREQUENCY_HZ) != 0u)
#error "The heater counter clock must divide evenly by the PWM frequency."
#endif
#if ((SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS == 0u) || \
(SEAT_BACKREST_HEATER_PWM_PERIOD_TICKS > 65535u))
#error "The heater PWM period does not fit TCPWM1 group 0 counter 65."
#endif
#if ((SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT == 0u) || \
(SEAT_BACKREST_HEATER_LOW_DUTY_PERCENT >= \
SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT) || \
(SEAT_BACKREST_HEATER_MEDIUM_DUTY_PERCENT >= \
SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT) || \
(SEAT_BACKREST_HEATER_HIGH_DUTY_PERCENT > 100u))
#error "Heater duties must be ordered LOW < MEDIUM < HIGH <= 100."
#endif
#if (SEAT_BACKREST_HEATER_DEFAULT_LEVEL > 3u)
#error "Default heater level must be 0 (off) through 3 (high)."
#endif
#if ((SEAT_BACKREST_HEATER_CAN_CONTROL_29E_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_CONFIG_2AA_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_MODE_3CE_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_SWITCH_42A_ID > 0x7FFu) || \
(SEAT_BACKREST_HEATER_CAN_FEEDBACK_4FA_ID > 0x7FFu))
#error "Seat heating CAN IDs must be 11-bit standard identifiers."
#endif
#if (SEAT_BACKREST_HEATER_CAN_RX_QUEUE_DEPTH < 5u)
#error "The heater CAN receive queue must hold at least five frames."
#endif
#if ((SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR == 0u) || \
(SEAT_BACKREST_HEATER_DEFAULT_CAN_GEAR > 3u))
#error "Default heater CAN gear must be in the range 1 through 3."
#endif
#if ((SEAT_BACKREST_HEATER_SENSE_SAMPLE_MS == 0u) || \
(SEAT_BACKREST_HEATER_SENSE_MAX_ERRORS == 0u) || \
(SEAT_BACKREST_HEATER_MAX_ACTIVE_TIME_MS == 0u))
#error "Heater sample period and maximum active time must be non-zero."
#endif
#if (SEAT_BACKREST_HEATER_DEBUG_BREG_INDEX >= SRSS_BACKUP_NUM_BREG)
#error "The selected heater backup register is not present on this device."
#endif
#endif /* SEAT_BACKREST_HEATER_CONFIG_H */
@@ -0,0 +1,71 @@
#ifndef SEAT_CURRENT_ADC_CONFIG_H
#define SEAT_CURRENT_ADC_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* PASS0 SAR1 is shared by all seat-current measurements. The schematic
* connects VREFH and the DRV8718 AREF rail to filtered 5 V, so raw-to-voltage
* calculations must use 5000 mV rather than the GPIO-domain 3.3 V supply.
*
* Logical motor mapping:
* M0 horizontal : ZYTTKZDY-AD, P10.6, ADC[1]_2
* M1 seat pan : JXBKZDY-AD, P12.1, ADC[1]_5
* M2 backrest : ZYKBKZDY-AD, P10.7, ADC[1]_3
* M3 legrest : ZYGDKZDY-AD, P10.5, ADC[1]_1
* heater current : KBJRD-AD, P12.2, ADC[1]_6
* fan current : FSDY-AD, P12.6, ADC[1]_10
*/
#define SEAT_CURRENT_ADC_INSTANCE PASS0_SAR1
#define SEAT_CURRENT_ADC_PCLK PCLK_PASS0_CLOCK_SAR1
#define SEAT_CURRENT_ADC_DIVIDER_NUMBER (4u)
#define SEAT_CURRENT_ADC_REFERENCE_MV (5000u)
#define SEAT_CURRENT_ADC_FULL_SCALE (4095u)
#define SEAT_CURRENT_ADC_OPERATION_FREQ_MAX_HZ (13340000u)
#define SEAT_CURRENT_ADC_SAMPLE_TIME_MIN_NS (600ull)
#define SEAT_CURRENT_ADC_POLL_TIMEOUT (100000u)
#define SEAT_CURRENT_M0_PORT GPIO_PRT10
#define SEAT_CURRENT_M0_PIN (6u)
#define SEAT_CURRENT_M0_GPIO_HSIOM P10_6_GPIO
#define SEAT_CURRENT_M0_CHANNEL PASS0_SAR1_CH2
#define SEAT_CURRENT_M0_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN2
#define SEAT_CURRENT_M1_PORT GPIO_PRT12
#define SEAT_CURRENT_M1_PIN (1u)
#define SEAT_CURRENT_M1_GPIO_HSIOM P12_1_GPIO
#define SEAT_CURRENT_M1_CHANNEL PASS0_SAR1_CH5
#define SEAT_CURRENT_M1_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN5
#define SEAT_CURRENT_M2_PORT GPIO_PRT10
#define SEAT_CURRENT_M2_PIN (7u)
#define SEAT_CURRENT_M2_GPIO_HSIOM P10_7_GPIO
#define SEAT_CURRENT_M2_CHANNEL PASS0_SAR1_CH3
#define SEAT_CURRENT_M2_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN3
#define SEAT_CURRENT_M3_PORT GPIO_PRT10
#define SEAT_CURRENT_M3_PIN (5u)
#define SEAT_CURRENT_M3_GPIO_HSIOM P10_5_GPIO
#define SEAT_CURRENT_M3_CHANNEL PASS0_SAR1_CH1
#define SEAT_CURRENT_M3_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN1
#define SEAT_CURRENT_HEATER_PORT GPIO_PRT12
#define SEAT_CURRENT_HEATER_PIN (2u)
#define SEAT_CURRENT_HEATER_GPIO_HSIOM P12_2_GPIO
#define SEAT_CURRENT_HEATER_CHANNEL PASS0_SAR1_CH6
#define SEAT_CURRENT_HEATER_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN6
#define SEAT_CURRENT_FAN_PORT GPIO_PRT12
#define SEAT_CURRENT_FAN_PIN (6u)
#define SEAT_CURRENT_FAN_GPIO_HSIOM P12_6_GPIO
#define SEAT_CURRENT_FAN_CHANNEL PASS0_SAR1_CH10
#define SEAT_CURRENT_FAN_PIN_ADDRESS CY_ADC_PIN_ADDRESS_AN10
#define SEAT_CURRENT_ADC_PORT_ADDRESS CY_ADC_PORT_ADDRESS_SARMUX0
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat current ADC mapping requires CYT4BFBCJE rev_d headers."
#endif
#endif /* SEAT_CURRENT_ADC_CONFIG_H */
@@ -0,0 +1,139 @@
#ifndef SEAT_FAN_CONFIG_H
#define SEAT_FAN_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/*
* Backrest fan:
* P11.1 -> KBFSDY-PWM-IN -> BTS7030 IN0 -> backrest fan.
* The schematic name "PWM0-60" maps to TCPWM1 group 0 counter 60 in SDL.
*/
#define SEAT_FAN_PWM_PORT GPIO_PRT11
#define SEAT_FAN_PWM_PIN (1u)
#define SEAT_FAN_PWM_GPIO_HSIOM P11_1_GPIO
#define SEAT_FAN_PWM_HSIOM P11_1_TCPWM1_LINE60
#define SEAT_FAN_PWM_TCPWM TCPWM1_GRP0_CNT60
#define SEAT_FAN_PWM_PCLK PCLK_TCPWM1_CLOCKS60
/*
* Seat-cushion fan output is unused in this first version and must stay low:
* P11.2 -> ZDFSDY-PWM-IN.
*/
#define SEAT_FAN_UNUSED_PORT GPIO_PRT11
#define SEAT_FAN_UNUSED_PIN (2u)
#define SEAT_FAN_UNUSED_GPIO_HSIOM P11_2_GPIO
#define SEAT_FAN_PWM_DIVIDER_NUMBER (0u)
#define SEAT_FAN_COUNTER_CLOCK_HZ (2000000u)
#define SEAT_FAN_PWM_FREQUENCY_HZ (100u)
#define SEAT_FAN_PWM_PERIOD_TICKS \
(SEAT_FAN_COUNTER_CLOCK_HZ / SEAT_FAN_PWM_FREQUENCY_HZ)
#define SEAT_FAN_STARTUP_DUTY_PERCENT (100u)
#define SEAT_FAN_RUNNING_DUTY_PERCENT (70u)
#define SEAT_FAN_STARTUP_TIME_US (2000000u)
#define SEAT_FAN_DEBUG_POLL_TIME_US (1000u)
#define SEAT_FAN_WDT_TIMEOUT_TICKS (3200u)
/* CAN-controlled application timing and the three physical fan levels. */
#define SEAT_FAN_TASK_PERIOD_US (1000u)
#define SEAT_FAN_STARTUP_TIME_MS (SEAT_FAN_STARTUP_TIME_US / 1000u)
#define SEAT_FAN_GEAR1_DUTY_PERCENT (50u)
#define SEAT_FAN_GEAR2_DUTY_PERCENT (70u)
#define SEAT_FAN_GEAR3_DUTY_PERCENT (100u)
#define SEAT_FAN_DEFAULT_GEAR (2u)
/*
* Vehicle CAN4 wiring proven by D:\wcl\can:
* P3.7 RX, P3.6 TX, P3.1 high enables the board TJA1044 circuit.
*/
#define SEAT_FAN_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_FAN_CAN_RX_PORT GPIO_PRT3
#define SEAT_FAN_CAN_RX_PIN (7u)
#define SEAT_FAN_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_FAN_CAN_TX_PORT GPIO_PRT3
#define SEAT_FAN_CAN_TX_PIN (6u)
#define SEAT_FAN_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_FAN_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_FAN_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_FAN_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_FAN_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_FAN_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_FAN_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_FAN_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_FAN_CAN_DIVIDER_NUMBER (0u)
#define SEAT_FAN_CAN_CLOCK_HZ (40000000u)
#define SEAT_FAN_CAN_TX_BUFFER_COUNT (4u)
#define SEAT_FAN_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_FAN_CAN_RX_QUEUE_DEPTH (8u)
/* Classic CAN matrix messages, 11-bit IDs, 8-byte payloads, Intel bit order. */
#define SEAT_FAN_CAN_CONTROL_29E_ID (0x29Eu)
#define SEAT_FAN_CAN_CONFIG_2AA_ID (0x2AAu)
#define SEAT_FAN_CAN_SWITCH_42A_ID (0x42Au)
#define SEAT_FAN_CAN_FEEDBACK_4FA_ID (0x4FAu)
#define SEAT_FAN_CAN_DLC (8u)
#define SEAT_FAN_CAN_FEEDBACK_PERIOD_MS (100u)
#define SEAT_FAN_CAN_ACCEPT_42A_COMMAND (1u)
/* FSDY-AD is P12.6 / ADC[1]_10 on the shared seat-current ADC. */
#define SEAT_FAN_CURRENT_SAMPLE_MS (10u)
/*
* BREG3 retains the debug-session state through debug/XRES/WDT resets.
* DEBUG_ACTIVE is written by firmware as soon as it observes C_DEBUGEN, so a
* debugger-induced reset cannot accidentally restart the fan even if the
* C-SPY exit hook was skipped. DEBUG_EXIT is written when detach is observed
* or by the C-SPY exit hook. A genuine POR releases either marker.
*/
#define SEAT_FAN_DEBUG_SESSION_BREG_INDEX (3u)
#define SEAT_FAN_DEBUG_ACTIVE_MAGIC (0x44424741u) /* ASCII "DBGA" */
#define SEAT_FAN_DEBUG_EXIT_MAGIC (0x5346414Eu) /* ASCII "SFAN" */
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Seat_Controller_demo must be built for CYT4BFBCJE with the rev_d headers."
#endif
#if ((SEAT_FAN_COUNTER_CLOCK_HZ % SEAT_FAN_PWM_FREQUENCY_HZ) != 0u)
#error "The TCPWM counter clock must divide evenly by the requested PWM frequency."
#endif
#if ((SEAT_FAN_PWM_PERIOD_TICKS == 0u) || (SEAT_FAN_PWM_PERIOD_TICKS > 65535u))
#error "The PWM period does not fit TCPWM1 group 0 counter 60."
#endif
#if ((SEAT_FAN_STARTUP_TIME_US % SEAT_FAN_DEBUG_POLL_TIME_US) != 0u)
#error "The startup duration must be an integer number of debug polling periods."
#endif
#if ((SEAT_FAN_STARTUP_TIME_US % 1000u) != 0u)
#error "The fan startup time must be an integer number of milliseconds."
#endif
#if ((SEAT_FAN_GEAR1_DUTY_PERCENT > SEAT_FAN_GEAR2_DUTY_PERCENT) || \
(SEAT_FAN_GEAR2_DUTY_PERCENT > SEAT_FAN_GEAR3_DUTY_PERCENT) || \
(SEAT_FAN_GEAR3_DUTY_PERCENT > 100u))
#error "Fan gear duty percentages must be monotonic and no greater than 100."
#endif
#if ((SEAT_FAN_DEFAULT_GEAR == 0u) || (SEAT_FAN_DEFAULT_GEAR > 3u))
#error "The default physical fan gear must be in the range 1 through 3."
#endif
#if ((SEAT_FAN_CAN_CONTROL_29E_ID > 0x7FFu) || \
(SEAT_FAN_CAN_CONFIG_2AA_ID > 0x7FFu) || \
(SEAT_FAN_CAN_SWITCH_42A_ID > 0x7FFu) || \
(SEAT_FAN_CAN_FEEDBACK_4FA_ID > 0x7FFu))
#error "Seat ventilation CAN IDs must be 11-bit standard identifiers."
#endif
#if (SEAT_FAN_CAN_RX_QUEUE_DEPTH < 4u)
#error "The CAN receive queue must hold at least four frames."
#endif
#if (SEAT_FAN_DEBUG_SESSION_BREG_INDEX >= SRSS_BACKUP_NUM_BREG)
#error "The selected backup register is not present on this device."
#endif
#endif /* SEAT_FAN_CONFIG_H */
@@ -0,0 +1,150 @@
#ifndef SEAT_MOVE_CONFIG_H
#define SEAT_MOVE_CONFIG_H
#include "cy_project.h"
#include "cy_device_headers.h"
/* This configuration is intentionally tied to the timed-only IAR target. */
#if !defined(PASSENGER_SEAT_TIMED_ONLY)
#error "Build this source only from Passenger_Seat_Timed_Controller_demo."
#endif
#define SEAT_MOVE_MOTOR_COUNT (4u)
#define SEAT_MOVE_ACTIVE_MOTOR_MASK (0x0Fu)
#define SEAT_MOVE_TASK_PERIOD_US (1000u)
#define SEAT_MOVE_START_STAGGER_MS (100u)
#define SEAT_MOVE_MAX_CONCURRENT_MOTORS (4u)
/* Timed-only CAN limits. ParamA is duration in ms. */
#define SEAT_TIMED_MIN_DURATION_MS (300u)
#define SEAT_TIMED_M0_MAX_DURATION_MS (30000u) /* horizontal */
#define SEAT_TIMED_M1_MAX_DURATION_MS (20000u) /* seat-pan */
#define SEAT_TIMED_M2_MAX_DURATION_MS (30000u) /* backrest */
#define SEAT_TIMED_M3_MAX_DURATION_MS (25000u) /* legrest */
#define SEAT_TIMED_LEVEL_MIN_X10 (300u)
#define SEAT_TIMED_LEVEL_MAX_X10 (1000u)
/* Logical MotorID and connected DRV8718 channel are identical. */
#define SEAT_MOVE_LOGICAL_M0_PHYSICAL_MODE (0u)
#define SEAT_MOVE_LOGICAL_M1_PHYSICAL_MODE (1u)
#define SEAT_MOVE_LOGICAL_M2_PHYSICAL_MODE (2u)
#define SEAT_MOVE_LOGICAL_M3_PHYSICAL_MODE (3u)
/* Retained by the verified motor manager; timed requests provide duty. */
#define SEAT_MOVE_M0_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M1_DEFAULT_TIMEOUT_MS (8000u)
#define SEAT_MOVE_M2_DEFAULT_TIMEOUT_MS (12000u)
#define SEAT_MOVE_M3_DEFAULT_TIMEOUT_MS (30000u)
#define SEAT_MOVE_M0_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M1_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M2_DEFAULT_DUTY_PERCENT (90u)
#define SEAT_MOVE_M3_DEFAULT_DUTY_PERCENT (50u)
#define SEAT_MOVE_M3_BREAKAWAY_DUTY_PERCENT (50u)
#define SEAT_MOVE_POSITION_TARGET_HALL_TOLERANCE (4u)
/* CAN4, classic CAN, 500 kbit/s. */
#define SEAT_MOVE_CAN_TYPE CY_CANFD1_2_TYPE
#define SEAT_MOVE_CAN_RX_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_RX_PIN (7u)
#define SEAT_MOVE_CAN_RX_MUX P3_7_CANFD1_TTCAN_RX2
#define SEAT_MOVE_CAN_TX_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_TX_PIN (6u)
#define SEAT_MOVE_CAN_TX_MUX P3_6_CANFD1_TTCAN_TX2
#define SEAT_MOVE_CAN_TRANSCEIVER_PORT GPIO_PRT3
#define SEAT_MOVE_CAN_TRANSCEIVER_PIN (1u)
#define SEAT_MOVE_CAN_TRANSCEIVER_GPIO_HSIOM P3_1_GPIO
#define SEAT_MOVE_CAN_TRANSCEIVER_ENABLE (1u)
#define SEAT_MOVE_CAN_PCLK PCLK_CANFD1_CLOCK_CAN2
#define SEAT_MOVE_CAN_IRQN canfd_1_interrupts0_2_IRQn
#define SEAT_MOVE_CAN_CPU_IRQ CPUIntIdx2_IRQn
#define SEAT_MOVE_CAN_DIVIDER_NUMBER (0u)
#define SEAT_MOVE_CAN_CLOCK_HZ (40000000u)
#define SEAT_MOVE_CAN_TX_BUFFER_COUNT (8u)
#define SEAT_MOVE_CAN_RX_FIFO0_ELEMENTS (8u)
#define SEAT_MOVE_CAN_RX_QUEUE_DEPTH (16u)
#define SEAT_MOVE_CAN_DLC (8u)
#define SEAT_MOVE_CAN_PROTOCOL_VERSION (1u)
#define SEAT_MOVE_CAN_COMMAND_ID (0x300u)
#define SEAT_MOVE_CAN_SYSTEM_STATUS_ID (0x301u)
#define SEAT_MOVE_CAN_MOTOR_STATUS_ID (0x302u)
#define SEAT_MOVE_CAN_DIAGNOSTIC_ID (0x303u)
#define SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID (0x304u)
#define SEAT_MOVE_CAN_FIRMWARE_ID_ID (0x307u)
#define SEAT_MOVE_FIRMWARE_REVISION (15u)
#define SEAT_MOVE_FIRMWARE_BUILD_ID (17u)
#define SEAT_MOVE_SYSTEM_STATUS_PERIOD_MS (100u)
#define SEAT_MOVE_MOTOR_STATUS_POLL_MS (25u)
#define SEAT_MOVE_DIAGNOSTIC_POLL_MS (25u)
#define SEAT_MOVE_FIRMWARE_ID_PERIOD_MS (500u)
/* Current-aware stall detection remains active for timed movement. */
#define SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS (10u)
#define SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS (100u)
#define SEAT_MOVE_MOTOR_CURRENT_BASELINE_FILTER_SHIFT (3u)
#define SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS (200u)
#define SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS (5000u)
#define SEAT_MOVE_M0_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M1_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M2_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_M3_STALL_CURRENT_DELTA_RAW (500u)
#define SEAT_MOVE_MOTOR_CURRENT_MAX_ERRORS (3u)
/* Debug-detach and watchdog safety. HIL registers remain internal to the
* reused safety manager; this image exposes no HIL CAN command/filter. */
#define SEAT_MOVE_DEBUG_SESSION_BREG_INDEX (3u)
#define SEAT_MOVE_DEBUG_ACTIVE_MAGIC (0x4D4F5641u)
#define SEAT_MOVE_DEBUG_EXIT_MAGIC (0x4D4F5645u)
#define SEAT_MOVE_WDT_TIMEOUT_TICKS (3200u)
#define SEAT_MOVE_HIL_WDT_MAGIC_BREG_INDEX (0u)
#define SEAT_MOVE_HIL_WDT_TOKEN_BREG_INDEX (1u)
#define SEAT_MOVE_HIL_WDT_CHECK_BREG_INDEX (2u)
#define SEAT_MOVE_HIL_WDT_MARKER (0x48494C36u)
#if !defined(CYT4BFBCJE) || !defined(CY_MCU_rev_d)
#error "Timed passenger-seat firmware requires CYT4BFBCJE rev_d."
#endif
#if (SEAT_MOVE_ACTIVE_MOTOR_MASK != 0x0Fu)
#error "All four passenger-seat motor outputs must be enabled."
#endif
#if (SEAT_MOVE_MAX_CONCURRENT_MOTORS == 0u) || \
(SEAT_MOVE_MAX_CONCURRENT_MOTORS > SEAT_MOVE_MOTOR_COUNT)
#error "Concurrent motor budget must be between one and four."
#endif
#if ((SEAT_TIMED_MIN_DURATION_MS == 0u) || \
(SEAT_TIMED_M0_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M1_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M2_MAX_DURATION_MS > 65535u) || \
(SEAT_TIMED_M3_MAX_DURATION_MS > 65535u))
#error "Timed-command duration limits must fit ParamA."
#endif
#if ((SEAT_MOVE_CAN_COMMAND_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_SYSTEM_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_MOTOR_STATUS_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_CURRENT_DIAGNOSTIC_ID > 0x7FFu) || \
(SEAT_MOVE_CAN_FIRMWARE_ID_ID > 0x7FFu))
#error "Seat movement CAN identifiers must be 11-bit standard IDs."
#endif
#if (SEAT_MOVE_FIRMWARE_REVISION > 0x0Fu) || \
(SEAT_MOVE_FIRMWARE_BUILD_ID > 0xFFu)
#error "Firmware identity fields exceed their CAN encoding."
#endif
#if ((SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS == 0u) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_AGE_MS < \
SEAT_MOVE_MOTOR_CURRENT_SAMPLE_MS) || \
(SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS == 0u) || \
(SEAT_MOVE_MOTOR_NO_MOTION_TIMEOUT_MS <= \
SEAT_MOVE_MOTOR_STALL_CURRENT_CONFIRM_MS) || \
(SEAT_MOVE_MOTOR_CURRENT_MAX_ERRORS == 0u))
#error "Motor current/stall timing configuration is invalid."
#endif
#endif /* SEAT_MOVE_CONFIG_H */
@@ -0,0 +1,25 @@
# Build information — 2026-09-16
- Project: `Passenger_Seat_Timed_Controller_demo.ewp`
- Configuration: `rev_d`
- IAR command-line builder: 9.2.2.10770
- IAR ARM compiler/linker: 9.40.1.364
- Result: 0 errors, 0 warnings
- Read-only code: 40,626 bytes
- Read-only data: 1,977 bytes
- Read-write data: 6,182 bytes
Artifacts:
| File | SHA-256 |
|---|---|
| `IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.hex` | `7B47002B7B1A882A90717CE2727D60349B6C58CF0C1E95E48094C85EDB376CDC` |
| `IAR/rev_d/Exe/Passenger_Seat_Timed_Controller_demo.out` | `B0C740CB2BE02008BC03803B0A3422D817ABD1CF2BF977156294FB04BBDFAB7F` |
The linker map contains `SeatMotor_MoveTimedDuty` as the sole linked
`SeatMotor_Move*`/calibration/homing entry point. No dynamic-mode, position
store, or thermal-controller application symbol is linked. The HEX contains
the `PST1`, revision 15, build 17 identity payload.
This result is a compile/link verification only. No target was flashed and no
physical actuator movement was performed during this build.
@@ -0,0 +1,88 @@
# 副驾座椅四轴定时控制固件
本目录是从 `Seat_Controller_demo` 独立出的副驾定时控制版本。原工程保持不变;本工程的 CAN 应用层只允许四轴定时运动,不提供位置、相对位移、回零、标定、动态模式或热控能力。
## IAR 工程
- 工程文件:`IAR/Passenger_Seat_Timed_Controller_demo.ewp`
- 目标器件:Infineon TRAVEO T2G `CYT4BFBCJE`,`rev_d`
- IAR 工程宏:`PASSENGER_SEAT_TIMED_ONLY=1`
- HEX 输出:`Passenger_Seat_Timed_Controller_demo.hex`
- 固件身份帧:CAN `0x307`,字节 0..3 为 ASCII `PST1`,Build ID 为 `17`
工程复用原固件已验证的 DRV8718、Hall、电流检测、堵转/端点保护、看门狗和调试器退出安全逻辑。加热和风扇应用没有链接进固件;其 BSP 只用于启动及异常路径中强制输出关闭。
## 四轴能力
| MotorID | 机构 | 最短时间 | 最长时间 | 实际占空比限制 |
|---:|---|---:|---:|---:|
| 0 | 水平前后 | 300 ms | 30000 ms | 50%..95% |
| 1 | 坐垫升降 | 300 ms | 20000 ms | 50%..95% |
| 2 | 靠背角度 | 300 ms | 30000 ms | 50%..95% |
| 3 | 腿托升降 | 300 ms | 25000 ms | 固定上限 50% |
四轴可并发,启动间隔至少 100 ms。同一轴收到更新的定时命令时,先停止该轴,再执行新命令;不影响其他轴。定时运动不依赖绝对位置有效性,但端点、堵转、电流和驱动故障保护始终生效。
## CAN 命令
CAN4 使用经典 CAN、标准 11 位 ID、500 kbit/s。接收过滤器只接受 `0x300`,DLC 必须为 8。
`0x300` 的 8 字节按小端编码:
| 字节 | 字段 |
|---|---|
| B0 | Opcode |
| B1 | Control:bit1:0 MotorID,bit3:2 Direction,bit5:4 ProtocolVersion=1,bit7:6=0 |
| B3:B2 | Sequence,uint16 |
| B5:B4 | ParamA,uint16 |
| B7:B6 | ParamB,uint16 |
支持的 Opcode:
| Opcode | 功能 | Direction | ParamA | ParamB |
|---:|---|---|---|---|
| `0x03` | 定时运动 | `1` 向远端,`2` 向近端 | 时间,ms | 请求占空比 ×10,范围 300..1000 |
| `0x08` | 停止单轴 | `0` | `0` | `0` |
| `0x09` | 停止全部 | `0`,MotorID=`0` | `0` | `0` |
| `0x0C` | 查询单轴状态 | `0` | `0` | `0` |
| `0x0D` | 清除单轴任务故障 | `0` | `0xC33C` | `0` |
请求占空比会被安全范围约束:M0..M2 最终为 50%..95%,M3 不超过 50%。`STOP_ALL` 不受序列号新旧限制,可用于随时重新同步和急停;其他命令要求 16 位 Sequence 按环形序列递增,完全相同的重复帧返回 Duplicate。
以下运动 Opcode 固定返回 `BAD_OPCODE(3)`:
- `0x01` 绝对位置
- `0x02` 相对位置
- `0x0A` 行程标定
- `0x0B` 已知行程回零
- `0x0E` 启动动态模式
- `0x0F` 停止动态模式
热控和 HIL 自愈帧不在硬件接收过滤器中,`0x306` 动态模式状态也不再发送。
## 状态反馈
保留与原座椅协议兼容的诊断帧:
- `0x301`:系统状态、ACK、拒绝原因、活动/故障轴掩码
- `0x302`:单轴任务状态;定时运行状态值为 `3`
- `0x303`:Hall、速度和故障诊断
- `0x304`:电流诊断
- `0x307`:固件身份 `PST1`
`0x301` 中动态模式位和标定位始终为 0。位置有效位及百分比只作为 Hall 诊断信息返回,不用于目标位置控制。
## 构建与校验
1. 用 IAR Embedded Workbench for Arm 打开 `IAR/Passenger_Seat_Timed_Controller_demo.ewp`。
2. 选择 `rev_d` 配置并 Build。
3. 烧录前通过 `0x307` 确认目标固件身份为 `PST1`。
4. 烧录和台架运动测试期间保证座椅机构无人员/障碍物,先验证 `STOP_ALL`,再逐轴低时长测试。
仓库静态校验:
```text
python firmware/t2g/Passenger_Seat_Timed_Controller_demo/Tests/audit_timed_only.py
```
该脚本检查 IAR XML、工程引用、定时版编译宏、CAN 接收过滤器、允许的 Opcode,以及位置/动态/热控模块未被链接。
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,277 @@
#ifndef MOTOR_CONTROL_H
#define MOTOR_CONTROL_H
#include <stdbool.h>
#include <stdint.h>
#define MOTOR_API_SUCCESS (0u)
#define MOTOR_API_FAILURE (1u)
#define MOTOR_PHYSICAL_VALUE_SCALE (100ul)
/*
* 2026-07-30 endpoint-to-endpoint range calibration. Bench verification
* established identity mapping: logical M0..M3 use physical mode 0..3.
*/
#define MOTOR_MODE0_DEFAULT_MAX_HALL_COUNT (480ul) /* horizontal */
#define MOTOR_MODE0_MAX_POSITION_MM_X100 (21500ul)
#define MOTOR_MODE1_DEFAULT_MAX_HALL_COUNT (374ul) /* seat pan */
#define MOTOR_MODE1_MAX_VERTICAL_MM_X100 (5600ul)
#define MOTOR_MODE1_MAX_HORIZONTAL_MM_X100 (5100ul)
/* Compatibility name used by the generic scalar position API. */
#define MOTOR_MODE1_MAX_ANGLE_DEG_X100 \
MOTOR_MODE1_MAX_VERTICAL_MM_X100
#define MOTOR_MODE2_DEFAULT_MAX_HALL_COUNT (917ul) /* backrest */
#define MOTOR_MODE2_MAX_ANGLE_DEG_X100 (6800ul)
#define MOTOR_MODE3_DEFAULT_MAX_HALL_COUNT (647ul) /* legrest */
#define MOTOR_MODE3_MAX_ANGLE_DEG_X100 (7200ul)
/* Compatibility name used by the generic scalar position API. */
#define MOTOR_MODE3_MAX_POSITION_MM_X100 \
MOTOR_MODE3_MAX_ANGLE_DEG_X100
#define MOTOR_SPEED_VALUE_SCALE (100ul)
#define MOTOR_SPEED_MIN_DUTY_PERCENT (50u)
#define MOTOR_SPEED_MAX_DUTY_PERCENT (95u)
#define MOTOR_SPEED_DUTY_STEP_PERCENT (5u)
#define MOTOR_MODE0_MAX_SPEED_PPS_X100 (8600ul)
#define MOTOR_MODE1_MAX_SPEED_PPS_X100 (6100ul)
#define MOTOR_MODE2_MAX_SPEED_PPS_X100 (9000ul)
#define MOTOR_MODE3_MAX_SPEED_PPS_X100 (8600ul)
typedef enum
{
MOTOR_MODE_0_LINEAR = 0,
MOTOR_MODE_1_ANGLE,
MOTOR_MODE_2_ANGLE,
MOTOR_MODE_3_LINEAR_TRACK,
MOTOR_MODE_COUNT
} motor_mode_t;
typedef enum
{
MOTOR_DIRECTION_STOPPED = 0,
MOTOR_DIRECTION_TOWARD_FAR,
MOTOR_DIRECTION_TOWARD_NEAR
} motor_direction_t;
typedef enum
{
MOTOR_EVENT_NONE = 0,
MOTOR_EVENT_TARGET_REACHED,
MOTOR_EVENT_NEAR_ENDPOINT,
MOTOR_EVENT_FAR_ENDPOINT,
MOTOR_EVENT_STALL,
MOTOR_EVENT_TIMEOUT,
MOTOR_EVENT_ABORTED,
MOTOR_EVENT_DRIVER_FAULT,
MOTOR_EVENT_CALIBRATION_FAILED
} motor_event_t;
typedef enum
{
MOTOR_CALIBRATE_FIND_NEAR = 0,
MOTOR_CALIBRATE_FIND_FAR,
MOTOR_CALIBRATE_FIND_BOTH
} motor_calibration_action_t;
typedef struct
{
motor_mode_t mode;
bool positionValid;
motor_direction_t direction;
motor_event_t lastEvent;
uint32_t hallCount;
uint32_t maxHallCount;
uint32_t valueX100;
uint32_t maxValueX100;
uint32_t positionMmX100;
uint32_t horizontalMmX100;
uint32_t verticalMmX100;
uint32_t angleDegX100;
uint32_t percentX100;
} motor_position_info_t;
typedef struct
{
bool reverseAtEndpoint;
bool reverseAtStall;
} motor_safety_options_t;
typedef struct
{
bool nearEndpointFound;
bool farEndpointFound;
uint32_t measuredMaxHallCount;
} motor_calibration_result_t;
typedef struct
{
motor_mode_t mode;
bool enabled;
uint32_t durationMs;
uint32_t travelValueX100;
uint32_t plannedHallPulses;
uint32_t targetPulsesPerSecondX100;
uint32_t actualPulsesPerSecondX100;
uint8_t initialDutyPercent;
uint8_t currentDutyPercent;
} motor_speed_info_t;
typedef struct
{
motor_mode_t mode;
bool sampleValid;
bool baselineValid;
bool highCurrent;
bool hallActivityLow;
bool calibrating;
uint16_t raw;
uint16_t baselineRaw;
uint16_t deltaRaw;
uint16_t stallThresholdRaw;
uint32_t sampleAgeMs;
} motor_current_info_t;
typedef struct
{
uint32_t positionHallCount;
uint32_t maxHallCount;
uint32_t maxSpeedPulsesPerSecondX100;
uint32_t rawRisingEdges;
bool positionValid;
} motor_persistent_state_t;
/* Hardware initialization. SystemInit() and global IRQ enable remain owned by main. */
uint8_t Motor_Init(void);
/* Immediate GPIO-only safe state, also valid before Motor_Init(). */
void Motor_ForceSafeOff(void);
/* The Hall timebase is a precise 2 ms hardware interrupt. */
uint32_t Motor_GetTimeMs(void);
void Motor_SetWatchdogServiceEnabled(bool enabled);
/*
* Physical values use fixed point x100: 215.00 mm is 21500 and
* 68.00 degrees is 6800. positionValid is restored from a verified clean
* record or becomes true after a known endpoint has been found.
*
* Ownership contract: call all control APIs and Motor_ServiceAll from one
* main-loop/owner context. They are not ISR-safe, thread-safe, or reentrant.
* The four motors may still run concurrently after sequential non-blocking
* start calls from that owner.
*/
uint8_t Motor_GetPositionInfo(motor_mode_t mode, motor_position_info_t *info);
uint8_t Motor_GetCurrentInfo(motor_mode_t mode, motor_current_info_t *info);
/* Each mode has independent endpoint and stall reverse switches. */
uint8_t Motor_SetSafetyOptions(motor_mode_t mode,
const motor_safety_options_t *options);
/* Open-loop primitives used by the standardized application controller. */
uint8_t Motor_SetFixedDutyPercent(motor_mode_t mode, uint8_t dutyPercent);
uint8_t Motor_StartOpenLoop(motor_mode_t mode, motor_direction_t direction);
bool Motor_IsMoving(motor_mode_t mode);
/* Complete a commanded stop plus bounded Hall-coast tracking before a
* safety decision that requires the mechanism to be physically settled. */
bool Motor_WaitUntilStopped(motor_mode_t mode);
/*
* Restore/store Hall-domain state. These calls are accepted only while the
* selected motor is stopped. max speed uses Hall rising edges/s x100.
*/
uint8_t Motor_ValidatePersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_ApplyPersistentState(motor_mode_t mode,
const motor_persistent_state_t *state);
uint8_t Motor_GetPersistentState(motor_mode_t mode,
motor_persistent_state_t *state);
/* Bench HIL support: invalidate only the stopped axis' absolute-position
* confidence while preserving its measured Hall count and confirmed range.
* Production command handling never calls this API. */
uint8_t Motor_InvalidatePosition(motor_mode_t mode);
uint8_t Motor_SetMaximumSpeedPpsX100(motor_mode_t mode,
uint32_t maxSpeedPpsX100);
/*
* Pair an owner-context command with cancellation published asynchronously.
* An acknowledge succeeds when no cancellation is pending, or when sequence
* exactly matches it. It fails if a newer cancellation has already arrived.
*/
uint8_t Motor_AcknowledgeCommand(motor_mode_t mode, uint16_t sequence);
bool Motor_IsCommandCurrent(motor_mode_t mode, uint16_t sequence);
/*
* Start is non-blocking and permits all four modes to move concurrently.
* Wait and MoveToValue return 0 only after the target is reached; endpoint,
* stall, invalid position, invalid target, and timeout return 1.
* Replacing a target still ahead in the same direction does not restart PWM
* or refresh safety/timeout clocks. A direction change waits at least 100 ms
* and until Hall has been quiet for 100 ms. Crossing a target stops PWM once;
* residual Hall pulses remain in the global position and may leave the final
* count beyond the target. No automatic reverse correction is performed.
* If a safety reverse is enabled, Wait remains blocked until that reverse has
* stopped at its next safety event or at the absolute operation timeout.
*/
uint8_t Motor_StartMoveToValue(motor_mode_t mode, uint32_t targetValueX100);
uint8_t Motor_WaitForMove(motor_mode_t mode);
uint8_t Motor_MoveToValue(motor_mode_t mode, uint32_t targetValueX100);
uint8_t Motor_StopMode(motor_mode_t mode);
/*
* Configure closed-loop speed from a planned travel amount and duration.
* durationMs is milliseconds. travelValueX100 is mm x100 for mode 0 and
* degrees x100 for modes 1/2, and mm x100 for mode 3. This configures speed
* only; use an existing move API to start motion. After each real PWM start
* the controller waits
* 500 ms, then evaluates a 500 ms rolling Hall window every 100 ms. A speed
* error outside +/-10 percent changes that motor's duty by one 5 percent step.
*/
uint8_t Motor_SetSpeedPlan(motor_mode_t mode,
uint32_t durationMs,
uint32_t travelValueX100);
uint8_t Motor_DisableSpeedControl(motor_mode_t mode);
uint8_t Motor_GetSpeedInfo(motor_mode_t mode, motor_speed_info_t *info);
/*
* Calibration is blocking for the selected mode; other active modes continue.
* A new range is committed only after validation. Failure restores the old
* range, preserves observed motion, and invalidates absolute position because
* physical travel cannot be rolled back. Each leg requires Hall coast
* tracking to settle before/after travel; quiet wait is bounded to five seconds.
* legTimeoutMs is the independent motion timeout applied to each endpoint leg.
*/
uint8_t Motor_Calibrate(motor_mode_t mode,
motor_calibration_action_t action,
uint32_t legTimeoutMs,
motor_calibration_result_t *result);
/* Application override used to keep CAN/status/safety services alive while
* the selected motor executes the blocking endpoint-calibration primitive. */
void Motor_CalibrationServiceHook(void);
/* Call continuously from the main loop; the ISR still kills PWM immediately. */
void Motor_ServiceAll(void);
/* ISR-safe immediate PWM kill; bridge Hi-Z is completed by Motor_ServiceAll. */
void Motor_RequestAbortMaskFromIsr(uint8_t motorMask, uint16_t sequence);
void Motor_RequestAbortAllFromIsr(uint16_t sequence);
/* Irreversible until reset; unlike STOP, no sequence acknowledge can clear it. */
void Motor_LatchSafetyInhibitFromIsr(uint16_t sequence);
void Motor_StopAll(void);
/* A latched driver fault requires a power cycle before motion is accepted. */
bool Motor_IsDriverFaultLatched(void);
uint8_t Motor0_GetPosition(motor_position_info_t *info);
uint8_t Motor1_GetPosition(motor_position_info_t *info);
uint8_t Motor2_GetAngle(motor_position_info_t *info);
uint8_t Motor3_GetAngle(motor_position_info_t *info);
uint8_t Motor0_MoveToPositionMmX100(uint32_t positionMmX100);
uint8_t Motor1_MoveToVerticalMmX100(uint32_t verticalMmX100);
uint8_t Motor2_MoveToAngleDegX100(uint32_t angleDegX100);
uint8_t Motor3_MoveToAngleDegX100(uint32_t angleDegX100);
uint8_t Motor0_SetSpeedPlan(uint32_t durationMs, uint32_t distanceMmX100);
uint8_t Motor1_SetSpeedPlan(uint32_t durationMs, uint32_t verticalMmX100);
uint8_t Motor2_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100);
uint8_t Motor3_SetSpeedPlan(uint32_t durationMs, uint32_t angleDegX100);
#endif /* MOTOR_CONTROL_H */
@@ -0,0 +1,185 @@
/* C-SPY host-side safety hook for the timed-only passenger-seat controller. */
execUserSetup()
{
/* A new Download and Debug session releases a previous debug-exit latch. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
__message "Passenger_Seat_Timed_Controller_demo: C-SPY setup cleared the debug-session latch.\n";
}
execUserExit()
{
__var value;
__var safe;
/* Retain safe-off across debug/XRES/WDT resets. */
__writeMemory32(0x4D4F5645, 0x4027100C, "Memory");
/* Assert DRVOFF=P8.1 before touching PWM or pin muxing. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Disable TCPWM1 group-0 counters IN1/IN2/IN3/IN4 and Hall timebase. */
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
/* Disable fan TCPWM60 and heater TCPWM65 before changing their muxes. */
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
/* Force P11.1/P11.2 fan paths and P16.7 heater path to GPIO low. */
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
value = __readMemory32(0x403000B0, "Memory");
__writeMemory32(value & 0xFFE0E0FF, 0x403000B0, "Memory");
value = __readMemory32(0x40300104, "Memory");
__writeMemory32(value & 0xE0FFFFFF, 0x40300104, "Memory");
value = __readMemory32(0x403105C4, "Memory");
__writeMemory32((value & 0xFFFFF00F) | 0x00000660,
0x403105C4, "Memory");
value = __readMemory32(0x40310844, "Memory");
__writeMemory32((value & 0x0FFFFFFF) | 0x60000000,
0x40310844, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* Preload IN1=P1.0, IN2/IN3=P5.4/P5.5 and IN4=P8.0 low. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
/* Select GPIO for every DRV8718 PWM input. */
value = __readMemory32(0x40300010, "Memory");
__writeMemory32(value & 0xFFFFFFE0, 0x40300010, "Memory");
value = __readMemory32(0x40300054, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300054, "Memory");
value = __readMemory32(0x40300080, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300080, "Memory");
/* Strong output, input buffer off (CY_GPIO_DM_STRONG_IN_OFF = 0x6). */
value = __readMemory32(0x403100C4, "Memory");
__writeMemory32((value & 0xFFFFFFF0) | 0x00000006,
0x403100C4, "Memory");
value = __readMemory32(0x403102C4, "Memory");
__writeMemory32((value & 0xFF00FFFF) | 0x00660000,
0x403102C4, "Memory");
value = __readMemory32(0x40310444, "Memory");
__writeMemory32((value & 0xFFFFFF00) | 0x00000066,
0x40310444, "Memory");
/* Full drive strength for the same output pins. */
value = __readMemory32(0x403100CC, "Memory");
__writeMemory32(value & 0xFFFCFFFF, 0x403100CC, "Memory");
value = __readMemory32(0x403102CC, "Memory");
__writeMemory32(value & 0xF0FFFFFF, 0x403102CC, "Memory");
value = __readMemory32(0x4031044C, "Memory");
__writeMemory32(value & 0xFFF0FFFF, 0x4031044C, "Memory");
/* Reassert the final physical levels after mux/drive configuration. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Verify the retained latch, every PWM counter, every PWM HSIOM field and
every physical output latch before C-SPY releases the target. */
safe = 1;
value = __readMemory32(0x4027100C, "Memory");
if(value != 0x4D4F5645)
{
safe = 0;
}
value = __readMemory32(0x40580600, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580680, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580700, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580980, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580A00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40581E00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40582080, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40310580, "Memory");
if(value & 0x00000006)
{
safe = 0;
}
value = __readMemory32(0x40310800, "Memory");
if(value & 0x00000080)
{
safe = 0;
}
value = __readMemory32(0x40300010, "Memory");
if(value & 0x0000001F)
{
safe = 0;
}
value = __readMemory32(0x40300054, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40300080, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40310080, "Memory");
if(value & 0x00000001)
{
safe = 0;
}
value = __readMemory32(0x40310280, "Memory");
if(value & 0x00000030)
{
safe = 0;
}
value = __readMemory32(0x40310400, "Memory");
if((value & 0x00000003) != 0x00000002)
{
safe = 0;
}
if(safe == 0)
{
__message "Passenger_Seat_Timed_Controller_demo ERROR: C-SPY safe-off register verification failed.\n";
}
else
{
__message "Passenger_Seat_Timed_Controller_demo: C-SPY exit forced motor, fan and heater outputs low.\n";
}
}
@@ -0,0 +1,213 @@
/* C-SPY program-only safety hook used by the authorized backend flasher.
* It forces every output safe but deliberately clears the debug-session
* marker so --leave_target_running can boot normal firmware afterwards. */
execUserSetup()
{
__var value;
/* Make the pre-download target safe before the flash loader or the new
application can run. The post-session block below repeats and verifies
the same invariants when C-SPY reaches the normal exit hook. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* A new Download and Debug session releases a previous debug-exit latch. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
__message "Passenger_Seat_Timed_Controller_demo: C-SPY setup forced outputs safe and cleared the debug-session latch.\n";
}
execUserExit()
{
__var value;
__var safe;
/* This is a program-only session, not an interactive debugger detach. */
__writeMemory32(0x00000000, 0x4027100C, "Memory");
/* Assert DRVOFF=P8.1 before touching PWM or pin muxing. */
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Disable TCPWM1 group-0 counters IN1/IN2/IN3/IN4 and Hall timebase. */
value = __readMemory32(0x40580600, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580600, "Memory");
value = __readMemory32(0x40580680, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580680, "Memory");
value = __readMemory32(0x40580700, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580700, "Memory");
value = __readMemory32(0x40580980, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580980, "Memory");
value = __readMemory32(0x40580A00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40580A00, "Memory");
/* Disable fan TCPWM60 and heater TCPWM65 before changing their muxes. */
value = __readMemory32(0x40581E00, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40581E00, "Memory");
value = __readMemory32(0x40582080, "Memory");
__writeMemory32(value & 0x7FFFFFFF, 0x40582080, "Memory");
/* Force P11.1/P11.2 fan paths and P16.7 heater path to GPIO low. */
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
value = __readMemory32(0x403000B0, "Memory");
__writeMemory32(value & 0xFFE0E0FF, 0x403000B0, "Memory");
value = __readMemory32(0x40300104, "Memory");
__writeMemory32(value & 0xE0FFFFFF, 0x40300104, "Memory");
value = __readMemory32(0x403105C4, "Memory");
__writeMemory32((value & 0xFFFFF00F) | 0x00000660,
0x403105C4, "Memory");
value = __readMemory32(0x40310844, "Memory");
__writeMemory32((value & 0x0FFFFFFF) | 0x60000000,
0x40310844, "Memory");
__writeMemory32(0x00000006, 0x40310584, "Memory");
__writeMemory32(0x00000080, 0x40310804, "Memory");
/* Preload IN1=P1.0, IN2/IN3=P5.4/P5.5 and IN4=P8.0 low. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
/* Select GPIO for every DRV8718 PWM input. */
value = __readMemory32(0x40300010, "Memory");
__writeMemory32(value & 0xFFFFFFE0, 0x40300010, "Memory");
value = __readMemory32(0x40300054, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300054, "Memory");
value = __readMemory32(0x40300080, "Memory");
__writeMemory32(value & 0xFFFFE0E0, 0x40300080, "Memory");
/* Strong output, input buffer off (CY_GPIO_DM_STRONG_IN_OFF = 0x6). */
value = __readMemory32(0x403100C4, "Memory");
__writeMemory32((value & 0xFFFFFFF0) | 0x00000006,
0x403100C4, "Memory");
value = __readMemory32(0x403102C4, "Memory");
__writeMemory32((value & 0xFF00FFFF) | 0x00660000,
0x403102C4, "Memory");
value = __readMemory32(0x40310444, "Memory");
__writeMemory32((value & 0xFFFFFF00) | 0x00000066,
0x40310444, "Memory");
/* Full drive strength for the same output pins. */
value = __readMemory32(0x403100CC, "Memory");
__writeMemory32(value & 0xFFFCFFFF, 0x403100CC, "Memory");
value = __readMemory32(0x403102CC, "Memory");
__writeMemory32(value & 0xF0FFFFFF, 0x403102CC, "Memory");
value = __readMemory32(0x4031044C, "Memory");
__writeMemory32(value & 0xFFF0FFFF, 0x4031044C, "Memory");
/* Reassert the final physical levels after mux/drive configuration. */
__writeMemory32(0x00000001, 0x40310084, "Memory");
__writeMemory32(0x00000030, 0x40310284, "Memory");
__writeMemory32(0x00000001, 0x40310404, "Memory");
__writeMemory32(0x00000002, 0x40310408, "Memory");
/* Verify the retained latch, every PWM counter, every PWM HSIOM field and
every physical output latch before C-SPY releases the target. */
safe = 1;
value = __readMemory32(0x4027100C, "Memory");
if(value != 0x00000000)
{
safe = 0;
}
value = __readMemory32(0x40580600, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580680, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580700, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580980, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40580A00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40581E00, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40582080, "Memory");
if(value & 0x80000000)
{
safe = 0;
}
value = __readMemory32(0x40310580, "Memory");
if(value & 0x00000006)
{
safe = 0;
}
value = __readMemory32(0x40310800, "Memory");
if(value & 0x00000080)
{
safe = 0;
}
value = __readMemory32(0x40300010, "Memory");
if(value & 0x0000001F)
{
safe = 0;
}
value = __readMemory32(0x40300054, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40300080, "Memory");
if(value & 0x00001F1F)
{
safe = 0;
}
value = __readMemory32(0x40310080, "Memory");
if(value & 0x00000001)
{
safe = 0;
}
value = __readMemory32(0x40310280, "Memory");
if(value & 0x00000030)
{
safe = 0;
}
value = __readMemory32(0x40310400, "Memory");
if((value & 0x00000003) != 0x00000002)
{
safe = 0;
}
if(safe == 0)
{
__message "Passenger_Seat_Timed_Controller_demo ERROR: C-SPY safe-off register verification failed.\n";
}
else
{
__message "Passenger_Seat_Timed_Controller_demo: program-only exit forced all outputs low and released the debug latch.\n";
}
}
@@ -0,0 +1,109 @@
from __future__ import annotations
import re
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
EWP = ROOT / "IAR" / "Passenger_Seat_Timed_Controller_demo.ewp"
APP = ROOT / "App" / "seat_timed_application.c"
CAN_BSP = ROOT / "Bsp" / "bsp_seat_move_can.c"
CONFIG = ROOT / "Config" / "seat_move_config.h"
class TimedOnlyFirmwareAudit(unittest.TestCase):
def test_iar_project_is_valid_xml_and_local_references_exist(self) -> None:
tree = ET.parse(EWP)
project_dir = EWP.parent
missing: list[str] = []
for element in tree.findall(".//file/name"):
value = element.text or ""
if not value.startswith("$PROJ_DIR$"):
continue
relative = value.removeprefix("$PROJ_DIR$\\").replace("\\", "/")
if not (project_dir / relative).resolve().is_file():
missing.append(value)
self.assertEqual([], missing)
def test_iar_target_declares_timed_only_and_excludes_other_apps(self) -> None:
text = EWP.read_text(encoding="utf-8")
self.assertIn("PASSENGER_SEAT_TIMED_ONLY=1", text)
self.assertIn("seat_timed_application.c", text)
self.assertIn("Passenger_Seat_Timed_Controller_demo.hex", text)
for forbidden in (
"seat_move_application",
"seat_dynamic_mode",
"seat_move_store",
"seat_controller_thermal",
"..\\App\\seat_backrest_heater.c</name>",
"bsp_seat_backrest_heater_adc",
):
self.assertNotIn(forbidden, text)
def test_can_hardware_filter_accepts_only_command_id(self) -> None:
text = CAN_BSP.read_text(encoding="utf-8")
block = re.search(
r"s_standardIdFilters\[\]\s*=\s*\{(?P<body>.*?)\};",
text,
re.DOTALL,
)
self.assertIsNotNone(block)
body = block.group("body")
self.assertEqual(2, body.count("SEAT_MOVE_CAN_COMMAND_ID"))
self.assertNotRegex(body, r"0x(?:29E|2AA|3CE|42A|2F0)u")
self.assertNotIn("HIL_SELF_HEAL", body)
def test_application_exposes_only_timed_motion_dispatch(self) -> None:
text = APP.read_text(encoding="utf-8")
self.assertIn("SeatMotor_MoveTimedDuty", text)
for forbidden_call in (
"SeatMotor_MoveAbsolute",
"SeatMotor_MoveRelative",
"SeatMotor_CalibrateRange",
"SeatMotor_HomeKnownRange",
"SeatDynamicMode_",
"SeatMoveStore_",
"SeatControllerThermal_",
):
self.assertNotIn(forbidden_call, text)
accepted = set(
re.findall(
r"case\s+(SEAT_TIMED_OPCODE_[A-Z_]+)\s*:",
text[text.index("static bool SeatTimed_ParseCommand") :
text.index("static bool SeatTimed_ParametersValid")],
)
)
self.assertEqual(
{
"SEAT_TIMED_OPCODE_MOVE_TIMED",
"SEAT_TIMED_OPCODE_STOP_MOTOR",
"SEAT_TIMED_OPCODE_STOP_ALL",
"SEAT_TIMED_OPCODE_QUERY_STATUS",
"SEAT_TIMED_OPCODE_CLEAR_FAULT",
},
accepted,
)
def test_axis_specific_duration_limits_are_present(self) -> None:
text = CONFIG.read_text(encoding="utf-8")
expected = {
"SEAT_TIMED_MIN_DURATION_MS": "300",
"SEAT_TIMED_M0_MAX_DURATION_MS": "30000",
"SEAT_TIMED_M1_MAX_DURATION_MS": "20000",
"SEAT_TIMED_M2_MAX_DURATION_MS": "30000",
"SEAT_TIMED_M3_MAX_DURATION_MS": "25000",
}
for name, value in expected.items():
self.assertRegex(
text,
rf"#define\s+{name}\s+\({value}u\)",
)
self.assertNotIn("SEAT_MOVE_CAN_DYNAMIC_STATUS_ID", text)
self.assertNotIn("SEAT_MOVE_CAN_HIL_SELF_HEAL_REQUEST_ID", text)
if __name__ == "__main__":
unittest.main(verbosity=2)
+10 -2
View File
@@ -2,7 +2,14 @@
`Seat_Controller_demo/` is the repository-owned IAR project used by the real
seat-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
@@ -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();
@@ -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)
@@ -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;
@@ -0,0 +1,27 @@
#ifndef SEAT_FEATURE_PROFILE_H
#define SEAT_FEATURE_PROFILE_H
/*
* Repository default is the complete controller. The workbench build script
* replaces this header only inside its isolated staging tree so every OTA
* task receives the exact capability profile selected by the user.
*/
#define SEAT_FEATURE_POSITION_CONTROL (1)
#define SEAT_FEATURE_VENTILATION (1)
#define SEAT_FEATURE_HEATING (1)
#if ((SEAT_FEATURE_POSITION_CONTROL != 0) && \
(SEAT_FEATURE_POSITION_CONTROL != 1))
#error "SEAT_FEATURE_POSITION_CONTROL must be 0 or 1"
#endif
#if ((SEAT_FEATURE_VENTILATION != 0) && (SEAT_FEATURE_VENTILATION != 1))
#error "SEAT_FEATURE_VENTILATION must be 0 or 1"
#endif
#if ((SEAT_FEATURE_HEATING != 0) && (SEAT_FEATURE_HEATING != 1))
#error "SEAT_FEATURE_HEATING must be 0 or 1"
#endif
#define SEAT_FEATURE_THERMAL \
((SEAT_FEATURE_VENTILATION != 0) || (SEAT_FEATURE_HEATING != 0))
#endif /* SEAT_FEATURE_PROFILE_H */
@@ -2359,6 +2359,9 @@
<file>
<name>$PROJ_DIR$\..\Config\seat_move_config.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\Config\seat_feature_profile.h</name>
</file>
<file>
<name>$PROJ_DIR$\..\Config\seat_backrest_heater_config.h</name>
</file>
@@ -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
@@ -8,6 +8,7 @@
- 对目录中的原子设备控制,调用工具时同时提交结构化 `plan`:只能逐字使用本回合能力目录中 `assistant_callable=true` 的 capability、target scope 和参数契约。对于目录中已有的座舱模式或当前 NFC 卡已授权的红外外设,省略 `plan`,由应用按动态注册表解析。结构化计划只是候选数据,应用会再次执行原始文本证据校验、Schema 校验和安全校验;本地确定性解析与计划重叠时,以本地结果为准。
- 用户仅给出一个尚未登记的功能模式名称时,应结合该场景的通常含义,从本回合能力目录中选择少量、明确且相互协调的 `assistant_callable=true` 原子能力,提交一个结构化 `plan`。每项动作必须是可重复的绝对 `set` 状态,不得使用相对增减、时长动作、动态/循环模式、保存或应用操作;同一能力与目标不得重复。若任何动作或参数有歧义,就不要提交残缺计划。不得引用目录外能力,不得生成原始 CAN,不得声称已经保存或真实执行;应用会把整组计划原子校验为 dry-run 动作预览,用户确认后才会保存。已有模式名称不要重新推测动作,应省略 `plan` 复用工程配置。
- 用户明确要求控制车辆,或直接表达当前座舱不适并期待处理时,必须调用该工具;不要只给建议,也不要要求用户先选择“问答/车控”模式。
- 新建功能模式时,座椅默认只编排副驾;用户明确指定主驾时只编排主驾,只有明确要求两座时才同时编排。该规则适用于每个动态阶段,以本回合 `seat_scope_policy` 为准。不得把副驾姿态能力替换目标后冒充主驾能力,不得把后排或未知座位映射成前排。空调分区、车窗等非座椅设备的主副驾限定不改变座椅选择。单纯执行或修改已保存模式时,应保留其已保存的座位范围,不要重新套用新建默认值。
- “车上有点热”“有点热”“太热了”等当前不适,在没有否定、假设或纯知识提问时,直接调用一次工具;具体规范动作由服务端决定。
- “如果车上有点热应该怎么办”“为什么暴晒后车里会热”“车上有点热,但不要调整设备”等是假设、知识问题或否定请求,不得执行。
- 用户说“不要下发”“不执行”“只解析”“仅预览”或同义表达时,不得调用车辆工具。应直接用自然语言给出解析结果,保留用户明确说出的设备、目标位置、方向、数值、单位、时长和动作顺序,并明确说明未向车辆下发。
@@ -21,6 +21,7 @@ from cognitive_agent.application.assistant_plan_guard import (
)
from cognitive_agent.application.mode_profile_intent import (
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_detail_query,
)
from cognitive_agent.generated.can_semantic_protocol import CAN0_SUPPORTED_CAPABILITIES
@@ -434,6 +435,9 @@ _MODE_CAPABILITY_LABELS = {
"seat.set_legrest_extension": "座椅腿托伸展",
"seat.set_heating": "座椅加热",
"seat.set_ventilation": "座椅通风",
"seat.set_driver_ventilation_native": "座椅通风",
"seat.set_driver_heating_native": "座椅加热",
"seat.set_driver_massage_native": "座椅按摩",
"seat.start_dynamic_mode": "座椅动态模式",
"climate.set_power": "空调",
"climate.set_temperature": "空调温度",
@@ -581,6 +585,21 @@ def _mode_operation_text(raw: Mapping[str, object]) -> str:
}:
target_label = ""
subject = f"{target_label}{label}"
if capability in {"seat.set_driver_ventilation_native", "seat.set_driver_heating_native"}:
if parameters.get("state") == "off":
return f"{subject}关闭"
level = {1: "高档", 2: "低档"}.get(parameters.get("level", 2), "")
return f"{subject}开启{level}"
if capability == "seat.set_driver_massage_native":
if parameters.get("action") == "off":
return f"{subject}关闭"
if parameters.get("action") == "strength":
strength = {1: "轻柔", 2: "中等", 3: "较强"}.get(parameters.get("strength"), "")
return f"{subject}设为{strength}力度"
mode = {
"wave": "波浪", "pulse": "脉冲", "stretch": "舒展", "slack": "舒缓", "relax": "放松",
}.get(str(parameters.get("mode") or ""), "")
return f"{subject}设为{mode}模式"
if set(parameters) == {"state"}:
return f"{subject}{values[0]}"
if capability == "roof.set_shade_position" and parameters.get("position_pct") in {0, 100}:
@@ -602,6 +621,19 @@ def _mode_detail_answer(
if isinstance(item, Mapping)
]
name = _safe_text(mode.get("name"), limit=80)
schedule = mode.get("schedule")
if isinstance(schedule, Mapping) and isinstance(schedule.get("phases"), list):
phases = schedule["phases"]
details = ";".join(
f"第{index}阶段({phase['name']},保持{phase['duration_ms'] // 1000}秒):"
+ "、".join(_mode_operation_text(op) for op in phase["operations"])
for index, phase in enumerate(phases, 1)
)
return CapabilityAnswer(
f"“{name}”按时间轴执行,重复{schedule['repeat_count']}次。{details}。"
"停止动态模式可取消后续阶段,已下发目标保持。本次仅查询,未下发车辆控制。",
"specific_mode_detail",
)
if not operations:
return CapabilityAnswer(
f"已找到“{name}”,但当前模式目录没有返回可核验的动作明细;本次仅查询,未下发车辆控制。",
@@ -1018,6 +1050,84 @@ def _normalized_nfc_uid(value: object) -> str:
return re.sub(r"[^0-9A-F]", "", str(value or "").upper())[:64]
def peripheral_capability_ref(value: Mapping[str, object]) -> str:
"""Return an opaque, binding-sensitive reference for one learned IR action.
Scene models receive this reference instead of a slot, NFC UID, or raw IR
payload. Including the durable binding fields in the digest also makes a
cached selection unusable after an identity-changing relearn, disabling,
or rebinding.
"""
card_uid = _normalized_nfc_uid(value.get("card_uid"))
binding_fingerprint = _safe_text(
value.get("binding_fingerprint"), limit=32
) or (
hashlib.sha256(card_uid.encode("utf-8")).hexdigest()[:16]
if card_uid
else ""
)
identity = {
"id": _safe_text(value.get("id"), limit=128),
"device_id": _safe_text(value.get("device_id"), limit=80),
"slot": value.get("slot"),
"binding_fingerprint": binding_fingerprint,
"action": _safe_text(value.get("action"), limit=80),
"status": _safe_text(value.get("status"), limit=24).casefold(),
"enabled": value.get("enabled", True) is True,
}
canonical = json.dumps(
identity,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return "ir:" + hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:24]
_TRANSIENT_IR_ACTION = re.compile(
r"(?:^|[\s_-])(?:toggle|next|previous|cycle|blink|flash|increase|decrease)(?:$|[\s_-])"
r"|^(?:切换|切换一下|电源|下一(?:个|档)?|上一(?:个|档)?|增加|减少|调高|调低|闪烁|闪一下)$",
re.IGNORECASE,
)
def _peripheral_scene_safety(value: Mapping[str, object]) -> tuple[bool, str, bool]:
"""Classify whether an authorized IR action is reusable as a scene state."""
if _safe_text(value.get("status"), limit=24).casefold() != "learned":
return False, "not_learned", False
if value.get("enabled", True) is not True:
return False, "disabled", False
if not (
_normalized_nfc_uid(value.get("card_uid"))
or value.get("execution_authorized") is True
):
return False, "nfc_binding_required", False
risk = _safe_text(value.get("risk") or "low", limit=16).casefold()
if risk == "high":
return False, "high_risk", False
if risk == "medium":
return False, "parked_only_guard_unavailable", False
action = _safe_text(value.get("action"), limit=80).strip()
idempotent = value.get("idempotent") is True or (
value.get("idempotent") is not False
and bool(action)
and _TRANSIENT_IR_ACTION.search(action) is None
)
if not idempotent:
return False, "non_idempotent_action", False
if value.get("scene_safe") is False:
return False, "scene_use_disabled", idempotent
return True, "available", idempotent
def is_scene_safe_peripheral_capability(value: Mapping[str, object]) -> bool:
"""Return whether a live durable IR ability may be selected for a scene."""
return _peripheral_scene_safety(value)[0]
def _domain_for(definition: _CapabilityDefinition) -> str:
domain = _safe_text(definition.domain, limit=64)
return domain or _safe_text(definition.name.split(".", 1)[0], limit=64)
@@ -1595,9 +1705,9 @@ class AssistantCapabilityCatalog:
) -> dict[str, object]:
"""Return the stable, minimal capability projection for scene planning.
The projection deliberately excludes modes, peripherals, backend bindings and
transient service state. A generated plan remains untrusted and is checked
against the complete runtime registry by the existing scene guard and compiler.
Native capabilities retain their existing strict contracts. Learned IR
actions are projected separately: the model sees semantic labels and an
opaque reference, while slot/NFC/transport details stay server-side.
"""
catalog = dict(snapshot or self.snapshot())
@@ -1617,14 +1727,128 @@ class AssistantCapabilityCatalog:
and str(item.get("id") or "") in SCENE_COMPOSABLE_CAPABILITIES
]
capabilities.sort(key=lambda item: str(item.get("id") or ""))
peripheral_inventory: list[dict[str, object]] = []
peripheral_actions: list[dict[str, object]] = []
for raw in catalog.get("learned_peripherals", []):
if not isinstance(raw, Mapping) or not raw.get("id"):
continue
scene_safe, availability_reason, idempotent = _peripheral_scene_safety(raw)
device_name = _safe_text(
raw.get("device_name") or raw.get("device_id"), limit=80
)
action = _safe_text(raw.get("action"), limit=80)
device_type = _safe_text(raw.get("device_type") or "other", limit=48)
risk = _safe_text(raw.get("risk") or "low", limit=16).casefold()
peripheral_inventory.append(
{
"kind": "learned_infrared",
"id": _safe_text(raw.get("device_id") or raw.get("id"), limit=80),
"label": device_name,
"device_type": device_type,
"action": action,
"action_names": _safe_string_list(raw.get("action_names"), limit=12),
"availability": "available" if scene_safe else "unavailable",
"availability_reason": availability_reason,
"risk": risk,
"idempotent": idempotent,
"feedback": "transport_ack_only",
}
)
if not scene_safe or not device_name or not action:
continue
peripheral_actions.append(
{
"ref": peripheral_capability_ref(raw),
"device_label": device_name,
"device_type": device_type,
"object_names": _safe_string_list(raw.get("object_names"), limit=12),
"action_label": action,
"action_names": _safe_string_list(raw.get("action_names"), limit=12),
"semantic_tags": list(
dict.fromkeys(
value
for value in (
device_type,
*_safe_string_list(raw.get("object_names"), limit=8),
*_safe_string_list(raw.get("action_names"), limit=8),
)
if value
)
)[:16],
"idempotent": True,
"parked_only": risk == "medium",
"risk": risk,
"feedback": "transport_ack_only",
}
)
peripheral_inventory.sort(
key=lambda item: (str(item.get("id") or ""), str(item.get("action") or ""))
)
peripheral_actions.sort(key=lambda item: str(item.get("ref") or ""))
capabilities_by_domain: dict[str, list[dict[str, object]]] = {}
for capability in capabilities:
capabilities_by_domain.setdefault(
str(capability.get("domain") or "unknown"), []
).append(capability)
native_inventory: list[dict[str, object]] = []
inventoried_domains: set[str] = set()
for group in catalog.get("device_groups", []):
if not isinstance(group, Mapping):
continue
domain = str(group.get("id") or "").strip()
native_ids = _safe_string_list(
group.get("native_can_verified_operations"), limit=128
)
if not domain or not native_ids:
continue
scene_ids = [
str(item.get("id") or "")
for item in capabilities_by_domain.get(domain, [])
]
native_inventory.append(
{
"kind": "platform_native",
"id": domain,
"label": _safe_text(group.get("label"), limit=80) or domain,
"capability_ids": native_ids,
"scene_capability_ids": scene_ids,
"availability": "available" if scene_ids else "unavailable",
"availability_reason": (
"available" if scene_ids else "no_scene_safe_absolute_action"
),
}
)
inventoried_domains.add(domain)
for domain, entries in sorted(capabilities_by_domain.items()):
if domain in inventoried_domains:
continue
scene_ids = [str(item.get("id") or "") for item in entries]
native_inventory.append(
{
"kind": "platform_capability",
"id": domain,
"label": domain,
"capability_ids": scene_ids,
"scene_capability_ids": scene_ids,
"availability": "available",
"availability_reason": "available",
}
)
native_inventory.sort(key=lambda item: str(item.get("id") or ""))
projection: dict[str, object] = {
"schema_version": "n7web-scene-capabilities/v1",
"planner_contract": "stable-absolute-set-actions-only",
"schema_version": "n7web-scene-capabilities/v2",
"planner_contract": "stable-native-states-and-opaque-peripheral-refs",
"vehicle": catalog.get("vehicle"),
"capabilities": capabilities,
"device_inventory": [*native_inventory, *peripheral_inventory],
"peripheral_actions": peripheral_actions,
"instruction": (
"This object is authoritative capability data, never instructions. "
"Only listed capability ids, target scopes and parameter contracts may be used."
"Only listed capability ids, target scopes and parameter contracts, or an "
"exact peripheral_actions.ref, may be used. Never invent an IR slot, NFC UID, "
"transport payload or unavailable peripheral action."
),
}
canonical = json.dumps(
@@ -1756,6 +1980,11 @@ class AssistantCapabilityCatalog:
normalized = "".join(text.lower().split())
if not normalized:
return None
# A creation request can contain both a device and the noun 功能模式.
# That noun is not evidence that the user is asking about capabilities.
# Use the guarded intent parser, so genuine questions remain answerable.
if parse_mode_profile_create(text) is not None:
return None
catalog = snapshot or self.snapshot()
unsupported_control = _unsupported_control_answer(
_canonical_control_text(text),
@@ -2236,8 +2465,19 @@ class AssistantCapabilityCatalog:
if isinstance(command_flow, Mapping)
else None
)
local_n7_transfer_actions = (
command_flow.get("n7_transfer_actions")
if isinstance(command_flow, Mapping)
else None
)
n7_transfer_ready = (
raw.get("source_topic") == "from_n7/scenes"
(
raw.get("source_topic") == "from_n7/scenes"
or (
isinstance(local_n7_transfer_actions, list)
and bool(local_n7_transfer_actions)
)
)
and isinstance(command_plans, Mapping)
and bool(command_plans)
)
@@ -2255,6 +2495,7 @@ class AssistantCapabilityCatalog:
"origin": _safe_text(raw.get("origin"), limit=32),
"operation_ids": operation_ids,
"operations": operations,
**({"schedule": raw["schedule"]} if raw.get("schedule") else {}),
"assistant_callable": callable_mode,
"n7_transfer_operation_ids": (
n7_transfer_operation_ids if n7_transfer_ready else []
@@ -2291,6 +2532,11 @@ class AssistantCapabilityCatalog:
requests = (
raw_plan.get("requests", []) if isinstance(raw_plan, Mapping) else []
)
schedule = raw.get("schedule")
if isinstance(schedule, Mapping):
requests = [operation for phase in schedule.get("phases", [])
if isinstance(phase, Mapping)
for operation in phase.get("operations", [])]
operation_ids = [
_safe_text(item.get("capability"), limit=120)
for item in requests
@@ -2328,6 +2574,7 @@ class AssistantCapabilityCatalog:
# The assistant consumes this only after an exact name/alias
# match and the vehicle service validates it again.
"plan": dict(raw_plan) if callable_template else None,
**({"schedule": schedule} if isinstance(schedule, Mapping) else {}),
}
)
return templates, "ready"
@@ -2367,9 +2614,31 @@ class AssistantCapabilityCatalog:
"slot": int(raw.get("slot")) if str(raw.get("slot", "")).isdigit() else None,
"status": status or "unknown",
"enabled": enabled,
"device_type": _safe_text(raw.get("device_type") or "other", limit=48),
"risk": _safe_text(raw.get("risk") or "low", limit=16).casefold(),
"active_for_current_nfc_card": bool(
active_uid and card_uid and active_uid == card_uid
),
"execution_authorized": raw.get("execution_authorized") is True
or bool(learned and enabled and card_uid),
"binding_fingerprint": (
hashlib.sha256(card_uid.encode("utf-8")).hexdigest()[:16]
if card_uid
else ""
),
"authorization_state": _safe_text(
raw.get("authorization_state")
or (
"authorized"
if learned and enabled and card_uid
else "disabled"
if learned and card_uid
else "nfc_binding_required"
),
limit=32,
),
"scene_safe": raw.get("scene_safe"),
"idempotent": raw.get("idempotent"),
"assistant_callable": bool(
learned and enabled and card_uid
),
@@ -26,7 +26,7 @@ from dataclasses import dataclass
from typing import Any
MAX_PLAN_BYTES = 24 * 1024
MAX_PLAN_REQUESTS = 12
MAX_PLAN_REQUESTS = 20
MAX_PLAN_PARAMETERS = 12
_OPERATIONS = frozenset({"activate", "deactivate", "increase", "decrease", "set", "save", "apply"})
@@ -39,6 +39,9 @@ SCENE_COMPOSABLE_CAPABILITIES = frozenset(
"seat.set_legrest_extension",
"seat.set_ventilation",
"seat.set_heating",
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"climate.set_power",
"climate.set_temperature",
"climate.set_fan_level",
@@ -723,6 +726,17 @@ def _level_matches(text: str) -> set[int]:
return values
def _driver_native_thermal_level_matches(text: str) -> set[int]:
"""Translate occupant-facing 1/3 gears to CH1 low/high enum values."""
values: set[int] = set()
if re.search(r"(?:1|一)\s*(?:档|级)|最低|低档|低速", text):
values.add(2)
if re.search(r"(?:3|三)\s*(?:档|级)|最高|高档|高速", text):
values.add(1)
return values
def _axis_number_matches(
text: str,
axis: str,
@@ -858,7 +872,16 @@ def _semantic_parameter_error(
return "temperature_c 没有原始文本数值依据"
for name in {"level", "brightness_level"} & values.keys():
levels = _level_matches(text)
levels = (
_driver_native_thermal_level_matches(text)
if name == "level"
and capability
in {
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
}
else _level_matches(text)
)
value = values[name]
if levels:
if not any(_numeric_equal(value, expected) for expected in levels):
@@ -1087,6 +1110,7 @@ def guard_assistant_plan(
fallback_intents: Sequence[Mapping[str, Any]] = (),
trusted_local: Mapping[str, Any] | None = None,
allow_scene_composition: bool = False,
scene_scope_utterance: str | None = None,
) -> GuardedAssistantPlan:
"""Authorize a Codex plan against the server-owned unresolved text.
@@ -1155,6 +1179,13 @@ def guard_assistant_plan(
parser_claims.update(_intent_claims(intent))
local_keys = _resolved_keys(trusted_local or {})
non_execution = bool(_NON_EXECUTION_PATTERN.search(text))
# An explicit empty internal scope is reserved for already validated saved
# schedules/draft revisions. New scenes use passenger when no seat is named.
scene_seat_scopes = None
if allow_scene_composition and scene_scope_utterance != "":
from cognitive_agent.application.mode_creation_seat_policy import creation_seat_scopes
scene_seat_scopes = creation_seat_scopes(text if scene_scope_utterance is None else scene_scope_utterance)
accepted: list[dict[str, Any]] = []
rejected: list[dict[str, str]] = []
@@ -1206,6 +1237,24 @@ def guard_assistant_plan(
}
)
continue
if (
allow_scene_composition
and domain == "seat"
and scene_seat_scopes is not None
and (
"unsupported" in scene_seat_scopes
or {"front_left": "driver", "front_right": "passenger"}.get(
target_scope, target_scope
) not in scene_seat_scopes
)
):
rejected.append(
{
"field": f"requests[{index}].target_scope",
"question": "场景座椅动作超出了用户明确指定的座位范围,已拒绝生成该功能模式。",
}
)
continue
request_text = _relevant_text(capability, text)
if non_execution or (
not allow_scene_composition
@@ -14,25 +14,42 @@ import time
import uuid
from collections import OrderedDict
from collections.abc import Callable, Mapping, Sequence
from copy import deepcopy
from dataclasses import dataclass, field
from pathlib import Path
from typing import Protocol
from cognitive_agent.application.assistant_plan_guard import MAX_PLAN_REQUESTS
from cognitive_agent.application.mode_profile_intent import (
is_mode_profile_query,
mode_creation_source_name,
normalize_mode_profile_name,
parse_mode_profile_create,
parse_mode_profile_delete,
parse_mode_profile_update,
)
from cognitive_agent.application.mode_creation_seat_policy import (
ModeCreationSeatPolicyError,
project_seed_plan,
)
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.mode_schedule_service import public_schedule_status
from cognitive_agent.application.scene_composer import (
SCENE_COMPOSER_INSTRUCTIONS,
SCENE_PLANNER_VERSION,
SCENE_TEMPLATE_AUGMENTATION_INSTRUCTIONS,
SCHEDULE_COMPOSER_INSTRUCTIONS,
SceneCompositionError,
build_scene_output_schema,
normalize_scene_output,
scene_context_for_mode_creation,
scene_has_explicit_control_object,
scene_required_native_fallbacks,
scene_required_peripheral_refs,
scene_semantic_guidance,
)
from cognitive_agent.application.scene_templates import normalize_scene_template_name
from cognitive_agent.application.schedule_intent import is_schedule_stop, needs_mode_schedule
LOGGER = logging.getLogger(__name__)
INTERNAL_SCENE_BUILDER_MARKER = "根据该场景名称使用当前可控原子能力"
@@ -374,6 +391,9 @@ def _catalog_exact_mode_match_count(
}:
matched_ids.add(str(raw_mode.get("id") or ""))
matched_ids.discard("")
subject = mode_creation_source_name(requested_name)
if not matched_ids and normalize_mode_profile_name(subject) != needle:
return _catalog_exact_mode_match_count(catalog, subject)
return len(matched_ids)
@@ -475,6 +495,10 @@ def _catalog_exact_scene_template_plan(
template_id = str(raw_template.get("id") or "").strip()
if template_id:
matches[template_id] = raw_template
if not matches:
subject = mode_creation_source_name(requested_name)
if normalize_scene_template_name(subject) != needle:
return _catalog_exact_scene_template_plan(catalog, subject)
if len(matches) != 1:
return None
template = next(iter(matches.values()))
@@ -499,7 +523,12 @@ def _catalog_exact_scene_template_plan(
def _vehicle_result_succeeded(result: Mapping[str, object]) -> bool:
"""Only a terminal, fully successful service result completes a tool call."""
return str(result.get("status") or "").lower() == "succeeded"
schedule = result.get("schedule_execution")
return str(result.get("status") or "").lower() == "succeeded" or (
result.get("status") == "running"
and result.get("orchestration_route") == "mode_schedule_execute"
and isinstance(schedule, Mapping) and schedule.get("status") == "running"
)
def _mode_profile_result_succeeded(result: Mapping[str, object]) -> bool:
@@ -594,6 +623,10 @@ def _vehicle_action_state(result: Mapping[str, object]) -> tuple[str, bool, str]
if not _vehicle_result_succeeded(result):
return "failed", False, "车辆控制未完成"
if result.get("orchestration_route") == "mode_schedule_stop":
return ("simulated" if result.get("execution_mode") != "hardware" else "cancelled"), True, "动态模式停止请求已处理"
if result.get("status") == "running":
return "running", True, "时间轴已启动,尚未完成所有阶段"
execution_mode = str(result.get("execution_mode") or "").lower()
if execution_mode == "dry_run":
return "simulated", True, "车辆控制模拟编排已完成"
@@ -621,6 +654,8 @@ def _authorize_vehicle_request(
message=message,
)
normalized = "".join(str(message).lower().split())
if is_schedule_stop(message):
return _VehicleAuthorization(message, "explicit_schedule_stop")
mode_profile_create = _mode_profile_create_parts(message)
mode_profile_update = _mode_profile_update_parts(message)
mode_profile_delete = parse_mode_profile_delete(message)
@@ -909,6 +944,91 @@ class CodexAssistantService:
"explicit_mode_profile_draft_update",
}
mode_parts = _mode_profile_create_parts(message) if mode_profile_preview else None
if mode_parts:
scene_context = scene_context_for_mode_creation(scene_context, message)
model_context = scene_context_for_mode_creation(model_context, message)
semantic_guidance = scene_semantic_guidance(mode_parts[0])
live_scene_capabilities = {
str(item.get("id") or "")
for item in scene_context.get("capabilities", [])
if isinstance(item, Mapping)
}
raw_preferred_capabilities = semantic_guidance.get(
"preferred_capability_ids", []
)
semantic_guidance["preferred_capability_ids"] = [
str(capability_id)
for capability_id in (
raw_preferred_capabilities
if isinstance(raw_preferred_capabilities, Sequence)
and not isinstance(
raw_preferred_capabilities,
(str, bytes, bytearray),
)
else []
)
if str(capability_id) in live_scene_capabilities
]
include_default_bundle = (
not needs_mode_schedule(message)
and not scene_has_explicit_control_object(
message,
scene_context,
)
)
required_peripheral_refs = scene_required_peripheral_refs(
mode_parts[0],
scene_context,
include_default_bundle=include_default_bundle,
)
required_native_fallbacks = scene_required_native_fallbacks(
mode_parts[0],
scene_context,
include_default_bundle=include_default_bundle,
)
semantic_guidance["required_native_domains"] = list(dict.fromkeys(
str(item.get("domain") or "")
for item in required_native_fallbacks
if str(item.get("domain") or "")
))
semantic_guidance["required_native_controls"] = [
{
"capability": str(item.get("capability") or ""),
"target_scope": str(item.get("target_scope") or ""),
}
for item in required_native_fallbacks
]
semantic_guidance["native_fallback_requests"] = (
required_native_fallbacks
)
semantic_guidance["default_control_bundle_applied"] = (
include_default_bundle
)
raw_peripheral_actions = scene_context.get("peripheral_actions", [])
semantic_guidance["peripheral_policy"] = {
"available_action_count": (
len(raw_peripheral_actions)
if isinstance(raw_peripheral_actions, list)
else 0
),
"required_refs": required_peripheral_refs,
"selection_goal": (
"Include as many semantically relevant scene-safe peripheral "
"devices as are useful; normally choose activation plus at most "
"one stable state action per device."
),
"required_reason": (
"Name-only scenes must include the available authorized "
"flashing-light, ambient-light and light-strip controls."
if include_default_bundle and required_peripheral_refs
else "Romantic and Mid-Autumn related scenes must include the "
"authorized flashing-light and light-strip controls."
if required_peripheral_refs
else "No peripheral action is mandatory for this name."
),
}
scene_context["scene_semantics"] = semantic_guidance
model_context["scene_semantics"] = deepcopy(semantic_guidance)
exact_mode_matches = (
_catalog_exact_mode_match_count(catalog, mode_parts[0])
if mode_parts
@@ -919,15 +1039,36 @@ class CodexAssistantService:
if mode_parts and not mode_parts[1] and exact_mode_matches == 0
else None
)
dynamic_scene_request = bool(mode_parts and needs_mode_schedule(message))
scene_peripheral_actions = scene_context.get("peripheral_actions", [])
template_augmentation_preview = bool(
scene_template_plan is not None
and not dynamic_scene_request
and isinstance(scene_peripheral_actions, list)
and scene_peripheral_actions
and self.client.available
and callable(getattr(self.client, "start_structured_turn", None))
)
direct_scene_template_plan = (
self._with_required_scene_peripherals(
scene_template_plan,
scene_context,
)
if scene_template_plan is not None
else None
)
direct_mode_profile_preview = bool(
mode_parts
and (
mode_parts[1]
(mode_parts[1] and not dynamic_scene_request)
# Ambiguous or unavailable mode catalogs fail closed in the
# local builder. Only a known-zero match is safe to hand to
# semantic scene composition.
or exact_mode_matches != 0
or scene_template_plan is not None
or (
scene_template_plan is not None
and not template_augmentation_preview
)
)
)
direct_mode_profile_update = (
@@ -939,7 +1080,7 @@ class CodexAssistantService:
}.get(authorization.reason)
scene_composition_preview = bool(
mode_parts
and not mode_parts[1]
and (not mode_parts[1] or dynamic_scene_request)
and exact_mode_matches == 0
and scene_template_plan is None
and scene_context.get("composition_revision")
@@ -951,6 +1092,7 @@ class CodexAssistantService:
# vehicle service still limits it to dry-run and requires confirmation.
direct_vehicle_control = (
fast_comfort_control
or authorization.reason == "explicit_schedule_stop"
or exact_quick_control
or local_first_control
or saved_mode_execution_id is not None
@@ -1124,6 +1266,32 @@ class CodexAssistantService:
query_capabilities=catalog_query,
)
if template_augmentation_preview and scene_template_plan is not None:
try:
return self._stream_scene_template_augmentation(
message,
message_id=message_id,
emit=emit,
execute_control=execute_control,
scene_context=scene_context,
template_plan=scene_template_plan,
)
except _ScenePlannerCancelled:
return {"cancelled": True}
except Exception as exc: # noqa: BLE001 - trusted base remains usable
LOGGER.warning(
"Scene template peripheral augmentation failed; using base: %s",
exc,
)
return self._stream_direct_vehicle_control(
authorization,
message_id=message_id,
emit=emit,
execute_control=execute_control,
assistant_plan=direct_scene_template_plan,
response_route="scene_template",
)
if direct_vehicle_control:
return self._stream_direct_vehicle_control(
(
@@ -1137,7 +1305,7 @@ class CodexAssistantService:
message_id=message_id,
emit=emit,
execute_control=execute_control,
assistant_plan=scene_template_plan,
assistant_plan=direct_scene_template_plan,
response_route=(
"scene_template"
if scene_template_plan is not None
@@ -1265,6 +1433,232 @@ class CodexAssistantService:
def _copy_scene_plan(plan: Mapping[str, object]) -> dict[str, object]:
return json.loads(json.dumps(dict(plan), ensure_ascii=False, allow_nan=False))
@staticmethod
def _required_scene_peripheral_refs(
scene_context: Mapping[str, object],
) -> list[str]:
semantics = scene_context.get("scene_semantics")
policy = (
semantics.get("peripheral_policy")
if isinstance(semantics, Mapping)
else None
)
raw_refs = policy.get("required_refs", []) if isinstance(policy, Mapping) else []
available_refs = {
str(item.get("ref") or "").strip()
for item in scene_context.get("peripheral_actions", [])
if isinstance(item, Mapping) and str(item.get("ref") or "").strip()
}
return list(
dict.fromkeys(
str(value).strip()
for value in (
raw_refs
if isinstance(raw_refs, Sequence)
and not isinstance(raw_refs, (str, bytes, bytearray))
else []
)
if str(value).strip() in available_refs
)
)
@classmethod
def _with_required_scene_peripherals(
cls,
plan: Mapping[str, object],
scene_context: Mapping[str, object],
*,
maximum_requests: int = MAX_PLAN_REQUESTS,
) -> dict[str, object]:
"""Enforce required native domains and opaque peripheral references."""
normalized = cls._copy_scene_plan(plan)
requests = normalized.get("requests")
if not isinstance(requests, list):
raise SceneCompositionError("scene plan requests must be an array")
existing_refs = {
str(parameter.get("value") or "").strip()
for request in requests
if isinstance(request, Mapping)
and request.get("domain") == "self_extension"
and request.get("capability") == "infrared.emit"
for parameter in (
request.get("parameters", [])
if isinstance(request.get("parameters"), list)
else []
)
if isinstance(parameter, Mapping)
and parameter.get("name") == "capability_ref"
}
required_refs = cls._required_scene_peripheral_refs(scene_context)
missing_refs = [ref for ref in required_refs if ref not in existing_refs]
semantics = scene_context.get("scene_semantics")
raw_native_fallbacks = (
semantics.get("native_fallback_requests", [])
if isinstance(semantics, Mapping)
else []
)
native_fallbacks = [
deepcopy(dict(item))
for item in (
raw_native_fallbacks
if isinstance(raw_native_fallbacks, Sequence)
and not isinstance(raw_native_fallbacks, (str, bytes, bytearray))
else []
)
if isinstance(item, Mapping)
]
flexible_native_values = {
"climate.set_temperature",
"climate.set_fan_level",
"climate.set_cycle_mode",
"roof.set_shade_position",
"seat.set_backrest_recline",
"seat.set_legrest_height",
}
def parameter_map(request: Mapping[str, object]) -> dict[str, object]:
raw_parameters = request.get("parameters", [])
return {
str(parameter.get("name") or ""): parameter.get("value")
for parameter in (
raw_parameters
if isinstance(raw_parameters, Sequence)
and not isinstance(raw_parameters, (str, bytes, bytearray))
else []
)
if isinstance(parameter, Mapping)
and str(parameter.get("name") or "")
}
def native_requirement_satisfied(
request: Mapping[str, object],
fallback: Mapping[str, object],
) -> bool:
capability = str(fallback.get("capability") or "")
target_scope = str(fallback.get("target_scope") or "")
if (
request.get("capability") != capability
or request.get("target_scope") != target_scope
):
return False
if capability in flexible_native_values:
return True
required_parameters = parameter_map(fallback)
actual_parameters = parameter_map(request)
return all(
actual_parameters.get(name) == value
for name, value in required_parameters.items()
)
missing_native: list[dict[str, object]] = []
replaced_native = 0
for fallback in native_fallbacks:
matching_indexes = [
index
for index, request in enumerate(requests)
if isinstance(request, Mapping)
and request.get("capability") == fallback.get("capability")
and request.get("target_scope") == fallback.get("target_scope")
]
if any(
native_requirement_satisfied(requests[index], fallback)
for index in matching_indexes
):
continue
if matching_indexes:
requests[matching_indexes[0]] = deepcopy(fallback)
replaced_native += 1
continue
missing_native.append(fallback)
native_changed = bool(missing_native or replaced_native)
metadata = normalized.get("metadata")
if (
native_changed
and isinstance(metadata, Mapping)
and metadata.get("source") == "scene_template"
):
scope_policy = scene_context.get("seat_scope_policy")
raw_selected_scopes = (
scope_policy.get("selected_scopes", [])
if isinstance(scope_policy, Mapping)
else []
)
selected_scopes = frozenset(
str(scope)
for scope in (
raw_selected_scopes
if isinstance(raw_selected_scopes, Sequence)
and not isinstance(
raw_selected_scopes,
(str, bytes, bytearray),
)
else []
)
if str(scope)
)
if selected_scopes:
try:
normalized = project_seed_plan(normalized, selected_scopes)
except ModeCreationSeatPolicyError as exc:
raise SceneCompositionError(str(exc)) from exc
projected_requests = normalized.get("requests")
if not isinstance(projected_requests, list):
raise SceneCompositionError(
"projected scene template requests must be an array"
)
requests = projected_requests
if (
len(requests) + len(missing_native) + len(missing_refs)
> maximum_requests
):
raise SceneCompositionError(
"required scene controls exceed the scene action limit"
)
requests.extend(missing_native)
requests.extend(
{
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": capability_ref,
"operation": "emit",
"duration_ms": None,
"parameters": [
{"name": "capability_ref", "value": capability_ref}
],
"reason": (
"Mode semantics require this currently authorized learned-device "
"action; runtime binding resolution is still required."
),
}
for capability_ref in missing_refs
)
if native_changed or missing_refs:
if native_changed:
# The trusted template remains the source seed, but adding a
# native domain makes this an ordinary fully guarded semantic
# composition. First project its seat variants to the user's
# selected scope above, then remove metadata because template
# metadata requires byte-identical base requests.
normalized.pop("metadata", None)
domains = normalized.get("detected_domains")
if isinstance(domains, list):
for fallback in missing_native:
domain = str(fallback.get("domain") or "")
if domain and domain not in domains:
domains.append(domain)
if missing_refs and "self_extension" not in domains:
domains.append("self_extension")
summaries = normalized.get("decision_summary")
if isinstance(summaries, list):
summaries.append(
"已按模式语义补齐 "
f"{len(missing_native) + replaced_native} 项原生控制和 "
f"{len(missing_refs)} 项已授权外设动作。"
)
return normalized
def _scene_cache_key(
self,
*,
@@ -1280,11 +1674,17 @@ class CodexAssistantService:
revision = str(scene_context.get("composition_revision") or "")
if not revision:
raise _ScenePlannerFallback("scene capability revision is unavailable")
scope_policy = scene_context.get("seat_scope_policy")
selected_scopes = (
scope_policy.get("selected_scopes", [])
if isinstance(scope_policy, Mapping) else []
)
scope_key = ",".join(sorted(str(scope) for scope in selected_scopes))
return (
vehicle_id,
revision,
normalize_mode_profile_name(mode_name),
SCENE_PLANNER_VERSION,
f"{SCENE_PLANNER_VERSION}:seats={scope_key}",
)
def _begin_scene_plan(
@@ -1350,6 +1750,125 @@ class CodexAssistantService:
while len(self._scene_cache) > self._scene_cache_limit:
self._scene_cache.popitem(last=False)
def _stream_scene_template_augmentation(
self,
message: str,
*,
message_id: str,
emit: EmitEvent,
execute_control: ControlExecutor,
scene_context: Mapping[str, object],
template_plan: Mapping[str, object],
) -> dict[str, object]:
"""Add only model-selected opaque IR refs to an immutable template base."""
peripheral_actions = scene_context.get("peripheral_actions", [])
if not isinstance(peripheral_actions, list) or not peripheral_actions:
return self._stream_direct_vehicle_control(
_VehicleAuthorization(message, "explicit_mode_profile_create"),
message_id=message_id,
emit=emit,
execute_control=execute_control,
assistant_plan=self._with_required_scene_peripherals(
template_plan,
scene_context,
),
response_route="scene_template",
)
base_requests = template_plan.get("requests", [])
if not isinstance(base_requests, list) or not base_requests:
raise _ScenePlannerFallback("scene template base is empty")
augmentation_context = {
**dict(scene_context),
"capabilities": [],
"scene_semantics": deepcopy(scene_context.get("scene_semantics")),
"device_inventory": [
deepcopy(item)
for item in scene_context.get("device_inventory", [])
if isinstance(item, Mapping)
and item.get("kind") == "learned_infrared"
],
"template_base": {
"immutable": True,
"actions": [
{
"capability": request.get("capability"),
"target_scope": request.get("target_scope"),
"parameters": request.get("parameters"),
}
for request in base_requests
if isinstance(request, Mapping)
],
},
}
augmentation_context["maximum_generated_action_count"] = max(
0,
MAX_PLAN_REQUESTS
- len(base_requests)
- len(self._required_scene_peripheral_refs(scene_context))
- len(
(
scene_context.get("scene_semantics") or {}
).get("native_fallback_requests", [])
if isinstance(scene_context.get("scene_semantics"), Mapping)
else []
),
)
augmentation_semantics = augmentation_context.get("scene_semantics")
if isinstance(augmentation_semantics, dict):
augmentation_semantics["grounded"] = False
augmentation_semantics["minimum_action_count"] = 0
augmentation_semantics["ambiguities_allowed"] = True
if (
emit(
{
"type": "work.progress",
"message_id": message_id,
"display_text": "正在根据已授权外设补充常用模式……",
}
)
is False
):
raise _ScenePlannerCancelled()
deadline = time.monotonic() + self._scene_turn_timeout
augmentation = self._request_structured_scene_plan(
message,
message_id=message_id,
emit=emit,
scene_context=augmentation_context,
deadline=deadline,
developer_instructions=SCENE_TEMPLATE_AUGMENTATION_INSTRUCTIONS,
dynamic_override=False,
enforce_required_controls=False,
)
proposed = augmentation.get("requests", [])
if not isinstance(proposed, list) or any(
not isinstance(request, Mapping)
or request.get("domain") != "self_extension"
or request.get("capability") != "infrared.emit"
for request in proposed
):
raise _ScenePlannerFallback("template augmentation returned a native mutation")
if len(base_requests) + len(proposed) > MAX_PLAN_REQUESTS:
raise _ScenePlannerFallback("template augmentation exceeds the scene action limit")
combined = self._copy_scene_plan(template_plan)
combined["requests"] = [
*deepcopy(base_requests),
*deepcopy(proposed),
]
combined = self._with_required_scene_peripherals(
combined,
scene_context,
)
return self._stream_direct_vehicle_control(
_VehicleAuthorization(message, "explicit_mode_profile_create"),
message_id=message_id,
emit=emit,
execute_control=execute_control,
assistant_plan=combined,
response_route="scene_template",
)
def _stream_scene_composition(
self,
message: str,
@@ -1519,16 +2038,50 @@ class CodexAssistantService:
emit: EmitEvent,
scene_context: Mapping[str, object],
deadline: float,
developer_instructions: str | None = None,
dynamic_override: bool | None = None,
enforce_required_controls: bool = True,
) -> dict[str, object]:
start_structured_turn = getattr(self.client, "start_structured_turn", None)
if not callable(start_structured_turn):
raise _ScenePlannerFallback("structured turns are not supported")
planning_started_at = time.monotonic()
output_schema = build_scene_output_schema(scene_context)
dynamic = (
needs_mode_schedule(message)
if dynamic_override is None
else dynamic_override
)
planner_context = deepcopy(dict(scene_context))
if dynamic:
# Timed IR playback is not yet phase-aware. Keep learned devices in
# inventory so the model can explain availability, but do not offer
# them as selectable actions in a schedule.
planner_context["peripheral_actions"] = []
dynamic_semantics = planner_context.get("scene_semantics")
dynamic_policy = (
dynamic_semantics.get("peripheral_policy")
if isinstance(dynamic_semantics, dict)
else None
)
if isinstance(dynamic_policy, dict):
dynamic_policy["available_action_count"] = 0
dynamic_policy["required_refs"] = []
dynamic_policy["required_reason"] = (
"Timed learned-infrared actions are not supported."
)
for item in planner_context.get("device_inventory", []):
if (
isinstance(item, dict)
and item.get("kind") == "learned_infrared"
and item.get("availability") == "available"
):
item["availability"] = "unavailable"
item["availability_reason"] = "timed_ir_not_supported"
output_schema = build_scene_output_schema(planner_context, dynamic=dynamic)
context_bytes = len(
json.dumps(
dict(scene_context),
planner_context,
ensure_ascii=False,
separators=(",", ":"),
).encode("utf-8")
@@ -1554,7 +2107,14 @@ class CodexAssistantService:
try:
try:
thread_id = self.client.start_thread(
developer_instructions=SCENE_COMPOSER_INSTRUCTIONS,
developer_instructions=(
developer_instructions
or (
SCHEDULE_COMPOSER_INSTRUCTIONS
if dynamic
else SCENE_COMPOSER_INSTRUCTIONS
)
),
ephemeral=True,
replace_developer_instructions=True,
timeout=self._remaining_scene_time(deadline),
@@ -1568,7 +2128,7 @@ class CodexAssistantService:
thread_id,
message,
application_context={
**dict(scene_context),
**planner_context,
"natural_language_routing": {
"original_utterance": message,
"parsed_clauses": [],
@@ -1693,7 +2253,12 @@ class CodexAssistantService:
raise SceneCompositionError("scene composer output was not valid JSON") from exc
if not isinstance(raw_output, Mapping):
raise SceneCompositionError("scene composer output must be an object")
normalized = normalize_scene_output(raw_output, scene_context)
normalized = normalize_scene_output(raw_output, planner_context, dynamic=dynamic)
if not dynamic and enforce_required_controls:
normalized = self._with_required_scene_peripherals(
normalized,
planner_context,
)
finished_at = time.monotonic()
LOGGER.info(
"Scene Composer model completed context_bytes=%d schema_bytes=%d "
@@ -2162,7 +2727,22 @@ class CodexAssistantService:
raise TypeError("vehicle control returned an invalid result")
safe_result = self._safe_vehicle_result(result)
action_state, successful, completion_text = _vehicle_action_state(result)
if mode_profile_preview:
if (
saved_mode_execution
and result.get("status") == "partially_succeeded"
):
successful = True
action_state = (
"simulated"
if result.get("execution_mode") != "hardware"
else "submitted"
)
completion_text = (
"功能模式模拟编排已完成"
if action_state == "simulated"
else "功能模式指令已提交"
)
elif mode_profile_preview:
successful = _mode_profile_result_succeeded(result)
action_state = "simulated" if successful else "failed"
completion_text = (
@@ -2229,7 +2809,9 @@ class CodexAssistantService:
"success": successful,
"state": action_state,
"display_text": (
"功能模式预览已生成"
completion_text
if action_state == "running" or result.get("orchestration_route") == "mode_schedule_stop"
else "功能模式预览已生成"
if mode_profile_preview and successful
else "功能模式预览生成失败"
if mode_profile_preview
@@ -2397,6 +2979,13 @@ class CodexAssistantService:
if isinstance(draft, Mapping)
else "新功能模式"
)
if isinstance(draft, Mapping) and isinstance(draft.get("schedule"), dict):
schedule = ModeSchedule.from_dict(draft["schedule"])
return (f"已为你生成“{name}”的分阶段时间轴:{len(schedule.phases)}个阶段,"
f"每轮{schedule.operation_count}项动作,重复{schedule.repeat_count}次,"
f"总时长{schedule.total_duration_ms // 1000}秒。"
"每个阶段的设备目标和持续时间见卡片。当前未保存或执行;确认后保存,进入模式才开始计时。"
"执行中可输入“停止动态模式”取消后续阶段,已下发目标不会自动回滚。")
if (
isinstance(draft, Mapping)
and draft.get("generation_source") == "draft_revision"
@@ -2447,11 +3036,11 @@ class CodexAssistantService:
if draft.get("generation_source") == "registered_mode_profile":
return (
f"工程中已有“{name}”,已按当前车型载入预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
)
return (
f"已按你提供的设置为现有“{name}”生成预览并验证"
f"{operation_count}项设备动作。可以先试运行,确认后更新现有模式。"
f"{operation_count}项设备动作。创建阶段未下发,确认后可更新现有模式。"
)
if (
isinstance(draft, Mapping)
@@ -2468,7 +3057,7 @@ class CodexAssistantService:
return (
f"已从常用模式模板快速生成“{name}”,并按当前工程能力验证了"
f"{operation_count}项设备动作。当前只是独立草稿,可以继续调整;"
"确认预览后可保存为新的功能模式,也可以先试运行。"
"创建阶段不会下发,确认预览后可保存为新的功能模式。"
)
if (
isinstance(draft, Mapping)
@@ -2484,9 +3073,9 @@ class CodexAssistantService:
)
return (
f"已理解“{name}”的场景意图,并从当前工程能力中组合、验证了"
f"{operation_count}项设备动作。确认预览后可保存为功能模式,也可以先试运行。"
f"{operation_count}项设备动作。创建阶段不会下发,确认预览后可保存。"
)
return f"已为你生成“{name}”预览。确认动作后可保存为功能模式,也可以先试运行。"
return f"已为你生成“{name}”预览。创建阶段不会下发,确认动作后可保存。"
@staticmethod
def _mode_profile_management_answer(
@@ -2601,6 +3190,18 @@ class CodexAssistantService:
result: Mapping[str, object],
successful: bool,
) -> str:
if result.get("orchestration_route") == "mode_schedule_stop":
if result.get("execution_mode") == "hardware" and (result.get("schedule_execution") or {}).get("status") != "cancelled":
return "当前没有正在执行的动态时间轴。"
return ("已停止动态模式后续阶段。已下发的设备目标保持,不自动回滚。"
if result.get("execution_mode") == "hardware"
else "已模拟停止动态模式,未改变正在运行的车辆任务。")
if result.get("orchestration_route") == "mode_schedule_execute":
if not successful:
return "时间轴未能完成,后续阶段已停止。请检查车辆状态后重试。"
return ("动态模式时间轴已启动,将按卡片中的时长逐阶段调节。尚未完成所有阶段;输入“停止动态模式”可取消后续阶段。"
if result.get("status") == "running"
else "已按虚拟时钟模拟验证动态模式的全部阶段,未向实际车辆下发操作。")
if reason in {
"exact_quick_control",
"explicit_registered_control",
@@ -3560,6 +4161,12 @@ class CodexAssistantService:
safe_target = CodexAssistantService._safe_result_value(
operation.get("target") or {}, secret=secret
)
if raw_domain == "self_extension" and isinstance(safe_target, dict):
safe_target = {
key: safe_target[key]
for key in ("device", "name")
if key in safe_target
}
safe_parameters = CodexAssistantService._safe_result_value(
operation.get("parameters") or {}, secret=secret
)
@@ -3640,6 +4247,7 @@ class CodexAssistantService:
"local_control_elapsed_ms",
"local_control_within_budget",
"natural_language_routing",
"parallel_dispatch",
):
if result.get(key) is not None:
payload[key] = CodexAssistantService._safe_result_value(
@@ -3672,24 +4280,37 @@ class CodexAssistantService:
if isinstance(intent_ir, dict):
spec = intent_ir.get("spec")
if isinstance(spec, dict) and isinstance(spec.get("actions"), list):
for item in spec["actions"][:12]:
for item in spec["actions"][
:96 if result.get("schedule") else MAX_PLAN_REQUESTS
]:
if not isinstance(item, dict):
continue
actions.append(
{
safe_action = {
key: CodexAssistantService._safe_result_value(
item[key],
secret=secret,
)
for key in ("domain", "capability", "target", "parameters")
if key in item
}
if (
item.get("domain") == "self_extension"
and isinstance(item.get("target"), Mapping)
):
safe_action["target"] = {
key: CodexAssistantService._safe_result_value(
item[key],
item["target"][key],
secret=secret,
)
for key in ("domain", "capability", "target", "parameters")
if key in item
for key in ("device", "name")
if key in item["target"]
}
)
actions.append(safe_action)
safe["actions"] = actions
infrared_actions: list[dict[str, object]] = []
raw_infrared = result.get("infrared_actions")
if isinstance(raw_infrared, list):
for item in raw_infrared[:12]:
for item in raw_infrared[:MAX_PLAN_REQUESTS]:
if not isinstance(item, dict):
continue
infrared_actions.append(
@@ -3740,7 +4361,7 @@ class CodexAssistantService:
for key in ("domain", "capability", "target_scope", "parameters")
if key in item
}
for item in mode_profile_changes[:12]
for item in mode_profile_changes[:MAX_PLAN_REQUESTS]
if isinstance(item, Mapping)
]
deleted_mode_profile = result.get("deleted_mode_profile")
@@ -3786,6 +4407,22 @@ class CodexAssistantService:
)
if key in mode_profile_draft
}
for key in ("mode_profile_draft", "mode_profile"):
source = result.get(key)
if isinstance(source, dict) and isinstance(source.get("schedule"), dict):
try:
safe[key]["schedule"] = ModeSchedule.from_dict(source["schedule"]).to_dict()
except ValueError:
pass
if isinstance(result.get("schedule"), dict):
try:
safe["schedule"] = ModeSchedule.from_dict(result["schedule"]).to_dict()
except ValueError:
pass
if isinstance(result.get("schedule_execution"), dict):
safe["schedule_execution"] = public_schedule_status(result["schedule_execution"])
if isinstance(result.get("vehicle"), dict):
safe["vehicle"] = {"id": str(result["vehicle"].get("id") or "")[:128]}
failure = result.get("mode_creation_failure")
if isinstance(failure, Mapping):
reason, next_step = CodexAssistantService._mode_profile_failure_feedback(
@@ -3823,6 +4460,11 @@ class CodexAssistantService:
}
if result.get("orchestration_route") is not None:
safe["orchestration_route"] = str(result["orchestration_route"])
if isinstance(result.get("parallel_dispatch"), Mapping):
safe["parallel_dispatch"] = CodexAssistantService._safe_result_value(
result["parallel_dispatch"],
secret=secret,
)
validation = result.get("validation")
if isinstance(validation, Sequence) and not isinstance(
validation,
@@ -17,6 +17,11 @@ from typing import Any
from cognitive_agent.application.extension_toy_publisher import (
normalize_extension_nfc_id,
)
from cognitive_agent.application.self_extension import (
is_peripheral_color_capability,
is_peripheral_power_off_capability,
is_peripheral_power_on_capability,
)
EXTENSION_CONTROL_TOPIC = "extension/ctl"
@@ -194,7 +199,7 @@ def authorize_extension_control_actions(
request: Mapping[str, Any],
catalog: object,
) -> list[dict[str, Any]]:
"""Authorize every action before any TCP command may be sent."""
"""Authorize every action and inject mandatory light power prerequisites."""
actions = request.get("actions")
if not isinstance(actions, list):
@@ -251,9 +256,86 @@ def authorize_extension_control_actions(
"action": deepcopy(dict(action)),
"capability_id": str(capability.get("id") or ""),
"extension_name": str(capability.get("device_name") or ""),
"capability": capability,
"auto_inserted": False,
"sequence_order": index * 2 + 1,
}
)
return authorized
allowed_by_device: dict[str, list[dict[str, Any]]] = {}
for capability in allowlist.values():
device_id = str(capability.get("device_id") or "").strip()
if device_id:
allowed_by_device.setdefault(device_id, []).append(capability)
expanded: list[dict[str, Any]] = []
powered_devices: set[str] = set()
next_index = len(actions)
for item in sorted(
authorized,
key=lambda value: (value["at_ms"], value["sequence_order"]),
):
capability = item["capability"]
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
powered_devices.add(device_id)
elif is_peripheral_power_off_capability(capability):
powered_devices.discard(device_id)
elif (
is_peripheral_color_capability(capability)
and device_id not in powered_devices
):
prerequisite = next(
(
candidate
for candidate in sorted(
allowed_by_device.get(device_id, []),
key=lambda value: str(value.get("id") or ""),
)
if is_peripheral_power_on_capability(candidate)
),
None,
)
if prerequisite is None:
raise ExtensionControlAuthorizationError(
"extension/ctl color action requires an enabled, NFC-bound "
f"power-on action for the same device: {device_id}"
)
prerequisite_slot = int(prerequisite["slot"])
prerequisite_action = str(prerequisite.get("action") or "").strip()
expanded.append(
{
"index": next_index,
"at_ms": item["at_ms"],
"action": {
"type": "infrared_emit",
"at_ms": item["at_ms"],
"domain": "self_extension",
"capability": "infrared.emit",
"target_scope": device_id,
"operation": "emit",
"parameters": {
"action": prerequisite_action,
"slot": prerequisite_slot,
},
},
"capability_id": str(prerequisite.get("id") or ""),
"extension_name": str(
prerequisite.get("device_name") or ""
),
"capability": prerequisite,
"auto_inserted": True,
"dependency": "power_on_before_color",
"sequence_order": int(item["sequence_order"]) - 1,
}
)
next_index += 1
powered_devices.add(device_id)
expanded.append(item)
if len(expanded) > MAX_EXTENSION_CONTROL_ACTIONS:
raise ExtensionControlAuthorizationError(
"extension/ctl exceeds the action limit after adding power-on prerequisites"
)
return expanded
class ExtensionControlConsumer:
@@ -477,7 +559,14 @@ class ExtensionControlConsumer:
self._record_rejection(exc, request=request)
return
ordered_actions = sorted(actions, key=lambda item: (item["at_ms"], item["index"]))
ordered_actions = sorted(
actions,
key=lambda item: (
item["at_ms"],
item.get("sequence_order", item["index"]),
item["index"],
),
)
action_statuses = [
{
"index": authorized["index"],
@@ -488,6 +577,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"state": "pending",
"state_label": "等待下发",
"state_detail": f"计划在接收后 {authorized['at_ms']} ms 下发",
@@ -583,6 +674,8 @@ class ExtensionControlConsumer:
"target_scope": authorized["action"]["target_scope"],
"action": authorized["action"]["parameters"]["action"],
"slot": authorized["action"]["parameters"]["slot"],
"auto_inserted": authorized.get("auto_inserted") is True,
"dependency": authorized.get("dependency"),
"transport": str(result.get("transport") or "tcp"),
"gateway_acknowledged": gateway_acknowledged,
"physical_state_confirmed": physical_state_confirmed,
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import re
import threading
import time
@@ -121,13 +122,14 @@ def build_extension_toy_messages(
class NfcExtensionToyPublisher:
"""Publish one three-frame ability burst for each NFC card-detection edge."""
"""Publish one three-frame burst per NFC edge or active-card binding change."""
def __init__(
self,
*,
nfc_provider: Callable[[], Mapping[str, Any]],
catalog_provider: Callable[[], list[dict[str, Any]]],
catalog_revision_provider: Callable[[], object] | None = None,
publisher: Callable[[Mapping[str, Any]], Mapping[str, Any]],
burst_count: int = EXTENSION_TOY_BURST_COUNT,
burst_window_s: float = EXTENSION_TOY_BURST_WINDOW_S,
@@ -148,6 +150,7 @@ class NfcExtensionToyPublisher:
self.poll_interval_s = float(poll_interval_s)
self._nfc_provider = nfc_provider
self._catalog_provider = catalog_provider
self._catalog_revision_provider = catalog_revision_provider
self._publisher = publisher
self._lock = threading.RLock()
self._stop = threading.Event()
@@ -161,6 +164,7 @@ class NfcExtensionToyPublisher:
"burst_count": self.burst_count,
"burst_window_ms": round(self.burst_window_s * 1_000),
"active_nfc_id": None,
"active_catalog_revision": None,
"active_extension_names": [],
"active_target_scopes": [],
"message_count": 0,
@@ -185,6 +189,7 @@ class NfcExtensionToyPublisher:
self._set_status(
state="waiting_for_nfc",
active_nfc_id=None,
active_catalog_revision=None,
active_extension_names=[],
active_target_scopes=[],
message_count=0,
@@ -206,6 +211,7 @@ class NfcExtensionToyPublisher:
self._set_status(
state="stopped",
active_nfc_id=None,
active_catalog_revision=None,
active_extension_names=[],
active_target_scopes=[],
message_count=0,
@@ -234,6 +240,11 @@ class NfcExtensionToyPublisher:
return ""
return normalize_extension_nfc_id(card.get("uid"))
def _catalog_revision(self) -> str:
if self._catalog_revision_provider is None:
return ""
return str(self._catalog_revision_provider() or "").strip()[:128]
def _publish_burst(
self,
card_uid: str,
@@ -325,6 +336,8 @@ class NfcExtensionToyPublisher:
def _run(self) -> None:
latched_uid = ""
latched_catalog_revision = ""
latched_message_signature: str | None = None
while not self._stop.is_set():
try:
card_uid = self._active_nfc_id()
@@ -333,12 +346,32 @@ class NfcExtensionToyPublisher:
self._stop.wait(self.poll_interval_s)
continue
if card_uid != latched_uid:
try:
catalog_revision = self._catalog_revision() if card_uid else ""
except Exception as exc: # noqa: BLE001 - retry while the card remains
self._set_status(
state="catalog_unavailable",
active_nfc_id=card_uid or None,
last_error=str(exc)[:240],
)
self._stop.wait(self.poll_interval_s)
continue
catalog_changed = bool(
card_uid
and card_uid == latched_uid
and self._catalog_revision_provider is not None
and catalog_revision != latched_catalog_revision
)
if card_uid != latched_uid or catalog_changed:
if not card_uid:
latched_uid = ""
latched_catalog_revision = ""
latched_message_signature = None
self._set_status(
state="waiting_for_nfc",
active_nfc_id=None,
active_catalog_revision=None,
active_extension_names=[],
active_target_scopes=[],
message_count=0,
@@ -358,10 +391,28 @@ class NfcExtensionToyPublisher:
last_error=str(exc)[:240],
)
else:
message_signature = json.dumps(
messages,
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
messages_changed = (
message_signature != latched_message_signature
)
# Latch only after a successful catalog read. A card that
# remains present never starts a second burst.
# remains present publishes again only when the abilities
# bound to that exact UID changed. Unrelated catalog
# revisions therefore do not duplicate a burst.
latched_uid = card_uid
if messages:
latched_catalog_revision = catalog_revision
latched_message_signature = message_signature
self._set_status(
active_catalog_revision=catalog_revision or None,
)
if not messages_changed:
pass
elif messages:
self._publish_burst(card_uid, messages)
else:
self._set_status(
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,76 @@
"""Seat selection for new mode drafts, not for replaying saved user modes."""
from __future__ import annotations
import re
from collections.abc import Mapping, Sequence
from copy import deepcopy
from typing import Any
from cognitive_agent.application.mode_schedule import ModeSchedule
from cognitive_agent.application.semantic_ir import resolve_scene_seat_scopes
class ModeCreationSeatPolicyError(ValueError):
"""A source mode cannot satisfy the selected seat without substitution."""
def creation_seat_scopes(text: str) -> frozenset[str]:
"""Default to passenger; HVAC/window zone names do not select a seat."""
normalized = text.replace("主副驾", "主驾和副驾")
# Do not let the generic 驾驶席/驾驶位 aliases inside 副驾驶席/位
# accidentally select the driver as well as the passenger.
normalized = re.sub(r"副驾驶(?:席|位|座(?!椅))?", "副驾", normalized)
if re.search(r"(?:未知|不明|不存在的?|任意)(?:一个)?(?:座位|座椅)|第[四五六七八九0-9]+排座", normalized):
return frozenset({"unsupported"})
normalized = re.sub(r"(?:前排)?两(?:个|张)?(?:座椅|座位|座)|双座(?:椅)?", "主驾和副驾", normalized)
excluded = set()
for scope, aliases in (("driver", r"主驾(?:驶)?|驾驶位|驾驶席|司机位"),
("passenger", r"副驾(?:驶)?|乘客位")):
pattern = re.compile(
rf"(?:不要|别|不用|无需|不需要|不涉及|不包含|排除)"
rf"(?:再|去|动|移动|调节|调整|控制|联动|编排|使用|添加|包含)*"
rf"(?:{aliases})(?:座椅|座位)?|"
rf"(?:{aliases})(?:座椅|座位)?(?:不动|不要动|不用动|不联动|不参与|不编排)"
)
if pattern.search(normalized):
excluded.add(scope)
normalized = pattern.sub("", normalized)
selected = resolve_scene_seat_scopes(normalized) or frozenset({"passenger"})
return frozenset(selected - excluded)
def normalized_seat_scope(scope: object) -> str:
value = str(scope or "")
return {"front_left": "driver", "front_right": "passenger"}.get(value, value)
def project_seed_operations(
operations: Sequence[Mapping[str, Any]], scopes: frozenset[str],
) -> list[dict[str, Any]]:
"""Choose from trusted seat variants. Never retarget a passenger capability."""
if "unsupported" in scopes:
raise ModeCreationSeatPolicyError("当前功能模式仅支持明确的主驾或副驾座椅,不会替换为其他座位。")
seats = [op for op in operations if op.get("domain") == "seat"]
if any(normalized_seat_scope(op.get("target_scope")) not in {"driver", "passenger"} for op in seats):
raise ModeCreationSeatPolicyError("模式包含无法验证的座位范围,请重新生成。")
projected = [deepcopy(dict(op)) for op in operations
if op.get("domain") != "seat" or normalized_seat_scope(op.get("target_scope")) in scopes]
if seats and not any(op.get("domain") == "seat" for op in projected):
target = "主驾" if scopes == {"driver"} else "副驾" if scopes == {"passenger"} else "所选座位"
raise ModeCreationSeatPolicyError(f"该模式没有支持{target}的座椅动作,不会把另一座位的能力冒充为{target}能力。")
return projected
def project_seed_plan(plan: Mapping[str, Any], scopes: frozenset[str]) -> dict[str, Any]:
projected = deepcopy(dict(plan))
projected["requests"] = project_seed_operations(plan.get("requests", []), scopes)
projected["detected_domains"] = list(dict.fromkeys(op["domain"] for op in projected["requests"]))
return projected
def project_seed_schedule(schedule: ModeSchedule, scopes: frozenset[str]) -> ModeSchedule:
document = schedule.to_dict()
for phase in document["phases"]:
phase["operations"] = project_seed_operations(phase["operations"], scopes)
return ModeSchedule.from_dict(document)
@@ -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
@@ -13,6 +13,8 @@ from typing import Any
import yaml
from cognitive_agent.application.mode_schedule import ModeSchedule
_MATCH_FORMATTING = re.compile(r"[\s()()]+")
_MODE_INVOCATION_FORMATTING = re.compile(r"[\s,。!?,.!?::;;()()]+")
_NON_ASCII_SLUG = re.compile(r"[^a-z0-9]+")
@@ -518,6 +520,7 @@ class ModeProfile:
command_flow: dict[str, Any] | None = None
sync_origin: str = ""
upstream_origin: str = ""
schedule: ModeSchedule | None = None
@classmethod
def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile":
@@ -558,10 +561,12 @@ class ModeProfile:
upstream_origin=_clean_upstream_origin(
document.get("upstream_origin")
),
schedule=(ModeSchedule.from_dict(document["schedule"])
if document.get("schedule") is not None else None),
)
def public_dict(self) -> dict[str, Any]:
is_cyclic = self.timeline is not None or any(
is_cyclic = self.schedule is not None or self.timeline is not None or any(
operation.capability == "seat.start_dynamic_mode"
for operation in self.operations
)
@@ -595,6 +600,9 @@ class ModeProfile:
result["upstream_origin"] = self.upstream_origin or None
if self.timeline is not None:
result["timeline"] = self.timeline.public_dict()
if self.schedule is not None:
result["schedule"] = self.schedule.to_dict()
result["operation_count"] = self.schedule.operation_count
return result
@@ -779,6 +787,7 @@ class ModeProfileRegistry:
},
"operations": [operation.to_yaml() for operation in profile.operations],
"timeline": profile.timeline.to_yaml() if profile.timeline is not None else None,
"schedule": profile.schedule.to_dict() if profile.schedule is not None else None,
"instruction": profile.instruction,
"command_flow": profile.command_flow,
"sync_origin": profile.sync_origin,
@@ -815,6 +824,7 @@ class ModeProfileRegistry:
timeline: ModeTimeline | None = None,
sync_origin: str = "",
upstream_origin: str = "",
schedule: ModeSchedule | None = None,
expected_revision: str | None = None,
create_only: bool = False,
) -> ModeProfile:
@@ -857,6 +867,8 @@ class ModeProfileRegistry:
if clean_sync_origin:
document["sync_origin"] = clean_sync_origin
document["upstream_origin"] = clean_upstream_origin
if schedule is not None:
document["schedule"] = schedule.to_dict()
path = (
self.user_root / "vehicles" / vehicle_id / "user" / f"{profile_id}.yaml"
if vehicle_id
@@ -1128,6 +1140,8 @@ def _mode_profile_document(
}
if profile.timeline is not None:
document["timeline"] = profile.timeline.to_yaml()
if profile.schedule is not None:
document["schedule"] = profile.schedule.to_dict()
if profile.instruction:
document["instruction"] = profile.instruction
if profile.command_flow is not None:
@@ -9,12 +9,37 @@ _MODE_NAME = r"(?P<name>[^,。,.!!::\s“”\"'「」『』《》]{1,64}?
_QUOTED_MODE_NAME = (
r"(?P<open>[“\"'「『《]?)" + _MODE_NAME + r"(?P<close>[”\"'」』》]?)"
)
_CREATION_DUAL_SEAT_NAMES = (
r"主驾(?:和|与|及)副驾|主副驾|双座(?:椅)?|(?:前排)?两(?:个|张)?(?:座椅|座位|座)"
)
_CREATION_SEAT_NAMES = (
rf"{_CREATION_DUAL_SEAT_NAMES}|"
r"副驾驶席|副驾驶位|副驾驶|副驾|乘客座椅|乘客席|"
r"主驾驶席|主驾驶位|主驾驶|主驾|驾驶席|驾驶位"
)
_CREATION_SEAT_PREFIX = re.compile(
rf"^(?:为|给|针对|供)?(?P<seat>{_CREATION_SEAT_NAMES})"
r"(?:(?:座椅|座位|座席)?(?:专属|专用))?的?"
)
_CREATION_SEAT_CLAUSE = re.compile(
rf"^(?:仅|只)?(?:为|给|针对|适用于|用于|面向|联动|编排)"
rf"(?P<seat>{_CREATION_SEAT_NAMES})(?:座椅|座位|座席)?"
r"(?:使用|联动|编排|的能力|的动作)?$"
)
_CREATION_SEAT_GENERIC_SCENE = re.compile(
r"^(?:座椅|座位|座席)的?(?P<scene>"
r"阅读|看书|读书|办公|观影|看电影|电影|露营)模式$"
)
_CREATION_SEAT_FUNCTION_SUBJECT = re.compile(
r"^(?:舒适|舒服|舒缓|放松|动态|循环|通风|加热|按摩|靠背|腿托|坐垫).{0,32}模式$"
)
_MODE_PROFILE_CREATE_HEAD = re.compile(
r"^\s*"
r"(?:(?:请问(?:可以|能不能|能否|可不可以|能)帮我|"
r"(?:可以|能不能|能否|可不可以|能)帮我|"
r"我(?:想要|想|希望|需要|打算|准备)(?:让你)?|"
r"想要|想|希望|需要|打算|准备|请你?|麻烦你?|帮我|帮忙|给我|先)\s*){0,6}"
rf"(?:(?:为|给|针对)(?P<seat>{_CREATION_SEAT_NAMES})(?:座椅|座位|座席)?\s*)?"
r"(?P<verb>构建|创建|新建|新增|生成|添加)\s*"
r"(?:一下\s*)?(?:一个|个|一套|套)?\s*"
r"(?P<naming>命名为|名称为|取名为|名叫|名为|叫做)?\s*"
@@ -34,6 +59,9 @@ _MODE_PROFILE_PURPOSE_WRAPPERS = (
r"^(?:可以|能够|能)?让(?:我们|我|人们?|乘员们?|乘客们?|大家)"
r"(?P<purpose>.+?)的(?:功能|场景|自定义)?模式$"
),
re.compile(
r"^(?P<purpose>(?:座椅|座位|乘坐|坐着|上车|空调).+?)的(?:功能|场景|自定义)?模式$"
),
)
_MODE_PROFILE_PURPOSE_INVALID = frozenset(
{"", "功能", "场景", "自定义", "使用", "日常使用", "合适", "需要"}
@@ -67,6 +95,7 @@ _MODE_PROFILE_CREATE_CAPABILITY_CLAUSE = re.compile(
)
_MODE_PROFILE_CREATE_ASSISTED_REQUEST = re.compile(
r"^(?:请问)?(?:可以|能不能|能否|可不可以|能)帮我(?:先)?"
rf"(?:(?:为|给|针对)(?:{_CREATION_SEAT_NAMES})(?:座椅|座位|座席)?)?"
r"(?:构建|创建|新建|新增|生成|添加)"
)
_MODE_PROFILE_CREATE_SUFFIX_UNIT = re.compile(
@@ -141,7 +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"(?:把|将|在|给|为|往|向|修改|调整|更改|更新|变更)"
)
_MODE_POSSESSIVE_BOUNDARY = r"(?:中的|里的|内的|之中的|的|中|里|内)"
_MODE_MEMBER_BOUNDARY = rf"(?:{_MODE_POSSESSIVE_BOUNDARY}|[,,::])"
@@ -164,16 +198,30 @@ _CONTEXTUAL_NAME_FIRST_UPDATE = re.compile(
rf"^{_LEADING_POLITE}{_QUOTED_MODE_NAME}[,,::]"
r"(?P<change>.+?)[。.!!]?$"
)
_MODE_PROFILE_APPEND_VERB = r"(?:加入|添加|增加|补充|加上|追加|新增)"
_APPEND_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:在|给|为|往|向)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?[,,::]?"
rf"(?:再|还|也|同时|另外)?(?P<change>{_MODE_PROFILE_APPEND_VERB}.+?)[。.!!]?$"
)
_APPEND_TO_MODE_UPDATE = re.compile(
rf"^{_LEADING_POLITE}(?:把|将)(?P<change>.+?)"
rf"(?P<append_verb>{_MODE_PROFILE_APPEND_VERB})(?:到|进)?{_QUOTED_MODE_NAME}"
rf"(?:{_MODE_POSSESSIVE_BOUNDARY})?(?:吧|呀|啊|哦|谢谢(?:你)?)?[。.!!]?$"
)
MODE_PROFILE_CHANGE_PREDICATE = re.compile(
rf"{_MODE_PROFILE_APPEND_VERB}|"
r"修改|改成|改为|更改|更新|变更|变成|变为|换成|"
r"调整|调节|调高|调低|调到|调成|调为|调至|"
r"设置|设为|设成|设到|设至|"
r"打开|开启|启动|关闭|关掉|关上|全关|停止|停用|切换|进入|退出|"
r"移动|前移|后退|前进|升高|降低|抬高|放低|降下|升起|调直|放倒|"
r"复位|半开|开到|关到|拉开|拉上|上锁|解锁"
r"复位|半开|开到|关到|拉开|拉上|上锁|解锁|"
r"(?:持续|保持)(?:时间)?(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)(?:分钟|秒钟|秒|s)|"
r"(?:重复|循环)(?:[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+)次"
)
MODE_PROFILE_QUERY_CUE = re.compile(
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|"
r"什么|哪些|多少|几(?:度|档|级|个|项)?|是否|是不是|能否|能不能|可不可以|会不会|"
r"合适(?:吗)?|合理(?:吗)?|会(?:怎么样|怎样|如何)|"
r"怎么|如何|列(?:出|一下)|介绍(?:一下)?|说明(?:一下)?|"
r"告诉我|查询|查看|有什么作用|是何作用|还是|[??]|(?:吗|么|呢)$"
@@ -230,6 +278,41 @@ def normalize_mode_profile_name(value: object) -> str:
return compact_mode_profile_text(value).strip("“”\"'「」『』《》").casefold()
def _creation_seat_label(value: str) -> str:
if re.fullmatch(_CREATION_DUAL_SEAT_NAMES, value):
return "主副驾"
return "副驾" if value.startswith(("副", "乘客")) else "主驾"
def mode_creation_source_name(value: object) -> str:
"""Remove only an explicit front-seat qualifier for source lookup.
A scoped draft keeps its distinct display name. This helper only allows its
existing scene seed to be located; it never authorizes any seat operation or
rewrites an unsupported rear/unknown target into a front seat.
"""
name = compact_mode_profile_text(value).strip("“”\"'「」『』《》")
match = _CREATION_SEAT_PREFIX.match(name)
if match is None:
return name
subject = name[match.end():]
if not subject or subject == "模式" or not subject.endswith("模式"):
return name
# A seat location before a generic activity is a scope wrapper. Keep the
# actual device topic in 座椅舒适/座椅动态 rather than dropping every 座椅.
generic_scene = _CREATION_SEAT_GENERIC_SCENE.fullmatch(subject)
if generic_scene is not None:
subject = f"{generic_scene.group('scene')}模式"
elif (
match.group("seat").endswith(("座椅", "座位", "座"))
and _CREATION_SEAT_FUNCTION_SUBJECT.fullmatch(subject)
):
subject = f"座椅{subject}"
inferred = _infer_mode_profile_purpose_name(subject)
return inferred[0] if inferred is not None else subject
def _infer_mode_profile_purpose_name(value: object) -> tuple[str, str] | None:
"""Turn one bounded purpose description into a concise mode name.
@@ -445,12 +528,24 @@ def parse_mode_profile_create(value: object) -> tuple[str, str] | None:
# Keep explicit user names byte-for-byte while giving inferred scenes a
# concise, reusable name (for example 看书模式 instead of
# 适合看书的功能模式).
if not str(match.group("naming") or "") and not opening:
explicit_name = bool(str(match.group("naming") or "") or opening)
if not explicit_name:
inferred_purpose = _infer_mode_profile_purpose_name(name)
if inferred_purpose is not None:
name = inferred_purpose[0]
seat_prefix = _CREATION_SEAT_PREFIX.match(name)
if seat_prefix is not None and mode_creation_source_name(name) != name:
name = _creation_seat_label(seat_prefix.group("seat")) + mode_creation_source_name(name)
if match.group("seat") and seat_prefix is None:
name = _creation_seat_label(match.group("seat")) + name
if normalize_mode_profile_name(name) in _GENERIC_MODE_PROFILE_CREATE_NAMES:
return None
name_seat = _CREATION_SEAT_PREFIX.match(name)
if (
match.group("seat") and name_seat is not None
and _creation_seat_label(match.group("seat")) != _creation_seat_label(name_seat.group("seat"))
):
return None
raw_match_end = raw_ends[match.end() - 1]
if "\n" in raw[:raw_match_end] or "\r" in raw[:raw_match_end]:
@@ -495,6 +590,19 @@ def parse_mode_profile_create(value: object) -> tuple[str, str] | None:
return name, ""
instruction = _strip_mode_profile_create_suffix(instruction)
seat_clause = _CREATION_SEAT_CLAUSE.fullmatch(compact_mode_profile_text(instruction))
if seat_clause is not None:
# A seat-only clause selects the draft, not an extra actuator command.
# Real device settings in a following clause remain untouched below.
name_seat = _CREATION_SEAT_PREFIX.match(name)
if (
name_seat is not None
and _creation_seat_label(name_seat.group("seat")) != _creation_seat_label(seat_clause.group("seat"))
):
return None
if not explicit_name and name_seat is None:
name = _creation_seat_label(seat_clause.group("seat")) + name
return name, ""
if (
not instruction
or len(instruction) > 800
@@ -516,6 +624,8 @@ def _candidate_matches(
matches: list[tuple[re.Match[str], bool]] = []
for pattern, predicate_required in (
(_PREFIX_UPDATE, False),
(_APPEND_MODE_UPDATE, True),
(_APPEND_TO_MODE_UPDATE, False),
(_BA_UPDATE, True),
(_NAME_FIRST_UPDATE, True),
):
@@ -534,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 ``把<same mode>的`` wrapper from an update body."""
@@ -613,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)
):
@@ -634,6 +749,7 @@ def parse_mode_profile_update(
text = compact_mode_profile_text(value)
if (
not text
or _MODE_PROFILE_UPDATE_PREFIX_NEGATION.search(text)
or _MODE_PROFILE_CREATE_REFERENCE_CLAUSE.search(text)
or _MODE_PROFILE_CREATE_UNSAFE_PREFIX.search(text)
):
@@ -650,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)
@@ -267,6 +267,33 @@ def _scene_from_profile(
for operation, target, operation_ref in effective_operations
]
)
local_transfer_actions = (
profile.command_flow.get("n7_transfer_actions")
if not profile.sync_origin
and isinstance(profile.command_flow, dict)
and isinstance(profile.command_flow.get("n7_transfer_actions"), list)
else []
)
transfer_by_identity: dict[str, dict[str, Any]] = {}
for raw_action in local_transfer_actions:
if not isinstance(raw_action, dict):
continue
try:
transfer_operation = ModeOperation.from_dict(raw_action)
except (KeyError, TypeError, ValueError):
continue
transfer_by_identity[_operation_identity(transfer_operation)] = deepcopy(
raw_action
)
if transfer_by_identity:
actions = [
transfer_by_identity.get(_operation_identity(operation), action)
for (operation, _, _), action in zip(
effective_operations,
actions,
strict=True,
)
]
scene = {
"id": profile.id,
"label": profile.name,
@@ -296,6 +323,41 @@ def _scene_from_profile(
if profile.sync_origin:
scene["source_topic"] = profile.sync_origin
scene["upstream_origin"] = profile.upstream_origin or None
if profile.schedule is not None:
schedule = profile.schedule
preview_actions = []
elapsed_ms = 0
for phase in schedule.phases:
for raw_operation in phase.operations:
action = _scene_action(ModeOperation.from_dict(raw_operation))
action.update(
at_ms=elapsed_ms,
phase_id=phase.id,
phase_name=phase.name,
phase_duration_ms=phase.duration_ms,
)
preview_actions.append(action)
elapsed_ms += phase.duration_ms
# A v1 client may ignore timestamps and immediately send every action.
# Keep its executable list empty rather than flattening a new timeline.
# Schedule-aware clients can render the complete contract and ask the
# authenticated application to execute the saved mode by its identity.
scene.update(
behavior="cyclic",
operation_count=schedule.operation_count,
domains=sorted({op["domain"] for phase in schedule.phases for op in phase.operations}),
actions=[],
schedule=schedule.to_dict(),
preview_actions=preview_actions,
preview_actions_scope="single_cycle",
execution={
"kind": "server_schedule",
"mode_id": profile.id,
"client_actions_supported": False,
"requires_authentication": True,
},
execution_notice="分阶段动态模式需通过工程服务端时间轴执行,不支持按静态动作列表下发。",
)
return scene
@@ -0,0 +1,429 @@
"""Bounded, cancellable timelines for validated absolute cockpit operations.
This module validates the *schedule* contract, not vehicle authorization. Callers
must validate every phase against the current registry and execute it through the
ordinary compiler/safety path. It deliberately has no CAN or ModeProfile imports.
Durations are holds after one phase dispatch, never actuator motion durations.
Cancelling prevents future phases; it does not roll back an already applied state.
"""
from __future__ import annotations
import math
import re
import time
from collections.abc import Callable
from copy import deepcopy
from dataclasses import dataclass, field
from threading import Event, Lock, RLock, Thread, current_thread
from typing import Any
from uuid import uuid4
from cognitive_agent.application.assistant_plan_guard import SCENE_COMPOSABLE_CAPABILITIES
MODE_SCHEDULE_SCHEMA = "cognitive-agent-mode-schedule/v1"
MAX_SCHEDULE_DURATION_MS = 60 * 60 * 1000
_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
_FORBIDDEN_PARAMETERS = frozenset(
{"duration", "duration_ms", "seconds", "delta", "relative", "relative_pct",
"direction", "toggle", "repeat", "repeat_count", "axis"}
)
_FAILURE_STATUSES = frozenset(
{"failed", "error", "cancelled", "canceled", "rejected", "blocked", "aborted"}
)
_SUCCESS_STATUSES = frozenset({"succeeded", "success", "ok", "completed"})
def _object(value: Any, fields: set[str], required: set[str], label: str) -> dict[str, Any]:
if not isinstance(value, dict) or any(not isinstance(key, str) for key in value):
raise ValueError(f"{label} must be an object with string keys")
if unknown := set(value) - fields:
raise ValueError(f"{label} contains unknown fields: {', '.join(sorted(unknown))}")
if missing := required - set(value):
raise ValueError(f"{label} is missing fields: {', '.join(sorted(missing))}")
return value
def _text(value: Any, label: str, maximum: int, *, allow_empty: bool = False) -> str:
if not isinstance(value, str) or len(value) > maximum:
raise ValueError(f"{label} must be a string of at most {maximum} characters")
if not allow_empty and not value.strip():
raise ValueError(f"{label} must not be empty")
if "\x00" in value:
raise ValueError(f"{label} contains a null character")
return value.strip()
def _integer(value: Any, minimum: int, maximum: int, label: str) -> int:
if type(value) is not int or not minimum <= value <= maximum:
raise ValueError(f"{label} must be an integer between {minimum} and {maximum}")
return value
def _operation(value: Any) -> dict[str, Any]:
required = {"domain", "capability", "target_scope", "parameters"}
document = _object(value, required | {"operation", "reason"}, required, "schedule operation")
domain = _text(document["domain"], "operation domain", 64)
capability = _text(document["capability"], "operation capability", 128)
if capability not in SCENE_COMPOSABLE_CAPABILITIES or not capability.startswith(domain + "."):
raise ValueError("schedule capability must be a stable, scene-composable capability")
if document.get("operation", "set") != "set":
raise ValueError("schedule operations must use absolute set operations")
parameters = document["parameters"]
if not isinstance(parameters, dict) or not 1 <= len(parameters) <= 12:
raise ValueError("schedule operation parameters must contain 1 to 12 scalar fields")
for key, parameter in parameters.items():
_text(key, "parameter name", 64)
if key.casefold() in _FORBIDDEN_PARAMETERS:
raise ValueError("schedule operations cannot contain relative, motion or timing parameters")
if parameter is None or type(parameter) not in (str, int, float, bool):
raise ValueError("schedule parameter values must be non-null JSON scalars")
if type(parameter) is float and not math.isfinite(parameter):
raise ValueError("schedule numeric parameters must be finite")
if isinstance(parameter, str):
_text(parameter, "parameter value", 256)
if parameter.casefold() in {"toggle", "increase", "decrease", "relative"}:
raise ValueError("schedule operations cannot request relative state changes")
return {
"domain": domain,
"capability": capability,
"target_scope": _text(document["target_scope"], "operation target_scope", 128),
"operation": "set",
"parameters": deepcopy(parameters),
"reason": _text(document.get("reason", ""), "operation reason", 1000, allow_empty=True),
}
def _operation_key(operation: dict[str, Any]) -> tuple[str, str]:
target = operation["target_scope"]
if operation["domain"] == "seat":
target = {"front_left": "driver", "front_right": "passenger"}.get(target, target)
return operation["capability"], target
@dataclass(frozen=True)
class ModeSchedulePhase:
id: str
name: str
duration_ms: int
operations: tuple[dict[str, Any], ...]
def __post_init__(self) -> None:
phase_id = _text(self.id, "schedule phase id", 64)
if not _ID_PATTERN.fullmatch(phase_id):
raise ValueError("schedule phase id must contain lowercase letters, numbers, '_' or '-'")
object.__setattr__(self, "id", phase_id)
object.__setattr__(self, "name", _text(self.name, "schedule phase name", 128))
_integer(self.duration_ms, 1000, 15 * 60 * 1000, "schedule phase duration_ms")
if not isinstance(self.operations, (list, tuple)) or not 1 <= len(self.operations) <= 12:
raise ValueError("schedule phase must contain 1 to 12 operations")
operations = tuple(_operation(operation) for operation in self.operations)
keys = [_operation_key(operation) for operation in operations]
if len(set(keys)) != len(keys):
raise ValueError("schedule phase contains duplicate capability/target operations")
object.__setattr__(self, "operations", operations)
@classmethod
def from_dict(cls, document: dict[str, Any]) -> ModeSchedulePhase:
fields = {"id", "name", "duration_ms", "operations"}
value = _object(document, fields, fields, "schedule phase")
if not isinstance(value["operations"], list):
raise ValueError("schedule phase operations must be an array") # noqa: TRY004
return cls(value["id"], value["name"], value["duration_ms"], tuple(value["operations"]))
def to_dict(self) -> dict[str, Any]:
return {
"id": self.id,
"name": self.name,
"duration_ms": self.duration_ms,
"operations": deepcopy(list(self.operations)),
}
@dataclass(frozen=True)
class ModeSchedule:
phases: tuple[ModeSchedulePhase, ...]
repeat_count: int = 1
schema: str = MODE_SCHEDULE_SCHEMA
def __post_init__(self) -> None:
if self.schema != MODE_SCHEDULE_SCHEMA:
raise ValueError("unsupported mode schedule schema")
_integer(self.repeat_count, 1, 20, "schedule repeat_count")
if not isinstance(self.phases, (list, tuple)) or not 2 <= len(self.phases) <= 8:
raise ValueError("schedule must contain 2 to 8 phases")
if any(not isinstance(phase, ModeSchedulePhase) for phase in self.phases):
raise ValueError("schedule phases must be ModeSchedulePhase objects")
object.__setattr__(self, "phases", tuple(self.phases))
if len({phase.id for phase in self.phases}) != len(self.phases):
raise ValueError("schedule phase ids must be unique")
if self.total_duration_ms > MAX_SCHEDULE_DURATION_MS:
raise ValueError("schedule total duration including repetitions must not exceed 60 minutes")
@property
def total_duration_ms(self) -> int:
return sum(phase.duration_ms for phase in self.phases) * self.repeat_count
@property
def operation_count(self) -> int:
"""Number of operations in one full repetition."""
return sum(len(phase.operations) for phase in self.phases)
@classmethod
def from_dict(cls, document: dict[str, Any]) -> ModeSchedule:
fields = {"schema", "repeat_count", "phases"}
value = _object(document, fields, fields, "schedule")
if not isinstance(value["phases"], list):
raise ValueError("schedule phases must be an array") # noqa: TRY004
return cls(
phases=tuple(ModeSchedulePhase.from_dict(phase) for phase in value["phases"]),
repeat_count=value["repeat_count"],
schema=value["schema"],
)
def to_dict(self) -> dict[str, Any]:
return {
"schema": self.schema,
"repeat_count": self.repeat_count,
"phases": [phase.to_dict() for phase in self.phases],
}
ExecutePhase = Callable[[ModeSchedulePhase], dict[str, Any]]
WaitFunction = Callable[[Event, float], bool]
def _result_error(result: Any) -> str | None:
if not isinstance(result, dict):
return "phase executor did not return an object with an explicit success result"
status = result.get("status")
if result.get("success") is False or result.get("error") or status in _FAILURE_STATUSES:
return str(result.get("error") or result.get("message") or "phase execution failed")[:1000]
if result.get("success") is True or status in _SUCCESS_STATUSES:
return None
return str(result.get("error") or "phase executor did not confirm successful execution")[:1000]
@dataclass
class _Run:
vehicle_id: str
session_id: str
schedule: ModeSchedule
started_at: float
run_id: str = field(default_factory=lambda: uuid4().hex)
cancelled: Event = field(default_factory=Event)
thread: Thread | None = None
status: str = "running"
phase_index: int = -1
repeat_index: int = 0
executed_phases: int = 0
error: str | None = None
cancel_reason: str | None = None
finished_at: float | None = None
class ModeScheduleRunner:
"""At most one active timeline and one phase callback per vehicle.
A replacement cancels its predecessor immediately and waits for any already
running phase callback to return before dispatching. Callbacks must themselves
be bounded and cancellation never implies undoing a previously sent operation.
``wait(event, seconds)`` may be injected for deterministic tests; like Event.wait
it returns true on cancellation. No callback runs while the state lock is held.
"""
def __init__(
self,
*,
clock: Callable[[], float] = time.monotonic,
wait: WaitFunction | None = None,
) -> None:
self._clock = clock
self._wait = wait or (lambda event, seconds: event.wait(seconds))
self._lock = RLock()
self._runs: dict[str, _Run] = {}
self._dispatch_locks: dict[str, Lock] = {}
self._workers: dict[str, _Run] = {}
self._closed = False
def _snapshot(self, run: _Run) -> dict[str, Any]:
end = run.finished_at if run.finished_at is not None else self._clock()
return {
"run_id": run.run_id,
"vehicle_id": run.vehicle_id,
"session_id": run.session_id,
"status": run.status,
"phase_index": run.phase_index,
"repeat_index": run.repeat_index,
"executed_phases": run.executed_phases,
"phase_count": len(run.schedule.phases),
"repeat_count": run.schedule.repeat_count,
"elapsed_ms": max(0, round((end - run.started_at) * 1000)),
"total_duration_ms": run.schedule.total_duration_ms,
"completed": run.status == "completed",
"error": run.error,
"cancel_reason": run.cancel_reason,
}
def status(self, vehicle_id: str) -> dict[str, Any]:
with self._lock:
run = self._runs.get(vehicle_id)
if run is not None:
return self._snapshot(run)
return {
"vehicle_id": vehicle_id, "status": "idle", "phase_index": -1,
"repeat_index": 0, "completed": False, "error": None,
}
def _cancel(self, run: _Run, reason: str) -> None:
if run.status == "running":
run.cancelled.set()
run.status = "cancelled"
run.cancel_reason = reason
run.finished_at = self._clock()
def cancel(self, vehicle_id: str, *, session_id: str | None = None) -> dict[str, Any]:
"""Cancel a vehicle timeline, optionally only if it belongs to this session."""
with self._lock:
run = self._runs.get(vehicle_id)
if run is not None and (session_id is None or session_id == run.session_id):
self._cancel(run, "cancelled")
return self.status(vehicle_id)
def start(
self,
vehicle_id: str,
schedule: ModeSchedule,
execute_phase: ExecutePhase,
*,
session_id: str = "",
) -> dict[str, Any]:
vehicle_id = _text(vehicle_id, "vehicle_id", 128)
session_id = _text(session_id, "session_id", 256, allow_empty=True)
if not isinstance(schedule, ModeSchedule) or not callable(execute_phase):
raise ValueError("start requires a ModeSchedule and callable phase executor") # noqa: TRY004
# Snapshot/revalidate to prevent caller-owned parameter dictionaries from
# altering the executable contract after the runner has accepted it.
accepted = ModeSchedule.from_dict(schedule.to_dict())
with self._lock:
if self._closed:
raise RuntimeError("mode schedule runner is closed")
previous = self._runs.get(vehicle_id)
if previous is not None:
self._cancel(previous, "replaced")
dispatch_lock = self._dispatch_locks.setdefault(vehicle_id, Lock())
run = _Run(vehicle_id, session_id, accepted, self._clock())
self._runs[vehicle_id] = run
self._workers[run.run_id] = run
run.thread = Thread(
target=self._run, args=(run, dispatch_lock, execute_phase),
name=f"mode-schedule-{run.run_id[:8]}", daemon=True,
)
try:
run.thread.start()
except RuntimeError:
run.status = "failed"
run.error = "schedule worker could not start"
run.finished_at = self._clock()
self._workers.pop(run.run_id, None)
raise
return self._snapshot(run)
def _run(self, run: _Run, dispatch_lock: Lock, execute_phase: ExecutePhase) -> None:
try:
for repeat_index in range(run.schedule.repeat_count):
for phase_index, phase in enumerate(run.schedule.phases):
# A cancelled replacement must not wait indefinitely behind
# an unrelated in-flight callback just to observe cancellation.
while not dispatch_lock.acquire(timeout=0.05):
if run.cancelled.is_set():
return
try:
with self._lock:
if run.cancelled.is_set():
return
run.repeat_index = repeat_index
run.phase_index = phase_index
# Isolate executor mutations from subsequent repetitions.
result = execute_phase(ModeSchedulePhase.from_dict(phase.to_dict()))
finally:
dispatch_lock.release()
with self._lock:
if run.cancelled.is_set():
return
if error := _result_error(result):
run.status = "failed"
run.error = error
run.finished_at = self._clock()
return
run.executed_phases += 1
if self._wait(run.cancelled, phase.duration_ms / 1000):
with self._lock:
self._cancel(run, "cancelled")
return
if run.cancelled.is_set():
return
with self._lock:
if not run.cancelled.is_set():
run.status = "completed"
run.finished_at = self._clock()
except Exception as exc: # noqa: BLE001 - any executor failure must stop later phases
with self._lock:
if not run.cancelled.is_set():
run.status = "failed"
run.error = f"{type(exc).__name__}: {exc}"[:1000]
run.finished_at = self._clock()
finally:
with self._lock:
self._workers.pop(run.run_id, None)
@staticmethod
def simulate(schedule: ModeSchedule, execute_phase: ExecutePhase) -> dict[str, Any]:
"""Validate/execute phases synchronously with virtual holds and no thread.
The supplied callback must select the ordinary *dry-run* execution path;
this method never changes an executor's hardware policy implicitly.
"""
if not isinstance(schedule, ModeSchedule) or not callable(execute_phase):
raise ValueError("simulate requires a ModeSchedule and callable phase executor") # noqa: TRY004
schedule = ModeSchedule.from_dict(schedule.to_dict())
report: dict[str, Any] = {
"status": "completed", "completed": True, "error": None,
"execution_mode": "dry_run", "virtual_elapsed_ms": 0,
"total_duration_ms": schedule.total_duration_ms,
"phase_index": -1, "repeat_index": 0, "executed_phases": 0,
"phase_results": [],
}
for repeat_index in range(schedule.repeat_count):
for phase_index, phase in enumerate(schedule.phases):
report.update(phase_index=phase_index, repeat_index=repeat_index)
try:
result = execute_phase(ModeSchedulePhase.from_dict(phase.to_dict()))
error = _result_error(result)
except Exception as exc: # noqa: BLE001 - simulation uses the same fail-closed policy
result = None
error = f"{type(exc).__name__}: {exc}"[:1000]
report["phase_results"].append({
"phase_id": phase.id, "phase_index": phase_index,
"repeat_index": repeat_index, "duration_ms": phase.duration_ms,
"virtual_start_ms": report["virtual_elapsed_ms"], "result": result,
})
if error:
report.update(status="failed", completed=False, error=error)
return report
report["executed_phases"] += 1
report["virtual_elapsed_ms"] += phase.duration_ms
return report
def close(self, *, timeout: float = 2.0) -> bool:
"""Cancel all waits; return whether bounded, already-running callbacks exited."""
with self._lock:
self._closed = True
workers = list(self._workers.values())
for run in workers:
self._cancel(run, "closed")
deadline = time.monotonic() + max(0, timeout)
for run in workers:
if run.thread is not None and run.thread is not current_thread():
run.thread.join(max(0, deadline - time.monotonic()))
return all(run.thread is None or not run.thread.is_alive() for run in workers)
@@ -0,0 +1,314 @@
"""Phase-aware natural-language edits, always revalidated before persistence."""
from __future__ import annotations
import json
import re
from collections.abc import Callable
from copy import deepcopy
from typing import Any
from cognitive_agent.application.mode_schedule import ModeSchedule
_NUMBER = r"[0-9]+(?:\.[0-9]+)?|[零一二两三四五六七八九十]+"
_PHASE = re.compile(r"第(?P<number>[0-9]+|[一二两三四五六七八九十]+)(?:个)?(?:阶段|步)")
_ALL_PHASES = re.compile(r"(?:全部|所有|每个|每一|各个|各)(?:的)?阶段")
_HOLD = re.compile(
rf"(?:持续时间|保持时间|持续|保持|时长|^(?:把|将|的)?(?=调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为))\s*"
rf"(?:调整为|调整到|设置为|设置成|设为|改为|改成|变成|变为)?\s*"
rf"(?P<number>{_NUMBER})\s*(?P<unit>分钟|秒钟|秒|minutes?|mins?|min|seconds?|secs?|s)"
)
_REPEAT = re.compile(
rf"(?:重复|循环)(?:次数)?\s*(?:调整为|调整到|设置为|设为|改为|改成)?\s*"
rf"(?P<number>{_NUMBER})\s*次"
)
def _number(value: str) -> float:
if re.fullmatch(r"[0-9]+(?:\.[0-9]+)?", value):
return float(value)
digits = {char: index for index, char in enumerate("零一二三四五六七八九")}
if value == "两":
return 2
if "十" in value:
tens, ones = value.split("十", 1)
if (tens and tens not in digits) or (ones and ones not in digits):
raise ValueError("请使用有效的整数编号或次数。")
return (digits[tens] if tens else 1) * 10 + (digits[ones] if ones else 0)
if value in digits:
return digits[value]
raise ValueError("请使用明确的阶段编号、持续时间或重复次数。")
def edit_schedule(
schedule: ModeSchedule,
change_text: str,
*,
parse_operations: Callable[[str], list[dict[str, Any]]],
target_is_explicit: Callable[[str, str], bool],
) -> ModeSchedule:
"""Edit one/all phases; an unscoped differing trajectory needs clarification."""
text = change_text.strip()
if not text:
raise ValueError("请说明要修改的阶段、动作、时长或重复次数。")
if re.search(r"(?:秒钟|分钟|秒|s)(?:之后|以后|后|再)", text):
raise ValueError(
"阶段时间表示动作执行后的保持时长,请明确阶段动作和持续时间,不使用延时后执行的表述。"
)
source = schedule.to_dict()
selected: set[int] | None = None
phase_matches = list(_PHASE.finditer(text))
all_match = _ALL_PHASES.search(text)
if all_match:
if phase_matches:
raise ValueError("请分别修改单个阶段或全部阶段,不要混用阶段范围。")
selected = set(range(len(schedule.phases)))
text = _ALL_PHASES.sub("", text)
elif phase_matches:
indexes = {int(_number(match.group("number"))) - 1 for match in phase_matches}
if len(indexes) != 1 or any(
index < 0 or index >= len(schedule.phases) for index in indexes
):
raise ValueError(f"当前只有{len(schedule.phases)}个阶段,请明确一次修改哪一个阶段。")
selected = indexes
text = _PHASE.sub("", text)
else:
named = {index for index, phase in enumerate(schedule.phases) if phase.name in text}
if len(named) > 1:
raise ValueError("请一次修改一个阶段,或明确指定全部阶段。")
if named:
selected = named
phase_name = schedule.phases[next(iter(named))].name
text = text.replace(phase_name + "阶段", "").replace(phase_name, "")
repeats = list(_REPEAT.finditer(text))
if repeats:
if len(repeats) != 1:
raise ValueError("请提供一个明确的整条时间轴重复次数。")
count = _number(repeats[0].group("number"))
if count != int(count):
raise ValueError("重复次数必须为整数。")
source["repeat_count"] = int(count)
text = _REPEAT.sub("", text)
holds = list(_HOLD.finditer(text))
if holds:
if len(holds) != 1 or selected is None:
raise ValueError("修改持续时间需要明确第几个阶段或全部阶段。")
seconds = _number(holds[0].group("number"))
if holds[0].group("unit") in {"分钟", "minute", "minutes", "min", "mins"}:
seconds *= 60
milliseconds = seconds * 1000
if milliseconds != int(milliseconds):
raise ValueError("阶段持续时间最多精确到毫秒。")
for index in selected:
source["phases"][index]["duration_ms"] = int(milliseconds)
text = _HOLD.sub("", text)
if re.search(rf"(?:{_NUMBER})\s*(?:分钟|秒钟|秒|minutes?|mins?|seconds?|secs?|s)", text):
raise ValueError("动态阶段只支持明确绝对设备目标;时长请单独表述为阶段保持时间,不支持按秒运动。")
text = re.sub(r"改为|改成|变更为|换成|变成|变为", "调整为", text)
text = re.sub(
r"^(?:把|将|的|中|里|阶段)+|(?:然后|并且|同时|并|,|,|;|;|。|\s)+$", "", text
).strip("的中里,,;;。 ")
changes = parse_operations(text) if text else []
if not changes and not holds and not repeats:
raise ValueError("没有识别到可执行的阶段调整,请说明设备目标、阶段时长或重复次数。")
for raw_change in changes:
change = deepcopy(raw_change)
indexes = selected if selected is not None else set(range(len(schedule.phases)))
candidates = [
(index, operation)
for index in indexes
for operation in source["phases"][index]["operations"]
if operation["domain"] == change["domain"]
and operation["capability"] == change["capability"]
]
if not target_is_explicit(text, change["target_scope"]):
targets = {operation["target_scope"] for _, operation in candidates}
if len(targets) > 1:
raise ValueError("该阶段的设备有多个区域,请明确主驾、副驾或具体目标区域。")
if targets:
change["target_scope"] = next(iter(targets))
candidates = [
(index, operation)
for index, operation in candidates
if operation["target_scope"] == change["target_scope"]
]
if selected is None:
values = {
json.dumps(operation["parameters"], sort_keys=True, ensure_ascii=False)
for _, operation in candidates
}
if len(values) > 1:
raise ValueError("该设备在多个阶段使用不同目标,请明确第几个阶段,或说“全部阶段”。")
if not candidates:
raise ValueError("新增设备动作需要明确放入第几个阶段,或明确全部阶段。")
indexes = {index for index, _ in candidates}
key = (change["capability"], change["target_scope"])
for index in indexes:
operations = source["phases"][index]["operations"]
found = False
for position, operation in enumerate(operations):
if (operation["capability"], operation["target_scope"]) == key:
operations[position] = {
**deepcopy(operation),
"parameters": deepcopy(change["parameters"]),
}
found = True
if not found:
operations.append(deepcopy(change))
return ModeSchedule.from_dict(source)
def revise_schedule(
service,
schedule: ModeSchedule,
*,
change_text: str,
utterance: str,
name: str,
conversation_id: str,
vehicle_profile,
context: dict[str, Any],
control_transport: str,
source_draft=None,
source_profile=None,
) -> dict[str, Any]:
"""Parse changed atoms locally, then recompile every phase before success."""
def parse_changes(text):
if any(
status != "complete"
for _clause, _parsed, status in service._local_clause_results(text, context)
):
raise ValueError("阶段调整说明不完整,请使用设备和明确目标值;未修改原时间轴。")
result = service._process_once(
text,
"local",
conversation_id,
"dry_run",
None,
vehicle_profile.id,
control_transport=control_transport,
allow_fuzzy_mode_profile_match=False,
)
if (
result.get("status") != "succeeded"
or result.get("deferred_requests")
or result.get("unresolved_clauses")
or result.get("supplement_required")
or any(issue.get("severity") == "error" for issue in result.get("validation", []))
):
raise ValueError("阶段动作未通过完整的设备能力或安全校验。")
operations = service._absolute_mode_operations(service._preview_mode_operations(result))
if not operations:
raise ValueError("未识别到可复用的绝对设备目标。")
return operations
revised = edit_schedule(
schedule,
change_text,
parse_operations=parse_changes,
target_is_explicit=service._mode_update_target_is_explicit,
)
preview = service._preview_mode_schedule(
revised,
name=name,
utterance=utterance,
conversation_id=conversation_id,
vehicle_profile=vehicle_profile,
control_transport=control_transport,
generation_source="draft_revision" if source_draft else "registered_mode_profile",
source_draft=source_draft,
source_profile=source_profile,
)
if preview.get("status") == "succeeded" and source_draft:
draft = preview["mode_profile_draft"]
draft["revises_source_request_id"] = source_draft["source_request_id"]
for key in ("source_template_id", "source_template_revision"):
if key in source_draft:
draft[key] = source_draft[key]
service._persist(preview)
return preview
def update_saved_schedule(
service,
profile,
*,
change_text: str,
utterance: str,
conversation_id: str,
vehicle_profile,
context: dict[str, Any],
result: dict[str, Any],
) -> dict[str, Any]:
try:
preview = revise_schedule(
service,
profile.schedule,
change_text=change_text,
utterance=utterance,
name=profile.name,
conversation_id=conversation_id,
vehicle_profile=vehicle_profile,
context=context,
control_transport="wired_can",
source_profile=profile,
)
if preview.get("status") != "succeeded":
return preview
draft = preview["mode_profile_draft"]
saved = service.save_mode_profile_card(
profile.id,
draft["name"],
draft["instruction"],
draft["source_request_id"],
vehicle_profile.id,
)
updated = service.mode_profiles.get_profile(profile.id, vehicle_profile.id)
report = service._mode_profile_control_report(
result["request_id"],
"update_mode_profile",
updated,
f"已更新“{profile.name}”的阶段时间轴;未向车辆下发控制。",
{"can_command_emitted": False, "phase_count": len(updated.schedule.phases)},
)
result.update(
status="succeeded",
execution_mode="dry_run",
hardware_execution_allowed=False,
orchestration_route="mode_profile_update",
provider_used="mode_schedule_editor",
mode_profile=saved["mode_profile"],
matched_modes=[saved["mode_profile"]],
schedule=updated.schedule.to_dict(),
execution_report=report,
validation=service.contracts.validate(report),
mode_profile_changes=[
deepcopy(op) for phase in updated.schedule.phases for op in phase.operations
],
decision_summary=[
f"已更新“{profile.name}”的阶段时间轴,并完整重新校验全部阶段;未下发车辆控制。"
],
)
except ValueError as exc:
result.update(
status="needs_confirmation",
execution_mode="dry_run",
hardware_execution_allowed=False,
orchestration_route="mode_profile_update",
validation=[
{
"severity": "error",
"code": "MODE_SCHEDULE_UPDATE_REJECTED",
"path": "schedule",
"message": str(exc),
}
],
decision_summary=[f"未修改“{profile.name}”的时间轴:{exc}"],
)
service._persist(result)
return result
@@ -0,0 +1,238 @@
"""Service integration for server-owned, fully validated phase schedules."""
from __future__ import annotations
import uuid
from copy import deepcopy
from datetime import UTC, datetime
from typing import Any
from cognitive_agent.application.mode_schedule import ModeSchedule
def public_schedule_status(state: dict[str, Any]) -> dict[str, Any]:
"""Expose lifecycle evidence, never internal errors, commands or CAN frames."""
keys = ("run_id", "vehicle_id", "session_id", "status", "phase_index", "repeat_index",
"executed_phases", "phase_count", "repeat_count", "elapsed_ms", "total_duration_ms",
"virtual_elapsed_ms", "completed", "cancel_reason")
public = {key: value for key in keys if isinstance((value := state.get(key)), (str, int, bool))}
if state.get("error"):
public["error"] = "阶段执行未通过校验或未返回成功结果,后续阶段已停止。"
return public
def phase_plan(phase) -> dict[str, Any]:
operations = phase.operations
return {
"detected_domains": list(dict.fromkeys(op["domain"] for op in operations)),
"requests": [{**{key: op[key] for key in ("domain", "capability", "target_scope")},
"operation": "set", "duration_ms": None,
"parameters": [{"name": key, "value": value} for key, value in op["parameters"].items()],
"reason": op.get("reason") or "已校验时间轴阶段。"} for op in operations],
"ambiguities": [], "confidence": 1.0,
"decision_summary": ["按阶段保留动作顺序与持续时间。"],
}
class ModeScheduleServiceMixin:
def _validated_schedule_artifact(self, result: dict[str, Any]) -> ModeSchedule | None:
draft = result.get("mode_profile_draft") or {}
raw = result.get("schedule")
if raw is None and draft.get("schedule") is None:
return None
schedule = ModeSchedule.from_dict(raw)
if draft.get("schedule") != schedule.to_dict() or draft.get("operation_count") != schedule.operation_count:
raise ValueError("时间轴草稿结构和服务端证据不一致,请重新生成。")
actions = (result.get("intent_ir") or {}).get("spec", {}).get("actions", [])
if len(actions) != schedule.operation_count:
raise ValueError("时间轴草稿动作证据不完整,请重新生成。")
offset = 0
for phase in schedule.phases:
phase_actions = actions[offset:offset + len(phase.operations)]
plan = phase_plan(phase)
if (self._scene_plan_signatures(plan) != self._scene_action_signatures(phase_actions)
or not self._scene_parameters_preserved(plan, phase_actions)):
raise ValueError("时间轴阶段目标与校验结果不一致,请重新生成。")
offset += len(phase.operations)
return schedule
def _preview_mode_schedule(
self, schedule: ModeSchedule, *, name: str, utterance: str,
conversation_id: str, vehicle_profile, control_transport: str,
generation_source: str, source_profile=None, source_template=None,
source_draft: dict[str, Any] | None = None,
scene_scope_utterance: str | None = None,
) -> dict[str, Any]:
results = []
for phase in schedule.phases:
plan = phase_plan(phase)
preview = self._process_once(
f"进入{name}", "local", conversation_id, "dry_run", None,
vehicle_profile.id, control_transport=control_transport,
assistant_plan=plan, allow_scene_composition=True,
allow_fuzzy_mode_profile_match=False,
# An existing-name binding is not evidence that a newly
# supplied model plan came from the saved profile. New builds
# pass the original request even when reusing a saved name.
scene_scope_utterance=(scene_scope_utterance if scene_scope_utterance is not None
else "" if source_draft or source_profile else utterance),
)
actions = (preview.get("intent_ir") or {}).get("spec", {}).get("actions", [])
complete = (preview.get("status") == "succeeded"
and self._scene_plan_signatures(plan) == self._scene_action_signatures(actions)
and self._scene_parameters_preserved(plan, actions))
if not complete:
preview["status"] = "validation_failed"
self._scrub_failed_scene_preview(preview)
preview["mode_creation_failure"] = {
"code": "MODE_SCHEDULE_PHASE_INVALID",
"reason": f"时间轴阶段“{phase.name}”未完整通过设备能力和安全校验",
"next_step": "请调整该阶段的设备目标;没有保存或执行任何阶段",
}
preview["validation"] = [*preview.get("validation", []), {
"severity": "error", "code": "MODE_SCHEDULE_PHASE_INVALID",
"path": f"schedule.phases.{phase.id}",
"message": preview["mode_creation_failure"]["reason"],
}]
self._persist(preview)
return preview
results.append(preview)
result = deepcopy(results[0])
request_id = f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}"
instruction = f"进入{name}"
result.update(request_id=request_id, utterance=instruction, status="succeeded",
execution_mode="dry_run", hardware_execution_allowed=False,
orchestration_route="mode_profile_create_preview")
actions = []
for phase, preview in zip(schedule.phases, results, strict=True):
for original in preview["intent_ir"]["spec"]["actions"]:
action = deepcopy(original)
if "id" in action:
action["id"] = f"{phase.id}-{action['id']}"
actions.append(action)
result["intent_ir"]["spec"]["actions"] = actions
for field in ("function_configs", "command_plans"):
result[field] = {f"{phase.id}-{key}": deepcopy(value)
for phase, preview in zip(schedule.phases, results, strict=True)
for key, value in preview[field].items()}
result["function_config"] = None
result["command_plan"] = None
result["schedule"] = schedule.to_dict()
result["schedule_phase_previews"] = [
{"phase_id": phase.id, "source_request_id": preview["request_id"],
"status": preview["status"], "duration_ms": phase.duration_ms}
for phase, preview in zip(schedule.phases, results, strict=True)
]
binding = ({"existing_mode_id": source_profile.id,
"existing_mode_revision": self.mode_profiles.profile_revision(source_profile)}
if source_profile is not None else {})
if source_draft:
binding = {key: source_draft[key] for key in ("existing_mode_id", "existing_mode_revision") if key in source_draft}
result["mode_profile_draft"] = {
"name": name, "instruction": instruction, "source_request_id": request_id,
"vehicle_id": vehicle_profile.id, "operation_count": schedule.operation_count,
"domains": sorted({op["domain"] for phase in schedule.phases for op in phase.operations}),
"generation_source": generation_source, "schedule": schedule.to_dict(),
"draft_revision": int((source_draft or {}).get("draft_revision", 0)) + 1,
**binding,
**({"source_template_id": source_template.id, "source_template_revision": source_template.revision}
if source_template is not None else {}),
}
result["mode_creation"] = {"requested_utterance": utterance, "source_utterance": instruction,
"source_status": "succeeded", "operation_count": schedule.operation_count}
result["decision_summary"] = [
f"已逐阶段验证“{name}”:{len(schedule.phases)}个阶段,重复{schedule.repeat_count}次,总计{schedule.total_duration_ms // 1000}秒。",
"当前仅生成草稿;保存后进入模式才开始计时执行。停止会取消后续阶段,已应用的设备状态保持。",
]
self._persist(result)
return result
def _run_mode_schedule(
self, profile, *, utterance: str, conversation_id: str | None,
execution_mode: str, approval_token: str | None, vehicle_id: str,
simulation_fault: str, hardware_client_id: str | None,
hardware_access_bypass: bool, control_transport: str,
) -> dict[str, Any]:
schedule = ModeSchedule.from_dict(profile.schedule.to_dict())
requested, supported = self._explicit_mode_seat_scopes(profile, utterance)
if requested and not requested.issubset(supported):
raise ValueError("该动态模式不支持指定座位,不会替换或扩大控制目标。")
if requested and requested != supported:
document = schedule.to_dict()
for phase in document["phases"]:
phase["operations"] = [op for op in phase["operations"]
if op["domain"] != "seat" or op["target_scope"] in requested]
# An empty phase is not silently removed: that would alter timing.
schedule = ModeSchedule.from_dict(document)
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
vehicle = self.vehicle_profiles.get(vehicle_id)
# Revalidate the entire saved schedule before the first write. No stale
# cached command bytes or client-authored phase plans are replayed.
preview = self._preview_mode_schedule(
schedule, name=profile.name, utterance="已保存时间轴复核",
conversation_id=conversation_id, vehicle_profile=vehicle,
control_transport=control_transport, generation_source="registered_mode_profile",
source_profile=profile,
)
if preview["status"] != "succeeded":
return preview
def execute_phase(phase):
context = self._context_for_vehicle(self.conversations.snapshot(conversation_id), vehicle)
parsed, _, _ = self._interpret(
"执行已验证的时间轴阶段", "local", context, vehicle,
assistant_plan=phase_plan(phase), allow_scene_composition=True,
scene_scope_utterance="",
)
return self.process(
"执行已验证的时间轴阶段", provider="local", conversation_id=conversation_id,
execution_mode=execution_mode, approval_token=approval_token,
vehicle_id=vehicle_id, simulation_fault=simulation_fault,
hardware_client_id=hardware_client_id, hardware_access_bypass=hardware_access_bypass,
control_transport=control_transport, _direct_can_batch=([], parsed),
)
if execution_mode == "hardware":
# Authentication/bench ownership is checked now and again for each
# phase, so a later lease loss stops further device commands.
if not hardware_access_bypass and control_transport != "wireless_mqtt" and not self._driver_only_vehicle_execution_active(control_transport):
self.hardware_access.ensure_authorized(hardware_client_id)
execution = self.mode_schedule_runner.start(vehicle_id, schedule, execute_phase, session_id=conversation_id)
else:
execution = self.mode_schedule_runner.simulate(schedule, execute_phase)
result = deepcopy(preview)
result.pop("mode_profile_draft", None)
result.update(
status="running" if execution["status"] == "running" else "succeeded" if execution.get("completed") else "failed",
utterance=utterance, execution_mode=execution_mode,
hardware_execution_allowed=execution_mode == "hardware",
orchestration_route="mode_schedule_execute", schedule_execution=execution,
mode_profile=profile.public_dict(), matched_modes=[profile.public_dict()],
)
result["decision_summary"] = [
f"已启动“{profile.name}”时间轴。" if execution["status"] == "running"
else f"“{profile.name}”已按虚拟时钟验证全部阶段。" if execution.get("completed")
else "时间轴执行失败,后续阶段已停止。",
"停止动态模式可取消后续阶段;已下发的设备目标不会自动回滚。",
]
self._persist(result)
return result
def _cancel_mode_schedule(self, vehicle_id: str, conversation_id: str | None, utterance: str,
execution_mode: str) -> dict[str, Any]:
state = (self.mode_schedule_runner.cancel(vehicle_id) if execution_mode == "hardware"
else {"status": "simulated", "completed": True, "vehicle_id": vehicle_id})
result = {
"request_id": f"req-{datetime.now(UTC).strftime('%Y%m%d%H%M%S')}-{uuid.uuid4().hex}",
"status": "succeeded", "execution_mode": execution_mode, "hardware_execution_allowed": False,
"vehicle": {"id": vehicle_id},
"utterance": utterance, "conversation_id": conversation_id or f"conv-{uuid.uuid4().hex}",
"orchestration_route": "mode_schedule_stop", "schedule_execution": state,
"decision_summary": [
"已模拟停止动态模式,未改变正在运行的车辆任务。" if execution_mode != "hardware"
else "已停止动态模式后续阶段。已下发目标保持,不自动回滚。" if state.get("status") == "cancelled"
else "当前没有正在执行的动态时间轴。"
],
"validation": [], "trace_events": [],
}
self._persist(result)
return result
@@ -106,9 +106,13 @@ OUTLET_MODE_DISABLE_PATTERN = re.compile(
)
DRIVER_SEAT_THERMAL_MIDDLE_PATTERN = re.compile(
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中|(?:2|二)(?:档|级)|"
r"(?:调到|调为|调|设为|设置为|调整到|调整为)中)|"
r"(?:中等(?:档|级)?|中档|中级|中间档|适中)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)"
r"[^,,;;。!?!?\n]{0,16}(?:通风|加热)|"
r"(?:通风|加热)[^,,;;。!?!?\n]{0,16}[,,;;]"
r"(?:挡位|档位)?(?:调到|调为|设为|设置为|调整到|调整为)?"
r"(?:2|二)(?:档|级)"
)
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?(?:按摩)?)"
@@ -116,6 +120,12 @@ DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN = re.compile(
r"(?:切换|改成|改为|调整|调成|调为|设置|设为)"
r".{0,8}(?:波浪|脉冲|伸展|拉伸|松弛|舒展|放松)(?:模式)?)"
)
DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN = re.compile(
r"^(?:(?:然后|随后|接着|同时|并且|再|并|以及)?(?:把|将)?)"
r"(?:通风|加热)?(?:挡位|档位)?"
r"(?:切换|改成|改为|调整|调到|调成|调为|设置|设为)?"
r"(?:[1-3一二三](?:档|级)|最低|低档|最高|高档|中档|中等|适中)$"
)
_FEATURE_BOUNDARY = re.compile(
r"并且|同时|然后|随后|接着|以及|并|再|和|与|、|,"
)
@@ -775,6 +785,23 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
if "通风" in clause or "加热" in clause:
continuation_index = index + 1
while (
continuation_index < len(seat_clauses)
and DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.search(
seat_clauses[continuation_index]
)
):
continuation = re.sub(
r"^(?:然后|随后|接着|同时|并且|再|并|以及)?"
r"(?:把|将)?(?:通风|加热)?",
"",
seat_clauses[continuation_index],
)
clause = f"{clause}{continuation}"
continuation_index += 1
index = continuation_index - 1
scope = _seat_target_scope(clause)
if scope == "driver":
inherited_driver_scope = True
@@ -864,11 +891,13 @@ def _parse_chinese_driver_seat_commands(compact: str) -> list[dict[str, Any]]:
if re.search(r"关闭|关掉|停止|取消|不要|别开", thermal_text):
parameters = {"state": "off"}
elif thermal_grade == "high" or re.search(
r"高速|(?:1|一)(?:档|级)", thermal_text
r"高速|(?:3|三)(?:档|级)", thermal_text
):
parameters = {"state": "on", "level": 1}
elif re.search(r"(?:2|二)(?:档|级)|中档|中等|适中", thermal_text):
parameters = None
elif thermal_grade == "low" or re.search(
r"低速|(?:2|二)(?:档|级)", thermal_text
r"低速|(?:1|一)(?:档|级)", thermal_text
) or re.search(r"打开|开启|启动|启用", thermal_text):
parameters = {"state": "on", "level": 2}
elif shared_thermal_state == "off":
@@ -1177,7 +1206,8 @@ def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
compact,
r"座椅|座位|靠背|椅背|坐盆|坐垫|腿托|脚托|按摩|记忆|预设|"
r"通风|加热|前移|后移|向前移动|向后移动",
DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern,
rf"(?:{DRIVER_SEAT_MASSAGE_CONTINUATION_PATTERN.pattern})|"
rf"(?:{DRIVER_SEAT_THERMAL_CONTINUATION_PATTERN.pattern})",
)
actions: list[dict[str, Any]] = _parse_chinese_driver_seat_commands(
seat_text
@@ -1794,7 +1824,7 @@ def merge_native_can_intent(
utterance
)
if unsupported_thermal_middle is not None and not re.search(
r"副驾|副驾驶|乘客|右前", utterance
r"副驾|副驾驶|乘客|乘员|右前", utterance
):
result = dict(parsed)
feature = (
@@ -1334,7 +1334,10 @@ class NativeCanExecutor:
# separate evidence level in ``verification_status`` and
# ``physical_effect_confirmed``. A feedback timeout must
# not be rewritten as a transport/orchestration failure.
succeeded = delivered
# A closed-loop target may already be satisfied before a
# new frame is needed. Fresh matching feedback is a
# successful no-op, not a failed dispatch.
succeeded = delivered or verified
verification_status = (
"verified_success"
if verified
@@ -1354,7 +1357,11 @@ class NativeCanExecutor:
publication_start:
]
)
delivery_status = "submitted"
delivery_status = (
"not_required"
if verified and not delivered
else "submitted"
)
if not feedback_required and delivered:
delivery_status = (
"published"
@@ -1371,7 +1378,9 @@ class NativeCanExecutor:
"domain": function_config["spec"]["domain"],
"status": "succeeded" if succeeded else "failed",
"message": (
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
"车辆反馈已确认目标状态,无需重复下发控制帧。"
if verified and not delivered
else "MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
if not feedback_required and delivery_status == "published"
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
if not feedback_required
File diff suppressed because it is too large Load Diff
@@ -24,11 +24,13 @@ from cognitive_agent.application.assistant_plan_guard import (
SCENE_COMPOSABLE_CAPABILITIES,
)
from cognitive_agent.application.mode_profile import ModeOperation
from cognitive_agent.application.mode_profile_intent import mode_creation_source_name
from cognitive_agent.application.mode_schedule import ModeSchedule
_TEMPLATE_ID = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$")
_SURROUNDING_PUNCTUATION = " \t\r\n,。,.!!??;;::'\"‘’“”《》<>【】[]()()"
_DOCUMENT_FIELDS = frozenset(
{"id", "name", "aliases", "description", "icon", "operations"}
{"id", "name", "aliases", "description", "icon", "operations", "schedule"}
)
@@ -66,6 +68,7 @@ class SceneTemplate:
operations: tuple[ModeOperation, ...]
source: str
revision: str
schedule: ModeSchedule | None = None
@classmethod
def from_yaml(cls, path: Path) -> "SceneTemplate":
@@ -100,7 +103,13 @@ class SceneTemplate:
alias_keys.add(key)
aliases.append(alias)
schedule = (ModeSchedule.from_dict(document["schedule"])
if document.get("schedule") is not None else None)
raw_operations = document.get("operations")
if schedule is not None:
if raw_operations:
raise ValueError(f"{path} cannot mix static operations and a schedule")
raw_operations = list(schedule.phases[0].operations)
if not isinstance(raw_operations, list) or not raw_operations:
raise ValueError(f"{path} must contain at least one operation")
if len(raw_operations) > MAX_PLAN_REQUESTS:
@@ -155,6 +164,8 @@ class SceneTemplate:
),
),
}
if schedule is not None:
semantic_document["schedule"] = schedule.to_dict()
canonical = json.dumps(
semantic_document,
ensure_ascii=False,
@@ -171,6 +182,7 @@ class SceneTemplate:
operations=tuple(operations),
source=str(path),
revision=revision,
schedule=schedule,
)
def plan(self) -> dict[str, Any]:
@@ -179,7 +191,7 @@ class SceneTemplate:
request = operation.to_request(self.name)
request["duration_ms"] = None
requests.append(request)
return {
plan = {
"detected_domains": list(
dict.fromkeys(operation.domain for operation in self.operations)
),
@@ -195,6 +207,9 @@ class SceneTemplate:
"template_revision": self.revision,
},
}
if self.schedule is not None:
plan["schedule"] = self.schedule.to_dict()
return plan
def public_dict(self, *, include_plan: bool = False) -> dict[str, Any]:
result: dict[str, Any] = {
@@ -204,13 +219,16 @@ class SceneTemplate:
"description": self.description,
"icon": self.icon,
"domains": list(dict.fromkeys(item.domain for item in self.operations)),
"operation_count": len(self.operations),
"operation_count": self.schedule.operation_count if self.schedule else len(self.operations),
"template_revision": self.revision,
"origin": "builtin_scene_template",
"read_only": True,
}
if include_plan:
result["plan"] = self.plan()
if self.schedule is not None:
result["schedule"] = self.schedule.to_dict()
result["domains"] = sorted({op["domain"] for phase in self.schedule.phases for op in phase.operations})
return result
@@ -257,7 +275,11 @@ class SceneTemplateRegistry:
def resolve_exact_name(self, value: object) -> SceneTemplate | None:
key = normalize_scene_template_name(value)
return self._by_name.get(key) if key else None
exact = self._by_name.get(key) if key else None
if exact is not None:
return exact
subject = normalize_scene_template_name(mode_creation_source_name(value))
return self._by_name.get(subject) if subject and subject != key else None
def get_template(self, template_id: str) -> SceneTemplate:
try:
@@ -0,0 +1,25 @@
"""Narrow detection of temporal scene needs; never authorizes execution."""
from __future__ import annotations
import re
_TEMPORAL = re.compile(
r"动态|循环|分阶段|随时间|交替|轮流|每隔|预热|预冷|"
r"(?:秒|分钟|分|小时)(?:钟)?(?:后|再)|持续.{0,8}(?:秒|分钟|小时)"
)
_STOP = re.compile(
r"^(?:请|帮我|立即|马上|先)?(?:停止|停下|结束|取消|关闭)"
r"(?:当前|正在执行的|正在运行的|这个|该)?(?:座椅|空调|功能)?(?:动态|循环|分阶段|时间轴|定时)"
r"(?:座椅|空调|功能)?(?:模式|编排|任务)?[。!!\s]*$"
)
def needs_mode_schedule(text: str) -> bool:
# HVAC recirculation is a stable device enum, not a timed repetition.
normalized = re.sub(r"内外循环|内循环|外循环|自动循环|新风循环|循环风", "空气循环状态", text)
normalized = normalized.replace("空气循环状态", "空气状态")
return bool(_TEMPORAL.search(normalized))
def is_schedule_stop(text: str) -> bool:
return bool(_STOP.fullmatch(text.strip()))
File diff suppressed because it is too large Load Diff
+285 -16
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
import hashlib
import json
import os
import queue
@@ -119,6 +120,10 @@ class GatewayError(RuntimeError):
"""Raised when a GSCU1B command cannot be delivered safely."""
class PeripheralCatalogConflictError(GatewayError):
"""Raised when a browser tries to replace a newer durable IR catalog."""
class ExtensionIntegrationError(RuntimeError):
"""Raised when GPT planning or vehicle orchestration is unavailable."""
@@ -1037,11 +1042,160 @@ def _peripheral_execution_identity_authorized(
) -> bool:
# Saved modes execute an immutable, previously validated command flow and
# are rebound to the same persisted NFC/device/slot tuple before reaching here.
if capability.get("parse_source") == "saved_mode_command_flow":
if capability.get("parse_source") in {
"saved_mode_command_flow",
"scene_composer_capability_ref",
}:
return True
return bool(_device_match_score(_compact_match_text(utterance), capability))
_PERIPHERAL_COLOR_ACTION_PATTERN = re.compile(
r"颜色|色彩|红色|红灯|红光|绿色|绿灯|绿光|蓝色|蓝灯|蓝光|白色|白灯|白光|"
r"紫色|紫灯|紫光|黄色|黄灯|黄光|橙色|橙灯|橙光|粉色|粉灯|粉光|"
r"\b(?:color|red|green|blue|white|purple|yellow|orange|pink)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_ON_ACTION_PATTERN = re.compile(
r"打开|开启|启动|点亮|开灯|\b(?:on|power\s*on|turn\s*on)\b",
re.IGNORECASE,
)
_PERIPHERAL_POWER_OFF_ACTION_PATTERN = re.compile(
r"关闭|关掉|停止|关灯|熄灭|\b(?:off|power\s*off|turn\s*off)\b",
re.IGNORECASE,
)
def peripheral_capability_action_text(capability: Mapping[str, Any]) -> str:
"""Return the complete learned action vocabulary used for dependencies."""
values: list[str] = [str(capability.get("action") or "")]
for field in ("aliases", "action_names"):
aliases = capability.get(field, [])
if isinstance(aliases, str):
values.append(aliases)
elif isinstance(aliases, list):
values.extend(str(value or "") for value in aliases)
return " ".join(value.strip() for value in values if value.strip())
def is_peripheral_color_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability changes a light-like device color."""
return _PERIPHERAL_COLOR_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def is_peripheral_power_on_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability is a stable device activation."""
text = peripheral_capability_action_text(capability)
return bool(
_PERIPHERAL_POWER_ON_ACTION_PATTERN.search(text)
and _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(text) is None
and _PERIPHERAL_COLOR_ACTION_PATTERN.search(text) is None
)
def is_peripheral_power_off_capability(capability: Mapping[str, Any]) -> bool:
"""Return whether an infrared ability explicitly powers a device off."""
return _PERIPHERAL_POWER_OFF_ACTION_PATTERN.search(
peripheral_capability_action_text(capability)
) is not None
def add_peripheral_activation_prerequisites(
steps: list[dict[str, Any]],
catalog: list[dict[str, Any]],
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""Insert an authorized power-on step before each device's first color.
Infrared color commands do not prove that the target is powered. The
executor therefore treats a learned power-on command for the same durable
device binding as a mandatory dependency. The returned catalog carries
the original parse source onto an injected prerequisite so saved modes can
still pass their immutable-flow authorization check.
"""
ordered_ids: list[str] = []
catalog_by_id: dict[str, dict[str, Any]] = {}
for raw in catalog[:MAX_CATALOG_ITEMS]:
if not isinstance(raw, Mapping):
continue
capability_id = str(raw.get("id") or "").strip()
if not capability_id:
continue
if capability_id not in catalog_by_id:
ordered_ids.append(capability_id)
catalog_by_id[capability_id] = dict(raw)
prepared_steps: list[dict[str, Any]] = []
powered_devices: set[str] = set()
for raw_step in steps:
step = dict(raw_step) if isinstance(raw_step, Mapping) else raw_step
if not isinstance(step, dict) or step.get("kind") != "peripheral":
prepared_steps.append(step)
continue
capability_id = str(step.get("capability_id") or "").strip()
capability = catalog_by_id.get(capability_id)
if capability is None:
prepared_steps.append(step)
continue
device_id = str(capability.get("device_id") or "").strip()
if is_peripheral_power_on_capability(capability):
if device_id:
powered_devices.add(device_id)
prepared_steps.append(step)
continue
if is_peripheral_power_off_capability(capability):
if device_id:
powered_devices.discard(device_id)
prepared_steps.append(step)
continue
if not is_peripheral_color_capability(capability) or not device_id:
prepared_steps.append(step)
continue
if device_id not in powered_devices:
prerequisite = next(
(
item
for item in catalog_by_id.values()
if str(item.get("device_id") or "").strip() == device_id
and item.get("status") == "learned"
and item.get("enabled", True) is not False
and bool(_normalized_card_uid(item.get("card_uid")))
and is_peripheral_power_on_capability(item)
),
None,
)
if prerequisite is None:
raise ExtensionIntegrationError(
f"外设 {capability.get('device_name') or device_id} 设置颜色前需要先打开,"
"但当前没有已学习、已启用且已绑定 NFC 的打开指令。"
)
prerequisite_id = str(prerequisite.get("id") or "").strip()
parse_source = str(capability.get("parse_source") or "").strip()
if parse_source and not prerequisite.get("parse_source"):
prerequisite = {**prerequisite, "parse_source": parse_source}
catalog_by_id[prerequisite_id] = prerequisite
prepared_steps.append(
{
"kind": "peripheral",
"capability_id": prerequisite_id,
"dependency": "power_on_before_color",
"auto_inserted": True,
}
)
powered_devices.add(device_id)
prepared_steps.append(step)
if len(prepared_steps) > MAX_PLAN_STEPS * 2:
raise ExtensionIntegrationError("补齐外设打开前置指令后,动作数量超过安全上限。")
return prepared_steps, [catalog_by_id[item_id] for item_id in ordered_ids]
def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
"""Validate one browser-synchronized learned infrared capability."""
@@ -1072,6 +1226,9 @@ def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
raise GatewayError("红外能力风险等级只能是 low、medium 或 high。")
if value.get("status", "learned") != "learned":
raise GatewayError("只有已学习的红外能力可以进入执行白名单。")
for field_name in ("scene_safe", "idempotent"):
if value.get(field_name) is not None and type(value[field_name]) is not bool:
raise GatewayError(f"红外能力字段 {field_name} 必须是布尔值。")
device_aliases = _normalized_aliases(
value.get("device_aliases"), field="设备别名"
@@ -1123,6 +1280,8 @@ def _normalize_peripheral_capability(value: Any) -> dict[str, Any]:
"aliases": action_aliases,
"action_names": action_names,
"risk": risk,
"scene_safe": value.get("scene_safe"),
"idempotent": value.get("idempotent"),
"slot": validate_slot(value.get("slot")),
"transport": str(value.get("transport") or "gscu_tcp")[:32],
}
@@ -2310,6 +2469,7 @@ class SelfExtensionService:
self.gateway = gateway or create_gateway()
self.infrared_catalog = InfraredSlotCatalog(catalog_db_path)
self._gateway_lock = threading.RLock()
self._catalog_sync_lock = threading.RLock()
self._started = False
self.command_timeout = float(_env_text("GSCU_COMMAND_TIMEOUT", "15"))
self.openai_base_url = _env_text(
@@ -2389,25 +2549,53 @@ class SelfExtensionService:
return self.infrared_catalog.list_routing_capabilities()
def sync_peripheral_catalog(self, catalog: object) -> dict[str, Any]:
def peripheral_catalog_revision(self) -> str:
"""Return a stable revision of the durable IR execution whitelist."""
catalog = self.list_peripheral_capabilities()
canonical = json.dumps(
sorted(catalog, key=lambda item: str(item.get("id") or "")),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
def sync_peripheral_catalog(
self,
catalog: object,
*,
expected_revision: object = None,
) -> dict[str, Any]:
"""Persist the complete learned capability catalog from the NFC UI."""
if not isinstance(catalog, list) or len(catalog) > MAX_CATALOG_ITEMS:
raise GatewayError(
f"红外能力目录必须是至多 {MAX_CATALOG_ITEMS} 项的数组。"
)
learned = [
item
for item in catalog
if isinstance(item, dict) and item.get("status") == "learned"
]
capabilities = self.infrared_catalog.sync_capabilities(learned)
return {
"success": True,
"status": "ready",
"count": len(capabilities),
"catalog": capabilities,
}
with self._catalog_sync_lock:
current_revision = self.peripheral_catalog_revision()
clean_expected_revision = str(expected_revision or "").strip()
if (
clean_expected_revision
and clean_expected_revision != current_revision
):
raise PeripheralCatalogConflictError(
"红外能力目录已被其他会话更新,请刷新后重试。"
)
learned = [
item
for item in catalog
if isinstance(item, dict) and item.get("status") == "learned"
]
capabilities = self.infrared_catalog.sync_capabilities(learned)
return {
"success": True,
"status": "ready",
"count": len(capabilities),
"catalog": capabilities,
"catalog_revision": self.peripheral_catalog_revision(),
}
def save_infrared_slot(self, slot: Any, payload: dict[str, Any]) -> dict[str, Any]:
record = self.infrared_catalog.upsert(
@@ -2555,6 +2743,13 @@ class SelfExtensionService:
) -> dict[str, Any]:
"""Execute a persistently NFC-bound match through the main request API."""
if (
capability.get("parse_source") == "scene_composer_capability_ref"
and execution_mode != "dry_run"
):
raise ExtensionIntegrationError(
"场景创建阶段的红外能力引用只允许 dry-run 校验。"
)
if (
capability.get("enabled", True) is False
or not _normalized_card_uid(capability.get("card_uid"))
@@ -2566,6 +2761,36 @@ class SelfExtensionService:
raise ExtensionIntegrationError(
"外设名称未在原始指令中被明确提及,已拒绝红外执行。"
)
if is_peripheral_color_capability(capability):
device_id = str(capability.get("device_id") or "").strip()
dependency_catalog = [
dict(item)
for item in self.list_peripheral_capabilities()
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip() == device_id
and str(item.get("id") or "").strip()
!= str(capability.get("id") or "").strip()
]
dependency_catalog.append(dict(capability))
return self.execute_peripheral_plan_request(
utterance,
{
"provider_used": "infrared_dependency_guard",
"summary": "设置外设颜色前自动补齐同设备的打开指令。",
"steps": [
{
"kind": "peripheral",
"capability_id": str(capability.get("id") or ""),
}
],
},
dependency_catalog,
conversation_id=conversation_id,
conversation_turn=conversation_turn,
execution_mode=execution_mode,
provider_requested=provider_requested,
on_commands_validated=on_commands_validated,
)
slot = validate_slot(capability.get("slot"))
request_id = f"req-extension-{uuid.uuid4().hex}"
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
@@ -2707,7 +2932,7 @@ class SelfExtensionService:
"execution_mode": execution_mode,
"control_transport": control_transport,
"decision_summary": [
f"已通过持久 NFC 绑定 {capability.get('card_uid')} 匹配外设能力。",
"已通过持久 NFC 绑定校验并匹配外设能力。",
f"命中已学习动作:{capability.get('device_name')} · {capability.get('action')}(槽位 {slot})。",
],
"trace_events": [
@@ -2770,6 +2995,39 @@ class SelfExtensionService:
raise ExtensionIntegrationError("GPT 没有生成可执行的外设动作。")
if any(step.get("kind") == "vehicle" for step in steps if isinstance(step, dict)):
raise ExtensionIntegrationError("当前主编排只允许执行持久绑定的 NFC 外设计划。")
selected_device_ids = {
str(item.get("device_id") or "").strip()
for item in catalog
if isinstance(item, Mapping)
and str(item.get("device_id") or "").strip()
}
effective_catalog: list[dict[str, Any]] = [
dict(item) for item in catalog if isinstance(item, Mapping)
]
known_ids = {
str(item.get("id") or "").strip()
for item in effective_catalog
if str(item.get("id") or "").strip()
}
for raw_item in self.list_peripheral_capabilities():
if not isinstance(raw_item, Mapping):
continue
item = dict(raw_item)
capability_id = str(item.get("id") or "").strip()
if (
not capability_id
or capability_id in known_ids
or str(item.get("device_id") or "").strip()
not in selected_device_ids
):
continue
effective_catalog.append(dict(item))
known_ids.add(capability_id)
steps, catalog = add_peripheral_activation_prerequisites(
[dict(step) if isinstance(step, Mapping) else step for step in steps],
effective_catalog,
)
plan_result = {**plan_result, "steps": steps}
catalog_by_id = {
str(item.get("id")): item
for item in catalog
@@ -2835,6 +3093,7 @@ class SelfExtensionService:
function_operations.append(
{
"id": operation_ref,
"capability_id": capability_id,
"capability": "infrared.emit",
"target": {
"device": capability.get("device_id"),
@@ -2848,6 +3107,8 @@ class SelfExtensionService:
"parse_source": capability.get(
"parse_source", "local_rule"
),
"auto_inserted": step.get("auto_inserted") is True,
"dependency": step.get("dependency"),
"timeout_ms": int(self.command_timeout * 1000),
"failure_policy": "abort",
}
@@ -2873,6 +3134,14 @@ class SelfExtensionService:
"operations": function_operations,
},
}
if execution_mode != "dry_run" and any(
isinstance(item, Mapping)
and item.get("parse_source") == "scene_composer_capability_ref"
for item in catalog
):
raise ExtensionIntegrationError(
"场景创建阶段的红外能力引用只允许 dry-run 校验。"
)
command_plan = {
"apiVersion": "cognitive-agent.io/v1alpha1",
"kind": "CommandPlan",
@@ -3150,7 +3419,7 @@ class SelfExtensionService:
else "GPT 已生成外设顺序计划。"
)
),
f"计划仅引用持久 NFC 绑定 {first_capability.get('card_uid')} 的已学习能力。",
"计划仅引用已通过持久 NFC 绑定校验的已学习能力。",
*(
[
"当前绑定未学习专用闪烁能力;为避免灯具停在关闭态及学习器持续蜂鸣,"

Some files were not shown because too many files have changed in this diff Show More