Compare commits
67 Commits
main
...
448c8b31a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 448c8b31a0 | |||
| 89707a42e2 | |||
| bfff1b515c | |||
| 904f5f8a6c | |||
| 24aa1371dc | |||
| 0ab1d76781 | |||
| e920a92505 | |||
| c812a1a595 | |||
| 7d196db02b | |||
| ab8e43050c | |||
| 75ce391312 | |||
| 0e4328f548 | |||
| 84a3cd73bb | |||
| 58f1a2da42 | |||
| aa6f59d3a6 | |||
| 3eeca2ea75 | |||
| d8dad22ec0 | |||
| d09b5967f8 | |||
| 812e67422d | |||
| 7eed11b774 | |||
| 3fc6f2c2a4 | |||
| 51ca12b624 | |||
| 76057be044 | |||
| 2af8b8f9ac | |||
| 93103c2579 | |||
| 5e228e12ae | |||
| f0a248ede3 | |||
| 6b2b644ee6 | |||
| cf9b89508c | |||
| 53d80ab662 | |||
| c96970f068 | |||
| 32d179143f | |||
| 009d8ca73e | |||
| ccd64adcfe | |||
| 00c7d6b874 | |||
| cbf230182d | |||
| ca26259e63 | |||
| 6d75944c0f | |||
| aa12135b22 | |||
| fd13d9f1ee | |||
| 4a0570706f | |||
| 2529b4896a | |||
| 9ac8fdface | |||
| 33c92d1352 | |||
| 8d05815773 | |||
| fff7657fea | |||
| 52c4dd08f9 | |||
| 772ab685d6 | |||
| 9b24d5c8c9 | |||
| 344f662970 | |||
| f77dda70fe | |||
| 6d768aaef4 | |||
| 1a96d39426 | |||
| 0ec43052f3 | |||
| fd3c219c39 | |||
| 9ce64b3a01 | |||
| 32379a1573 | |||
| 3701192a11 | |||
| 3bceb0d5a1 | |||
| 156b10fda1 | |||
| dc7c91800e | |||
| 155fbb59ba | |||
| a952f37ceb | |||
| 85b4740d29 | |||
| a6d974cb29 | |||
| 55d0d400cb | |||
| 4aded5c21d |
@@ -0,0 +1,12 @@
|
||||
.git
|
||||
.venv
|
||||
.env
|
||||
.env.auth
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
artifacts
|
||||
output
|
||||
tmp
|
||||
tests
|
||||
firmware
|
||||
docs/product
|
||||
@@ -4,4 +4,41 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
artifacts/runs/
|
||||
artifacts/mode-profiles/
|
||||
artifacts/server/
|
||||
artifacts/tmp/
|
||||
artifacts/self-repair/
|
||||
artifacts/models/
|
||||
artifacts/*.log
|
||||
tmp/*.log
|
||||
.codex_tmp/
|
||||
/build/
|
||||
/output/
|
||||
/apps/web-console/node_modules/
|
||||
/apps/web-console/dist/
|
||||
/tmp/imagegen/
|
||||
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/logs/
|
||||
firmware/t2g/Seat_Controller_demo/Tests/manual_hil/seat_function_runtime/
|
||||
tools/zcan/seat_function_runtime/
|
||||
artifacts/hil/*_server_*.log
|
||||
artifacts/hil/frontend_service_*.log
|
||||
tmp/can-matrix-artifact/
|
||||
tmp/reference-project-code-CAN_DEV/
|
||||
firmware/t2g/Seat_Controller_demo/IAR/settings/
|
||||
firmware/t2g/Seat_Controller_demo/IAR/Seat_Controller_demo.eww
|
||||
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.Seat_Controller_demo_build_cache
|
||||
firmware/t2g/Seat_Controller_demo/IAR/rev_d/.ninja_deps
|
||||
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/
|
||||
|
||||
@@ -1,14 +1,37 @@
|
||||
.PHONY: validate test dev dev-lan
|
||||
.PHONY: validate test test-frontend dev dev-lan can-protocol-generate can-protocol-check can-read-trans can-read-trans-test
|
||||
|
||||
validate:
|
||||
CAN_READ_TRANS_BUILD_DIR ?= build/can_read_trans
|
||||
CAN_READ_TRANS_CANFD_ROOT ?= /home/orin/project511/project-code/canfd
|
||||
TEST_PYTHONPATH := src:domains/_template/src:domains/ambient_light/src:domains/climate/src:domains/roof/src:domains/seat/src
|
||||
|
||||
can-protocol-generate:
|
||||
python tools/generate_can_semantic_protocol.py
|
||||
|
||||
can-protocol-check:
|
||||
python tools/generate_can_semantic_protocol.py --check
|
||||
|
||||
validate: can-protocol-check
|
||||
python tools/validate_repository.py
|
||||
|
||||
test:
|
||||
PYTHONPATH=src:domains/seat/src python -m unittest discover -s domains/seat/tests -p 'test_*.py'
|
||||
PYTHONPATH=src:domains/seat/src python -m unittest discover -s tests -p 'test_*.py'
|
||||
test-frontend:
|
||||
python tools/run_node.py --test tests/unit/assistant_stream_runtime.test.cjs
|
||||
python tools/run_node.py --test tests/unit/speech_input_runtime.test.cjs
|
||||
python -m pytest tests/unit/test_energy_monitor_frontend_contract.py -q
|
||||
|
||||
test: can-protocol-check test-frontend
|
||||
PYTHONPATH=$(TEST_PYTHONPATH)$${PYTHONPATH:+:$${PYTHONPATH}} python -m pytest
|
||||
|
||||
dev:
|
||||
PYTHONPATH=src:domains/seat/src python -m cognitive_agent.server
|
||||
|
||||
dev-lan:
|
||||
PYTHONPATH=src:domains/seat/src python -m cognitive_agent.server --host 0.0.0.0
|
||||
|
||||
can-read-trans: can-protocol-generate
|
||||
cmake -S tools/can_read_trans -B $(CAN_READ_TRANS_BUILD_DIR) \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCANFD_ROOT=$(CAN_READ_TRANS_CANFD_ROOT)
|
||||
cmake --build $(CAN_READ_TRANS_BUILD_DIR) --parallel
|
||||
|
||||
can-read-trans-test: can-read-trans
|
||||
ctest --test-dir $(CAN_READ_TRANS_BUILD_DIR) --output-on-failure
|
||||
|
||||
@@ -1,137 +1,287 @@
|
||||
# Cognitive Agent
|
||||
|
||||
面向车载执行器的认知编排 Agent 单仓工程。系统把自然语言请求转换为可校验的语义配置,再由确定性的领域插件编译为 CAN 命令计划,最后经安全策略和执行器下发。
|
||||
面向车载执行器的认知编排单仓工程。系统把自然语言或结构化请求转换为可校验的语义配置,由领域插件确定性编译为 `CommandPlan`,经过平台安全门禁后以 dry-run、模拟台架或真实 CAN 方式执行,并保存可审计结果。
|
||||
|
||||
启动前端:PYTHONPATH=src:domains/seat/src .omx/python311-venv/bin/python -m cognitive_agent.server
|
||||
当前仓库以腾势 N7 / T2G 座舱演示和硬件台架为主。默认环境不会打开真实硬件;气候、天幕和氛围灯领域的矩阵目前也只允许虚拟执行。
|
||||
|
||||
## 建设阶段
|
||||
## 能做什么
|
||||
|
||||
- 阶段一:每个领域独立完成能力定义、功能配置生成、配置校验、CAN 编译和单执行器测试。
|
||||
- 阶段二:由中央编排器生成 `SceneConfig`,引用多份 `FunctionConfig`,按依赖关系、时间偏移和失败策略组合执行。
|
||||
- 最终形态:Web 控制台接收一句话请求,展示结构化决策摘要、配置文件、校验结果和执行状态。
|
||||
- 同源 Python HTTP 服务与 Vue 3 + Vite Web 控制台。
|
||||
- 能力目录驱动的自然语言编排、模式卡片、跨域场景和 NDJSON 助手流。
|
||||
- `IntentIR`、`FunctionConfig`、`SceneConfig`、`CommandPlan`、`TraceEvent` 和 `ExecutionReport` 公共契约。
|
||||
- 座椅、空调、天幕、氛围灯四个领域插件,以及平台侧门窗、插座、阅读灯等原生 CAN 能力。
|
||||
- 虚拟 CAN、模拟硬件、T2G ZLG CAN 台架和双通道车身 CAN 接入。
|
||||
- 语音输入、MediaPipe 手势、NFC/红外外设、模式目录同步和只读固件 OTA 服务。
|
||||
|
||||
## 核心约束
|
||||
AI 模型只能提出受契约约束的语义计划,不能生成或发送原始 CAN 帧。真实执行仍由服务端的能力白名单、参数 Schema、硬件授权、安全策略、互斥和反馈检查共同决定。
|
||||
|
||||
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、完整隐私文本或未脱敏车辆标识。
|
||||
|
||||
## 可执行领域
|
||||
## 当前能力边界
|
||||
|
||||
当前已接入四个相互隔离的可执行领域:
|
||||
| 能力面 | 当前状态 | 事实来源 |
|
||||
| --- | --- | --- |
|
||||
| `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) |
|
||||
|
||||
- 座椅:基于 `座椅部分功能CAN矩阵表.xlsx` 的主/副驾通风和加热,以及明确标识为 synthetic 的五轴位置电机演示。
|
||||
- 空调:基于 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的电源、18–32℃ 分区温度、0–7 档风量、A/C、内外循环、风向和前除霜。
|
||||
- 天幕:基于同一矩阵的 0–100% 透光率、后遮阳帘绝对位置和自动调光。
|
||||
- 氛围灯:基于同一矩阵的总开关、24-bit RGB、0–100% 亮度和 ECU 场景模式。
|
||||
`platforms/reference_vehicle` 的 `execution_enabled` 为 `false`。`platforms/t2g_seat_bench` 和 `platforms/t2g_cabin_bench` 才是仓库内允许配置真实执行的台架平台,但仍需硬件开关、授权令牌、总线配置和反馈校验全部满足。
|
||||
|
||||
一句话只涉及一个领域时直接生成一份 `FunctionConfig`;涉及多个领域时生成一份 `SceneConfig` 引用各领域独立的 `FunctionConfig`,并由各自编译器产生 `CommandPlan`。三份新增矩阵仍缺少车型适用性、完整计数器、校验和和发送周期,因此与座椅演示一样只允许虚拟执行。
|
||||
## 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Python 3.11 或更高版本。
|
||||
- Node.js 与 npm(构建 Web 控制台)。
|
||||
- Codex CLI `0.150.0-alpha.8`,用于内置 AI 助手。
|
||||
- 真实台架另需 ZLG 驱动/桥接程序、对应 DBC、IAR 工具链和硬件授权。
|
||||
|
||||
### 安装(PowerShell)
|
||||
|
||||
```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]"
|
||||
|
||||
cd apps\web-console
|
||||
npm ci
|
||||
npm run build
|
||||
cd ..\..
|
||||
|
||||
npm install --global @openai/codex@0.150.0-alpha.8
|
||||
codex login status
|
||||
```
|
||||
|
||||
语音识别是可选安装项:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe -m pip install -e ".[speech]"
|
||||
.\.venv\Scripts\python.exe tools\install_sensevoice_model.py
|
||||
```
|
||||
|
||||
配置模型认证时使用 `OPENAI_API_KEY`,或让 Codex CLI 使用已登录账户。`.env` 和 `.env.auth` 已被 Git 忽略;可参照 [`config/environments/`](config/environments/) 中的示例配置。不要把密钥提交到仓库。
|
||||
|
||||
### 启动完整本地演示
|
||||
|
||||
推荐使用启动器。它会管理主服务、只读模式卡片服务和只读 OTA 服务:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe tools\start_n7web.py --skip-web-build
|
||||
```
|
||||
|
||||
默认端口:
|
||||
|
||||
| 服务 | 地址 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| 主服务和 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` | 只读发布清单和下载 |
|
||||
|
||||
启动前端构建:
|
||||
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe tools\start_n7web.py
|
||||
```
|
||||
|
||||
启动器支持 `--host`、`--port`、`--scene-port`、`--ota-port`、`--catalog`、`--server-id`、`--python` 和 `--dry-run`。使用 `--host 0.0.0.0` 前请确认网络可信,并为公网部署启用认证和 HTTPS。
|
||||
|
||||
健康检查:
|
||||
|
||||
```powershell
|
||||
Invoke-RestMethod http://127.0.0.1:8000/api/health
|
||||
Invoke-RestMethod http://127.0.0.1:8000/api/assistant/status
|
||||
```
|
||||
|
||||
`/api/health` 依赖 Codex App Server 就绪;助手进程不可用时返回 HTTP `503`,不会伪装成可用服务。
|
||||
|
||||
### 只启动主服务
|
||||
|
||||
需要自行运行前端开发服务器或不需要模式目录/OTA 服务时,可直接运行:
|
||||
|
||||
```powershell
|
||||
$env:PYTHONPATH = "src;domains/seat/src"
|
||||
.\.venv\Scripts\python.exe -m cognitive_agent.server
|
||||
```
|
||||
|
||||
前端热更新:
|
||||
|
||||
```powershell
|
||||
cd apps\web-console
|
||||
npm run dev
|
||||
```
|
||||
|
||||
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`。
|
||||
|
||||
## 开发与验证
|
||||
|
||||
先激活 `.venv`,或把下面的 `python` 替换为 `.\.venv\Scripts\python.exe`。仓库的 `Makefile` 使用 POSIX 风格环境变量,在 Windows 上也可以直接调用 Python 脚本。
|
||||
|
||||
```bash
|
||||
# 公共 Schema、领域清单、平台资源和生成 CAN 协议
|
||||
make validate
|
||||
|
||||
# 全量 Python、前端契约和 CAN 协议回归
|
||||
make test
|
||||
make dev
|
||||
|
||||
# 仅前端运行时契约
|
||||
make test-frontend
|
||||
|
||||
# 生成或检查共享 CAN 语义协议
|
||||
make can-protocol-generate
|
||||
make can-protocol-check
|
||||
```
|
||||
|
||||
浏览器访问 `http://127.0.0.1:8000`。设置 `OPENAI_API_KEY` 后,GPT Structured Outputs 会基于各领域的 `config/capabilities.yaml` 和当前会话状态直接生成通用能力请求;后端只做结构校验、领域路由和确定性执行适配,不用本地语义规则改写 GPT 动作。本地解析器仅是未配置 GPT 或 `auto` 模式调用失败时的显式降级路径。模型由 `OPENAI_MODEL=gpt-5.6-sol` 指定;兼容 OpenAI Responses API 的服务可通过 `OPENAI_BASE_URL` 指定。无论使用哪种解析器,配置校验、CAN 编译和虚拟执行均为同一确定性路径。
|
||||
不使用 `make` 时:
|
||||
|
||||
### 一句话、快捷模式与手势控制
|
||||
|
||||
首页左侧提供一句话控制,可同时描述座椅、空调、天幕和氛围灯目标;Enter 直接执行,Shift+Enter 换行。观影、午休和零重力是可直接调用的快捷模式,“保存当前”可把当前副驾驶姿态保存为命名模式。复杂跨域自然语言优先使用 GPT 解析,本地规则用于已明确覆盖的离线指令。
|
||||
|
||||
顶部“手势控制”打开同页抽屉。浏览器在本机完成 MediaPipe 推理,识别结果仍通过同一自然语言编排、安全校验与执行链路处理。运行必需资源已经纳入本工程:
|
||||
|
||||
```text
|
||||
apps/web-console/assets/gesture/gesture_recognizer.task
|
||||
config/gesture/default.yaml
|
||||
```powershell
|
||||
.\.venv\Scripts\python.exe tools\validate_repository.py
|
||||
.\.venv\Scripts\python.exe -m pytest
|
||||
```
|
||||
|
||||
因此源码打包和容器构建不依赖相邻的 `seat_gesture_control` 目录。模型文件只保留一份,不包含原工程的测试视频、日志或验证输出。浏览器摄像头要求安全来源,本地使用 `http://127.0.0.1:8000`,远程部署使用 HTTPS;首次使用仍需浏览器授予摄像头权限。MediaPipe Web 运行时从 jsDelivr 按需加载,视频帧不上传到后端。
|
||||
仓库配置了 `pytest` 的 `contract`、`integration` 和 `hardware` markers。默认测试使用虚拟或模拟执行;真实台架测试必须按 [T2G 前端测试集](docs/testing/t2g-seat-frontend-test-suite.md) 的顺序执行,并显式确认硬件。
|
||||
|
||||
手势安全状态机采用张掌保持 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 模式只应在可信网络内临时演示。
|
||||
|
||||
## 认证与公网体验
|
||||
|
||||
公网演示必须启用 `.env.auth` 中的服务端认证。密码使用 PBKDF2-SHA256 哈希保存;认证成功后签发有有效期的 HMAC 会话 Cookie,Cookie 使用 `HttpOnly` 和 `SameSite=Strict`,经 HTTPS 代理时自动增加 `Secure`。登录失败按来源 IP 限流,GPT 请求按体验账号限制每分钟调用次数。未登录访问页面会跳转 `/login`,未登录 API 请求返回 `401`。
|
||||
|
||||
生成认证材料:
|
||||
构建双通道 CAN 读取工具需要 CMake 和外部 CANFD SDK:
|
||||
|
||||
```bash
|
||||
PYTHONPATH=src python -m cognitive_agent.auth hash-password
|
||||
PYTHONPATH=src python -m cognitive_agent.auth generate-secret
|
||||
make can-read-trans
|
||||
make can-read-trans-test
|
||||
```
|
||||
|
||||
固定公网演示部署在 `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` 仍分别用于回环地址和可信局域网开发。
|
||||
## API 入口
|
||||
|
||||
当前 dry-run 规划策略允许 GPT 在没有同名 preset 时,将可理解的模式目标分解为已注册原子能力并生成保守候选参数。候选值只用于模型理解演示,不能替代实车模式标定。
|
||||
浏览器只访问同源服务。下面是主要集成面,完整路由以 [`src/cognitive_agent/server.py`](src/cognitive_agent/server.py) 为准:
|
||||
|
||||
靠背/坐垫与坐垫/腿托支持演示级精确夹角,并可将当前五轴姿态保存为会话持久化的命名 `FunctionPreset`。保存操作不发 CAN;再次调用时先展开为五个原子电机动作,再进入同一编译、校验和虚拟执行链路。
|
||||
| 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `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` | 只读模式目录 |
|
||||
|
||||
完整能力边界、矩阵解释和验收用例见 [座椅阶段一交付](docs/workflows/seat-stage1-delivery.md)。
|
||||
`/api/internal/codex/vehicle-control` 仅供本机 Codex MCP 回调使用,需要一次性令牌和原始请求绑定,不是浏览器或第三方 API。语音、手势、NFC、固件工作台和自修复接口也由服务端权限门禁保护。
|
||||
|
||||
已验证的 T2G 加热、通风和四电机能力已合并到仓库内 `firmware/t2g/Seat_Controller_demo`,并提供一条默认锁定的真实 HIL 路径。主驾与副驾目前映射到同一物理座椅;硬件请求会先做固件身份握手,可在明确启用后通过 IAR C-SPY 自动烧录,再发送 CAN 并等待真实回读。固件拓扑、IAR 构建、ZLG 32 位 bridge、CAN 接线、安全门禁和上电步骤见 [T2G 座椅真实硬件台架接入](docs/workflows/t2g-seat-hardware-bench.md)。
|
||||
## 配置与安全
|
||||
|
||||
默认 [`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 的变更,需要同时更新对应专题文档与回归用例。
|
||||
|
||||
@@ -1,22 +1,90 @@
|
||||
# Web Console
|
||||
|
||||
Web 控制台提供一句话、快捷功能模式与手势控制入口,不在浏览器中生成 CAN。所有动作统一提交给后端编排、配置校验、安全门禁和执行链路。
|
||||
## 宠物卡片与 NFC 上报
|
||||
|
||||
主页“宠物卡片”区域支持创建多张宠物卡片。`pet_name` 与 `action` 可在卡片中编辑,
|
||||
`nfc_id` 为只读字段,只能在“NFC 设备授权”弹窗中使用读卡器当前识别到的实体 NFC
|
||||
完成绑定,不能由浏览器或 API 请求直接填写。同一个 NFC UID 最多绑定一张宠物卡片;重新
|
||||
绑定时会自动解除旧卡片上的相同 UID。新卡片必须先完成编辑并点击保存,之后才能从
|
||||
“NFC 设备绑定”或“关联卡片”入口继续绑定;右侧卡片区域不再直接触发 NFC 绑定。
|
||||
|
||||
卡片由后端持久化到 `artifacts/server/pet_cards.sqlite3`,网页关闭或服务重启不会丢失。
|
||||
同源接口如下:
|
||||
|
||||
- `GET /api/extension/pets`:读取卡片及发布器状态。
|
||||
- `POST /api/extension/pets`:创建卡片,只接受 `pet_name`、`action`。
|
||||
- `PUT /api/extension/pets/{pet_id}`:修改宠物名和动作。
|
||||
- `DELETE /api/extension/pets/{pet_id}`:删除卡片。
|
||||
- `POST /api/extension/pets/{pet_id}/bind-current-nfc`:绑定读卡器中当前在场的 NFC。
|
||||
- `POST /api/extension/pets/{pet_id}/unbind`:解除绑定。
|
||||
|
||||
服务端首次检测到已绑定 NFC 进入读卡区时,后台线程复用当前车辆 MQTT Broker 连接,
|
||||
在 1 秒内向固定话题 `n7/pet` 发布 3 帧非保留消息,随后保持静默。只有将 NFC 移出读卡区
|
||||
后再次靠近,才会触发下一组 3 帧。消息只包含以下三个字段:
|
||||
|
||||
```json
|
||||
{"pet_name":"团团","nfc_id":"E113B203","action":"摇尾巴"}
|
||||
```
|
||||
|
||||
发布行为独立于网页是否打开。若本次 3 帧中存在 Broker 发布失败,卡片与绑定关系仍会
|
||||
保存,前端会显示本次突发发送失败;为避免持续发送,服务不会在 NFC 一直停留时无限重试,
|
||||
移开并重新靠近后才会发起下一组 3 帧。
|
||||
|
||||
Web 控制台是车机端的远程自编排与同步显示界面。网页保留一句话指令和预设功能模式入口,请求通过同源服务发送至车机执行;同时同步显示车机任务、编排结果、设备反馈和 USB 扩展能力。真实 CAN 与硬件执行权限仍由服务端安全门禁控制。
|
||||
|
||||
当前页面包括:
|
||||
|
||||
- 一句话联动座椅、空调、天幕和氛围灯,支持 Enter 执行和 GPT/本地规则切换。
|
||||
- 观影、午休、零重力快捷模式,以及当前副驾驶姿态的命名保存入口。
|
||||
- 六阶段执行链路、整车联合孪生、领域状态、配置、CAN 计划和执行结果。
|
||||
- 右侧手势控制抽屉,包含摄像头画面、关键点骨架、保持进度、动作速查与自定义映射。
|
||||
- 自编排和自扩展合并为一个实时界面,登录身份固定为 `user`,不再提供模块切换。
|
||||
- AI助手默认展开;左侧控制台、车端设备状态、外设与扩展能力默认收起。四个板块统一通过页面右上角的图标按钮展开或收回,主工作区随面板状态自适应。
|
||||
- AI助手支持普通问答、工程能力核对和车控工作。能力问答读取运行时权威目录,高置信度冷热请求走确定性快速控制链,其他问题和复杂车控由 Codex App Server 流式处理;车控结果仍以自然语言和可审计的动作进度呈现。
|
||||
- 网页端可输入一句话指令或点击预设模式,车机端同样可发起任务;两端任务与结果统一同步。
|
||||
- AI助手语音入口支持双模式:手动点击麦克风按钮会立即以 `manual` 模式录音,不要求唤醒词;浏览器已授权麦克风、助手展开且页面可见时自动进入 `wake` 待机,只有听到“你好,小迪”(兼容无标点与字间停顿)后才展示并继续转录。AudioWorklet 采集后,服务端 SenseVoiceSmall 以节流预览返回未语义清洗的实时文字;静音端点立即停止采音,再由 Silero VAD、幂等 TranscriptAssembler 和理解层生成最终 `assistant_text`。普通问答、未知能力、否定和撤销表达均可回填,`commands` 仅作为受信任车控旁路,不能反向过滤助手输入。
|
||||
- 整车数字孪生位于工作区中央,点击或悬停热点可查看车窗、遮阳帘、主/副驾座椅、阅读灯、氛围灯、前大灯和空调的完整状态。
|
||||
- 车端设备状态与外设能力板块均可独立隐藏或恢复,面板状态在当前浏览器会话中保留。
|
||||
- USB 外设接入后同步显示 GSCU 链路、外设名称、类型、已学习能力和最近一次跨设备联动。
|
||||
- 左侧运行日志集中显示网页/车机任务、链路、设备与外设同步事件。
|
||||
|
||||
UI 只展示可审计的“决策摘要”和“处理轨迹”,不展示模型私有思维过程。
|
||||
|
||||
## AI助手接口
|
||||
|
||||
浏览器只使用同源接口,不直接连接 App Server、MCP 或模型服务:
|
||||
|
||||
- `GET /api/assistant/status`:返回 App Server 状态、可用性和当前会话数。
|
||||
- `GET /api/assistant/capabilities`:返回由运行时注册表、模式目录、CAN 绑定和已学习外设共同生成的权威能力快照。`service_surfaces` 只提供语音、手势、NFC、自标定和自修复的状态信息,不代表助手可调用;执行资格以 `assistant_callable` 和服务端校验为准。
|
||||
- `POST /api/assistant/stream`:提交 `message`、`conversation_id`、执行模式和当前车型等上下文,返回 `application/x-ndjson`。前端消费 `message.accepted`、`work.*`、`action.*`、`answer.delta`、`control.suggestions`、`answer.completed` 与 `error` 事件,以增量方式展示回复和控制进度。不支持的车控请求可随 `control.suggestions` 返回当前车型可执行的替代指令;前端去重后渲染为按钮卡片,点击会作为新指令沿同一受控链路提交。
|
||||
|
||||
`POST /api/internal/codex/vehicle-control` 是 App Server MCP 子进程专用的回环接口,要求一次性内部令牌且与原始用户文本绑定,不是浏览器或第三方集成接口。浏览器也不能通过响应内容扩大执行目标、切换执行模式或提交原始 CAN。
|
||||
|
||||
当前座椅阶段一控制台为无构建静态页面,由 `cognitive_agent.server` 同源提供 API 和资源。运行:
|
||||
|
||||
```bash
|
||||
make dev
|
||||
```
|
||||
|
||||
语音听写首次运行前安装运行时、官方完整精度模型和 Silero VAD:
|
||||
|
||||
```bash
|
||||
python -m pip install -e ".[speech]"
|
||||
python tools/install_sensevoice_model.py
|
||||
```
|
||||
|
||||
录音只用于当前请求,不落盘;`captured_text` 保留完整声学结果,`raw_text` 在 `manual` 模式保留完整原文、在 `wake` 模式剥离唤醒词,`assistant_text` 是面向问答的轻清洗会话,`clean_text/control_text` 与结构化 Intent 则保持严格车控语义。前后端通过 `X-Speech-Input-Mode: manual|wake` 显式区分模式;可用 `SPEECH_WAKE_WORD_ENABLED=false` 关闭唤醒门,或用竖线分隔的 `SPEECH_WAKE_WORDS` 配置主唤醒词与别名。远程浏览器必须通过 HTTPS 才能访问麦克风。诊断可设置后端 `SPEECH_DEBUG=1` 并用 `?speechDebug=1` 打开前端日志,默认关闭。
|
||||
|
||||
Android PAD/模拟器联调使用:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1
|
||||
```
|
||||
|
||||
脚本会启动已配置的 Pixel Tablet AVD、建立 `adb reverse tcp:8000 tcp:8000`、
|
||||
以真实硬件关闭/模拟台架开启的方式启动服务,并在 Android Chrome 中打开
|
||||
`http://127.0.0.1:8000`。停止联调环境:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\tools\android_pad_dev.ps1 -Stop
|
||||
```
|
||||
|
||||
局域网演示使用:
|
||||
|
||||
```bash
|
||||
@@ -25,13 +93,89 @@ make dev-lan
|
||||
|
||||
其他设备访问启动日志打印的 `http://<LAN-IP>:8000`。页面与后端 API 由同一服务提供,无需单独配置前端地址。
|
||||
|
||||
自扩展接口统一位于 `/api/extension/*`。红外模块默认使用已验证的 GSCU1B TCP
|
||||
模式:Linux 端在 `0.0.0.0:4444` 监听,GSCU1B 作为 TCP Client 主动接入。
|
||||
“外设与扩展能力”中的“TCP 设置”可修改监听 IP、端口并立即重启监听,配置持久化到
|
||||
`artifacts/tmp/gscu_gateway.json`;同一弹窗可执行查询、学习、取消、发射、读取、
|
||||
写入、软重启、WiFi 配网锁、擦除、恢复出厂和原始 JSON 调试动作。模块配置的
|
||||
服务器地址必须填写 Linux 的实际局域网 IP,不能填写 `0.0.0.0`。
|
||||
|
||||
TCP 发射、取消、写入和设置类动作在操作系统确认写入连接后立即返回“已下发”,
|
||||
不会因设备不回 ACK 而阻塞前端。固件 1.1.5 的学习动作同样不主动返回 ACK,因此
|
||||
前端在学习命令下发后自动读取目标槽位,以实际 `ir_data` 回读确认学习完成;设备查询
|
||||
和读取槽位仍等待带 `messageId` 的实际回执。
|
||||
学习时可为每个 `1–248` 红外槽位填写信号名称;槽位与名称持久化在本机
|
||||
`artifacts/server/gscu_ir_catalog.sqlite3` 数据库中,并在 TCP 动作调试区按槽位号
|
||||
升序显示为小卡片。浏览器中已有的旧名称会自动迁移到数据库。单卡片删除操作只删除
|
||||
本地目录记录,不会擦除设备红外码;已验证的设备协议仅支持全部擦除。全部擦除会先
|
||||
弹出不可恢复确认,设备命令提交成功后才同步清空本地槽位数据库。
|
||||
|
||||
如需回退 MQTT,可设置 `GSCU_TRANSPORT=mqtt`,通过 `GSCU_MQTT_*`、
|
||||
`GSCU_COMMAND_TOPIC` 和 `GSCU_RESPONSE_TOPIC` 指定 Broker 与话题;通过
|
||||
`GSCU_TRANSPORT=simulation` 使用演示学习器。MQTT 依赖已包含在核心环境中;
|
||||
NFC 串口依赖通过 `ir` 可选依赖安装。自扩展不再需要独立的 `8010` 前端服务。
|
||||
|
||||
## NFC 玩具识别与红外学习
|
||||
|
||||
统一页面的“外设与扩展能力”区域提供“感应玩具 NFC”入口。点击后将玩具上的
|
||||
NFC 卡片靠近 PCR532/PN532(CH340/CH341 串口)读卡器:新卡可建立或绑定
|
||||
玩具档案,并立即进入现有 GSCU1B 红外学习流程;已绑定卡片会自动识别玩具,
|
||||
展示全部已学习/待学习动作,并可直接发射已验证动作。
|
||||
|
||||
安装串口支持并启动:
|
||||
|
||||
```bash
|
||||
python -m pip install -e ".[ir]"
|
||||
make dev
|
||||
```
|
||||
|
||||
服务会优先复用本机 `ir-toy-learning` 的 MQTT NFC 桥(默认
|
||||
`127.0.0.1:1883`),避免两个进程同时抢占同一个 `COM` 口;桥不可用时再自动选择
|
||||
唯一的 CH340/CH341、PN532 或 PCR532 串口。可通过 `NFC_PORT=COM5` 指定串口,
|
||||
或用 `NFC_MQTT_HOST`、`NFC_MQTT_PORT` 调整桥地址;`NFC_MQTT_DISABLED=true`
|
||||
强制只使用串口,`NFC_DISABLED=true` 可完全禁用读卡线程。
|
||||
|
||||
首次加载时页面会从 `http://localhost:3001/n7web-profile-bridge.html` 安全迁移旧
|
||||
`ir-toy-learning` 浏览器档案,保留工牌 UID、玩具编号、动作名称和 GSCU1B 槽位。
|
||||
不同浏览器运行时,旧工作台会把不含红外原始波形的档案元数据同步到本机 MQTT
|
||||
保留主题 `toy/ir/profiles`,当前服务同样会自动导入。迁移按档案与槽位幂等合并,
|
||||
不会覆盖当前工程中已经建立的能力。NFC UID 与玩具档案的绑定保存在当前浏览器,
|
||||
红外波形仍保存在 GSCU1B 的 1–248 号物理槽位。
|
||||
|
||||
TCP 模式下本服务是服务器,GSCU1B 是客户端:设备配网页中的“TCP服务器”
|
||||
必须填写运行本服务的主机地址,“TCP端口”必须与 `GSCU_TCP_PORT` 一致。
|
||||
如需绑定指定网卡地址,可设置 `GSCU_TCP_LISTEN_HOST`;网卡晚于服务就绪或
|
||||
端口被短暂占用时,监听器会按 `GSCU_TCP_BIND_RETRY_SECONDS`(默认 1 秒)
|
||||
自动重试。`/api/extension/health` 的 `listening` 表示服务器已监听,只有收到
|
||||
真实设备会话后 `connected`/`peer_connected` 才会变为 `true`。
|
||||
|
||||
车机与网页同步使用以下同源接口:
|
||||
|
||||
- `GET /api/vehicle-display-sync`:网页轮询最新车机任务、结果、外设目录与扩展链路状态。
|
||||
- `POST /api/requests`:车机执行时附带 `client_type: "head_unit"`;网页控制台附带 `client_type: "web_console"`。两类任务的执行结果都会写入同步快照。
|
||||
- `POST /api/extension/display-sync`:车机刚识别 USB 外设时上报 `device` 与 `peripherals` 能力目录,无需等待首次执行。
|
||||
- `POST /api/extension/assistant/plan` 和 `/api/extension/vehicle/execute`:联动规划与车端执行结果同样写入同步快照。
|
||||
|
||||
右上角“能耗监测”入口以独立固定页面展示 7 个当前可控车端设备的实时功率、
|
||||
本次监测累计能耗、总功率趋势和分设备趋势。能耗链路优先接收
|
||||
`cognitive-agent:power-telemetry` 事件,也兼容调用
|
||||
`window.updateDevicePowerTelemetry(payload)` 的现有集成方式;载荷可使用
|
||||
`devices.<device>.power_w` 或 `total_power_w`,并可附带 `energy_wh`、
|
||||
`trip_energy_kwh` 或 `today_energy_kwh`。累计能耗独立保留,瞬时功率超过 15 秒
|
||||
未更新时只会回退到状态估算,不会让已上报的车端累计值消失。每条遥测应显式提供
|
||||
`source: "vehicle_power_sensor"`(实测)或 `source: "estimated"`(估算)。
|
||||
当前 CAN 状态协议没有独立电流/
|
||||
功率电表信号,因此未注入新鲜功率数据的设备会显示带 `≈` 和“状态估算”标签的
|
||||
确定性实时估算,绝不标记为车端实测。真实功率通道接入后页面会逐设备自动切换为
|
||||
“实测”,无需更换页面或曲线实现。
|
||||
|
||||
公网入口由后端统一认证,前端不保存密码或 API Key。未认证访问自动进入
|
||||
`/login`,主控制台提供退出按钮。固定入口是
|
||||
`https://511.kendeji.fun`,应用容器只监听服务器回环地址,再由 Caddy 提供
|
||||
HTTPS。部署方式见
|
||||
[`deploy/server/README.md`](../../deploy/server/README.md)。
|
||||
|
||||
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。硬件执行在服务端固定关闭,不是仅靠前端按钮限制。
|
||||
页面支持 GPT/本地解析切换、六阶段流水线、主副驾热舒适、五轴位置状态和 3D 座椅孪生、GPT 结构化能力请求、决策与校验轨迹、FunctionConfig、CommandPlan、虚拟帧和 ExecutionReport。真实硬件执行由服务端门禁控制,不是仅靠前端按钮限制;Android 联调脚本会显式关闭真实硬件执行。
|
||||
|
||||
## 手势资源与安全规则
|
||||
|
||||
@@ -42,7 +186,16 @@ assets/gesture/gesture_recognizer.task
|
||||
../../config/gesture/default.yaml
|
||||
```
|
||||
|
||||
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web 运行时从 jsDelivr 按需加载;摄像头帧和手部关键点只在当前浏览器中处理。
|
||||
后端提供 `/api/gesture/runtime` 和 `/api/gesture/model`,前端以二进制缓冲区加载模型,避免依赖外部 `seat_gesture_control` 目录。MediaPipe Web、Lucide 和 Three.js 均按固定版本保存在 `vendor/`,运行时不访问外部 CDN;摄像头帧和手部关键点只在当前浏览器中处理。
|
||||
|
||||
## PAD、PWA 与离线行为
|
||||
|
||||
- 竖屏 PAD 在 900 CSS px 以下切换为上下布局,横屏保留指令栏与结果区;触控目标最小 48 dp,并处理安全区和动态视口。
|
||||
- 顶部连接徽标由 `/api/health` 周期探测驱动;GET 可有限重试,POST 控制请求不自动重试。
|
||||
- Service Worker 缓存页面壳、固定版本依赖和按需访问的 3D/手势资源;`/api/requests` 等控制 API 始终走网络且不会写入缓存。
|
||||
- 从 Android Chrome 的“安装应用”可进入 standalone PWA;首次完整在线加载后可离线打开控制台壳,实际编排仍要求 Windows 服务可达。
|
||||
|
||||
模拟台架需要 `COGNITIVE_AGENT_SIMULATED_HARDWARE_ENABLED=true`。选择“模拟台架”后可注入反馈超时、适配器断开、电机堵转、固件不匹配和部分反馈。结果会标记 `simulation=true`、`seat_feedback=simulated_hardware` 和 `physical_can_opened=false`,不会调用真实 CAN、ZLG 或 IAR。
|
||||
|
||||
安全规则:张掌保持 1 秒激活;动作保持 600 ms 且至少连续 8 帧才执行;换动作不重新锁止;握拳立即停止但不解除激活;3 秒无有效动作自动锁止;单次连续运动最多 1.5 秒;检测到多只手时不执行。自定义映射保存在浏览器 `localStorage`。
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 115 KiB |
@@ -0,0 +1,897 @@
|
||||
/* Independent whole-vehicle energy monitor -------------------------------- */
|
||||
.console-v2 .energy-monitor-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 .energy-monitor-layer[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-page {
|
||||
width: min(1480px, 100%);
|
||||
height: min(900px, 100%);
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
overflow: hidden;
|
||||
color: var(--foreground);
|
||||
background:
|
||||
radial-gradient(circle at 7% 0%, color-mix(in oklch, var(--primary) 10%, transparent), transparent 30%),
|
||||
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);
|
||||
font-variant-numeric: tabular-nums;
|
||||
animation: energy-monitor-enter 180ms ease-out both;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-header {
|
||||
min-height: 76px;
|
||||
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 .energy-monitor-heading,
|
||||
.console-v2 .energy-monitor-live-state,
|
||||
.console-v2 .energy-monitor-heading > div,
|
||||
.console-v2 .energy-monitor-heading > span,
|
||||
.console-v2 .energy-monitor-live-label,
|
||||
.console-v2 .energy-trend-card > header,
|
||||
.console-v2 .energy-devices-card > header,
|
||||
.console-v2 .energy-trend-card > footer,
|
||||
.console-v2 .energy-device-identity small {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading {
|
||||
min-width: 0;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading > span {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
flex: 0 0 46px;
|
||||
justify-content: 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;
|
||||
box-shadow: inset 0 1px 0 color-mix(in oklch, var(--primary-foreground) 70%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading > span svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading > div {
|
||||
min-width: 0;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading h2,
|
||||
.console-v2 .energy-trend-card h3,
|
||||
.console-v2 .energy-devices-card h3 {
|
||||
margin: 0;
|
||||
color: var(--heading-foreground, var(--foreground));
|
||||
letter-spacing: -.025em;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading h2 {
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading p,
|
||||
.console-v2 .energy-trend-card header p,
|
||||
.console-v2 .energy-devices-card header p {
|
||||
margin: 0;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-live-state {
|
||||
flex: 0 0 auto;
|
||||
gap: 10px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-live-label {
|
||||
gap: 6px;
|
||||
color: var(--primary);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-live-label > i,
|
||||
.console-v2 .energy-trend-card > footer span > i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
display: block;
|
||||
background: var(--primary);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-source {
|
||||
padding: 5px 9px;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: 999px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-source[data-source="simulated"],
|
||||
.console-v2 .energy-monitor-source[data-source="simulated_estimated"],
|
||||
.console-v2 .energy-monitor-source[data-source="measured_simulated"],
|
||||
.console-v2 .energy-monitor-source[data-source="measured_simulated_estimated"] {
|
||||
color: color-mix(in oklch, var(--primary) 72%, #7c3aed);
|
||||
}
|
||||
|
||||
.console-v2 #energyMonitorClose {
|
||||
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;
|
||||
box-shadow: 0 4px 12px color-mix(in oklch, var(--foreground) 7%, transparent);
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease, background 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.console-v2 #energyMonitorClose:hover,
|
||||
.console-v2 #energyMonitorClose:focus-visible {
|
||||
color: var(--primary);
|
||||
background: var(--accent);
|
||||
border-color: color-mix(in oklch, var(--primary) 28%, var(--border));
|
||||
outline: 3px solid color-mix(in oklch, var(--ring) 16%, transparent);
|
||||
outline-offset: 1px;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.console-v2 #energyMonitorClose svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-content {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: 1.18fr 1fr 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi {
|
||||
min-width: 0;
|
||||
min-height: 94px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
overflow: hidden;
|
||||
padding: 13px 15px;
|
||||
background: color-mix(in oklch, var(--card) 94%, var(--secondary));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 11%, var(--border));
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 6px 16px color-mix(in oklch, var(--foreground) 5%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi-primary {
|
||||
background:
|
||||
linear-gradient(135deg, color-mix(in oklch, var(--primary) 10%, var(--card)), color-mix(in oklch, var(--primary) 3%, var(--card)));
|
||||
border-color: color-mix(in oklch, var(--primary) 42%, var(--border));
|
||||
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > small {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > strong {
|
||||
min-width: 0;
|
||||
color: var(--heading-foreground, var(--foreground));
|
||||
font-size: clamp(19px, 1.9vw, 28px);
|
||||
line-height: 1.1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi-primary > strong {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
color: var(--primary);
|
||||
font-size: clamp(27px, 2.6vw, 38px);
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi-primary > strong span {
|
||||
color: var(--foreground);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > em {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 10px;
|
||||
font-style: normal;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi-primary > svg {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
color: color-mix(in oklch, var(--primary) 20%, transparent);
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-dashboard {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.58fr) minmax(330px, .92fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card,
|
||||
.console-v2 .energy-devices-card {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklch, var(--card) 96%, var(--secondary));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 11%, var(--border));
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 7px 18px color-mix(in oklch, var(--foreground) 5%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr) auto;
|
||||
padding: 15px 16px 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > header,
|
||||
.console-v2 .energy-devices-card > header {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card h3,
|
||||
.console-v2 .energy-devices-card h3 {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > header > strong {
|
||||
color: var(--primary);
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs {
|
||||
display: flex;
|
||||
gap: 7px;
|
||||
padding: 11px 0 4px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs button {
|
||||
min-height: 30px;
|
||||
padding: 5px 13px;
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, background 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs button:hover,
|
||||
.console-v2 .energy-trend-tabs button:focus-visible {
|
||||
color: var(--primary);
|
||||
border-color: color-mix(in oklch, var(--primary) 28%, var(--border));
|
||||
outline: 2px solid color-mix(in oklch, var(--ring) 14%, transparent);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs button.active {
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary);
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 5px 13px color-mix(in oklch, var(--primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-wrap {
|
||||
min-height: 210px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-chart {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 210px;
|
||||
display: block;
|
||||
color: var(--primary);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-grid line {
|
||||
stroke: color-mix(in oklch, var(--primary) 10%, var(--border));
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 3 5;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-area {
|
||||
fill: url(#energyMonitorAreaGradient);
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-line {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
filter: drop-shadow(0 3px 5px color-mix(in oklch, var(--primary) 18%, transparent));
|
||||
transition: points 260ms linear;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-y {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: 0;
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--card) 82%, transparent);
|
||||
font-size: 9px;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-y-top { top: 8px; }
|
||||
.console-v2 .energy-chart-y-mid { top: 49%; }
|
||||
.console-v2 .energy-chart-y-bottom { bottom: 26px; }
|
||||
|
||||
.console-v2 .energy-chart-x {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 3px;
|
||||
left: 42px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > footer {
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding-top: 5px;
|
||||
color: var(--muted-foreground);
|
||||
border-top: 1px solid color-mix(in oklch, var(--primary) 8%, var(--border));
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > footer span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--foreground);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > footer p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr) auto;
|
||||
padding: 15px 12px 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card > header {
|
||||
padding: 0 3px 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card > header > span {
|
||||
padding: 5px 8px;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 8%, var(--card));
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-list {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
align-content: start;
|
||||
gap: 6px;
|
||||
overflow: auto;
|
||||
padding: 1px 3px 4px;
|
||||
scrollbar-color: color-mix(in oklch, var(--primary) 24%, var(--border)) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(82px, 1fr) auto 72px 34px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 7px;
|
||||
color: var(--foreground);
|
||||
text-align: left;
|
||||
background: color-mix(in oklch, var(--card) 88%, var(--secondary));
|
||||
border: 1px solid transparent;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, background 150ms ease, border-color 150ms ease, transform 150ms ease;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row:hover,
|
||||
.console-v2 .energy-device-row:focus-visible,
|
||||
.console-v2 .energy-device-row.active {
|
||||
color: var(--foreground);
|
||||
background: color-mix(in oklch, var(--primary) 7%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--primary) 25%, var(--border));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row:focus-visible {
|
||||
box-shadow: 0 0 0 3px color-mix(in oklch, var(--ring) 14%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row.active {
|
||||
box-shadow: inset 3px 0 0 var(--primary);
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 14%, var(--border));
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity strong {
|
||||
overflow: hidden;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity small {
|
||||
min-width: 0;
|
||||
gap: 4px;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity small b {
|
||||
color: var(--primary);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity small b[data-source="estimated"] {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity small i {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
flex: 0 0 2px;
|
||||
background: currentColor;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-identity small span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-metrics {
|
||||
min-width: 64px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-metrics strong {
|
||||
color: var(--heading-foreground, var(--foreground));
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-metrics small {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-sparkline {
|
||||
width: 72px;
|
||||
height: 24px;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-sparkline polyline {
|
||||
fill: none;
|
||||
stroke: var(--primary);
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share > b {
|
||||
color: var(--primary);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share > i {
|
||||
width: 30px;
|
||||
height: 3px;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--border));
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share em {
|
||||
width: var(--energy-share, 0%);
|
||||
height: 100%;
|
||||
display: block;
|
||||
background: var(--primary);
|
||||
border-radius: inherit;
|
||||
transition: width 260ms linear;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card > footer {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 7px;
|
||||
margin-top: 6px;
|
||||
padding: 8px 9px;
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--primary) 5%, var(--secondary));
|
||||
border-radius: 9px;
|
||||
font-size: 9px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card > footer svg {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
flex: 0 0 13px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.console-v2 #v2PanelControlDock > .energy-monitor-toggle {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-toggle-live {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
top: 6px;
|
||||
display: block;
|
||||
background: var(--primary);
|
||||
border: 1.5px solid var(--card);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 2px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
@keyframes energy-monitor-enter {
|
||||
from { opacity: 0; transform: translateY(8px) scale(.992); }
|
||||
to { opacity: 1; transform: translateY(0) scale(1); }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.console-v2 .energy-monitor-content {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-dashboard {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card {
|
||||
min-height: 390px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card {
|
||||
min-height: 450px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row {
|
||||
grid-template-columns: 34px minmax(72px, 1fr) auto 52px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-sparkline {
|
||||
width: 52px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.console-v2 .energy-monitor-layer {
|
||||
inset: 0;
|
||||
padding: 56px 8px 8px;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 17px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-header {
|
||||
min-height: 66px;
|
||||
gap: 8px;
|
||||
padding: 10px 10px 9px 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading > span {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
flex-basis: 38px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading > span svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading h2 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-heading p,
|
||||
.console-v2 #energyMonitorFreshness {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-live-state {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-live-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-source {
|
||||
padding: 4px 7px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-v2 #energyMonitorClose {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex-basis: 34px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-content {
|
||||
gap: 9px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-kpis {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi {
|
||||
min-height: 82px;
|
||||
padding: 10px 11px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > strong {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi-primary > strong {
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card {
|
||||
min-height: 340px;
|
||||
padding: 12px 11px 9px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-wrap,
|
||||
.console-v2 .energy-monitor-chart {
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs {
|
||||
gap: 5px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-tabs button {
|
||||
flex: 0 0 auto;
|
||||
padding-inline: 11px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-devices-card {
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-list {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row {
|
||||
grid-template-columns: 34px minmax(70px, 1fr) auto 62px 34px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-sparkline {
|
||||
width: 62px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.console-v2 .energy-monitor-heading p,
|
||||
.console-v2 .energy-monitor-live-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-source {
|
||||
max-width: 74px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-kpis {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi {
|
||||
min-height: 78px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > small {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-kpi > em {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-row {
|
||||
grid-template-columns: 32px minmax(64px, 1fr) auto 50px;
|
||||
gap: 6px;
|
||||
padding-inline: 5px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-share {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-device-sparkline {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.console-v2 .energy-trend-card > footer p {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 340px) {
|
||||
.console-v2 .energy-monitor-heading > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-monitor-source {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.console-v2 .energy-monitor-page {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.console-v2 .energy-chart-line,
|
||||
.console-v2 .energy-device-share em,
|
||||
.console-v2 .energy-device-row,
|
||||
.console-v2 #energyMonitorClose {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const $ = (selector, root = document) => root.querySelector(selector);
|
||||
const $$ = (selector, root = document) => [...root.querySelectorAll(selector)];
|
||||
const SAMPLE_INTERVAL_MS = 1000;
|
||||
const MEASURED_STALE_MS = 15000;
|
||||
const HISTORY_LIMIT = 60;
|
||||
const MAX_DEVICE_POWER_W = 100000;
|
||||
const MAX_REPORTED_ENERGY_WH = 1000000000;
|
||||
|
||||
const ENERGY_MONITORED_DEVICES = Object.freeze([
|
||||
{ id: "passenger", telemetryKeys: ["passenger", "passenger_seat"], name: "副驾座椅", icon: "armchair", phase: 1 },
|
||||
{ id: "driver", telemetryKeys: ["driver", "driver_seat"], name: "主驾座椅", icon: "armchair", phase: 2 },
|
||||
{ id: "climate", telemetryKeys: ["climate", "hvac"], name: "空调", icon: "snowflake", phase: 3 },
|
||||
{ id: "shade", telemetryKeys: ["shade", "roof", "sunshade"], name: "遮阳帘", icon: "panels-top-left", phase: 4 },
|
||||
{ id: "windows", telemetryKeys: ["windows", "window"], name: "车窗", icon: "gallery-vertical-end", phase: 5 },
|
||||
{ id: "reading", telemetryKeys: ["reading", "reading_light", "interior_light"], name: "阅读灯", icon: "book-open", phase: 6 },
|
||||
{ id: "ambient", telemetryKeys: ["ambient", "ambient_light"], name: "氛围灯", icon: "sparkles", phase: 7 },
|
||||
]);
|
||||
|
||||
const SERIES = Object.freeze([
|
||||
{ id: "total", name: "总功率", devices: ENERGY_MONITORED_DEVICES.map(({ id }) => id) },
|
||||
{ id: "climate", name: "空调", devices: ["climate"] },
|
||||
{ id: "seats", name: "座椅", devices: ["passenger", "driver"] },
|
||||
{ id: "lights", name: "灯光", devices: ["reading", "ambient"] },
|
||||
]);
|
||||
|
||||
const aliasToDevice = new Map();
|
||||
ENERGY_MONITORED_DEVICES.forEach((device) => {
|
||||
device.telemetryKeys.forEach((key) => aliasToDevice.set(key, device.id));
|
||||
});
|
||||
|
||||
const monitor = {
|
||||
mounted: false,
|
||||
open: false,
|
||||
activeSeries: "total",
|
||||
timer: null,
|
||||
startedAt: Date.now(),
|
||||
lastSampleAt: null,
|
||||
frames: [],
|
||||
latest: null,
|
||||
measured: new Map(),
|
||||
reportedEnergy: new Map(),
|
||||
energyWh: Object.fromEntries(ENERGY_MONITORED_DEVICES.map(({ id }) => [id, 0])),
|
||||
motionUntil: new Map(),
|
||||
controlRequests: new Map(),
|
||||
returnFocus: null,
|
||||
backgroundInert: [],
|
||||
};
|
||||
|
||||
function finiteNonNegative(value) {
|
||||
if (value === null || value === undefined || value === "") return null;
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) && number >= 0 ? number : null;
|
||||
}
|
||||
|
||||
function finitePower(value) {
|
||||
const number = finiteNonNegative(value);
|
||||
return number !== null && number <= MAX_DEVICE_POWER_W ? number : null;
|
||||
}
|
||||
|
||||
function finiteReportedEnergy(value) {
|
||||
const number = finiteNonNegative(value);
|
||||
return number !== null && number <= MAX_REPORTED_ENERGY_WH ? number : null;
|
||||
}
|
||||
|
||||
function normalizeSource(value) {
|
||||
return String(value || "").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
||||
}
|
||||
|
||||
function telemetrySource(telemetry, payload) {
|
||||
if (telemetry?.simulated === true || payload?.simulated === true) return "simulated";
|
||||
if (telemetry?.estimated === true || payload?.estimated === true) return "estimated";
|
||||
|
||||
const declared = normalizeSource(
|
||||
telemetry?.source
|
||||
|| telemetry?.data_source
|
||||
|| payload?.source
|
||||
|| payload?.data_source,
|
||||
);
|
||||
if (/(^|_)(simulated|simulation|demo|mock|synthetic|virtual)(_|$)/.test(declared)) {
|
||||
return "simulated";
|
||||
}
|
||||
if (/(^|_)(estimated|estimate|derived|inferred|calculated|projected|unknown)(_|$)/.test(declared)) {
|
||||
return "estimated";
|
||||
}
|
||||
if (telemetry?.measured === true || payload?.measured === true) return "measured";
|
||||
if (/(^|_)(measured|meter|sensor|hardware|physical|vehicle|can|mqtt)(_|$)/.test(declared)) {
|
||||
return "measured";
|
||||
}
|
||||
// An unlabelled caller is not enough evidence to claim hardware measurement.
|
||||
return "estimated";
|
||||
}
|
||||
|
||||
function text(selector, fallback = "") {
|
||||
return $(selector)?.textContent?.trim() || fallback;
|
||||
}
|
||||
|
||||
function stateFlag(value) {
|
||||
const normalized = String(value || "").trim();
|
||||
if (/OFF|关闭|停止|未运行/i.test(normalized)) return false;
|
||||
if (/ON|ACTIVE|AMBIENT|INTERIOR|开启|运行|L[1-9]|高|中|低/i.test(normalized)) return true;
|
||||
return null;
|
||||
}
|
||||
|
||||
function numericText(selector, fallback) {
|
||||
const match = text(selector).match(/-?\d+(?:\.\d+)?/);
|
||||
const value = match ? Number(match[0]) : NaN;
|
||||
return Number.isFinite(value) ? value : fallback;
|
||||
}
|
||||
|
||||
function deterministicWave(now, phase, amplitude = 0.04) {
|
||||
const seconds = now / 1000;
|
||||
return 1
|
||||
+ Math.sin(seconds / 2.4 + phase * 1.37) * amplitude
|
||||
+ Math.sin(seconds / 5.7 + phase * 0.71) * amplitude * 0.45;
|
||||
}
|
||||
|
||||
function controlIsActive(deviceId, now) {
|
||||
if ((monitor.motionUntil.get(deviceId) || 0) > now) return true;
|
||||
const card = $(`.v2-synced-device[data-device="${deviceId}"]`);
|
||||
return /等待车机同步/.test(card?.querySelector("[data-device-control-apply]")?.textContent || "");
|
||||
}
|
||||
|
||||
function estimateDevicePower(device, now) {
|
||||
let watts = 0;
|
||||
let activity = "待机";
|
||||
const moving = controlIsActive(device.id, now);
|
||||
|
||||
if (device.id === "climate") {
|
||||
const power = stateFlag(text("#climatePower"));
|
||||
const fan = Math.max(0, Math.min(7, numericText("#fanLevel", 3)));
|
||||
const target = (numericText("#driverTemperature", 24) + numericText("#passengerTemperature", 24)) / 2;
|
||||
if (power === false) {
|
||||
watts = 3.2;
|
||||
activity = "待机";
|
||||
} else {
|
||||
watts = 390 + fan * 48 + Math.abs(24 - target) * 32;
|
||||
activity = power === true ? `${fan} 档运行` : "状态估算";
|
||||
}
|
||||
if (moving) {
|
||||
watts += 95;
|
||||
activity = "调节执行中";
|
||||
}
|
||||
} else if (device.id === "passenger") {
|
||||
const card = $('.seat-card[data-seat="passenger"]');
|
||||
const ventilation = stateFlag(card?.dataset.ventilationState);
|
||||
const heating = stateFlag(card?.dataset.heatingState);
|
||||
const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1));
|
||||
const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1));
|
||||
watts = 5.6 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (moving ? 130 : 0);
|
||||
activity = moving ? "执行器动作" : ventilation || heating ? "舒适功能运行" : "待机";
|
||||
} else if (device.id === "driver") {
|
||||
const card = $('.seat-card[data-seat="driver"]');
|
||||
const ventilation = stateFlag(card?.dataset.ventilationState || text('.seat-card[data-seat="driver"] [data-function="ventilation"] b'));
|
||||
const heating = stateFlag(card?.dataset.heatingState || text('.seat-card[data-seat="driver"] [data-function="heating"] b'));
|
||||
const massage = stateFlag(card?.dataset.massageState || text('.seat-card[data-seat="driver"] [data-function="massage"] b'));
|
||||
const ventilationLevel = Math.max(1, Math.min(3, Number(card?.dataset.ventilationLevel) || 1));
|
||||
const heatingLevel = Math.max(1, Math.min(3, Number(card?.dataset.heatingLevel) || 1));
|
||||
watts = 6.4 + (ventilation ? 7 + ventilationLevel * 7 : 0) + (heating ? 18 + heatingLevel * 16 : 0) + (massage ? 31 : 0) + (moving ? 92 : 0);
|
||||
activity = moving ? "执行器动作" : ventilation || heating || massage ? "舒适功能运行" : "待机";
|
||||
} else if (device.id === "windows") {
|
||||
watts = moving ? 118 : 2.8;
|
||||
activity = moving ? "车窗动作" : "待机";
|
||||
} else if (device.id === "shade") {
|
||||
watts = moving ? 86 : 1.9;
|
||||
activity = moving ? "遮阳帘动作" : "待机";
|
||||
} else if (device.id === "reading") {
|
||||
const power = stateFlag(text("#interiorLightPower"));
|
||||
watts = power === false ? 0.8 : power === true ? 18 : 7.5;
|
||||
activity = power === true ? "已开启" : power === false ? "已关闭" : "状态估算";
|
||||
if (moving) {
|
||||
watts += 6;
|
||||
activity = "切换执行中";
|
||||
}
|
||||
} else if (device.id === "ambient") {
|
||||
const power = stateFlag(text("#ambientPower"));
|
||||
const brightness = Math.max(1, Math.min(5, numericText("#ambientBrightness", 3)));
|
||||
watts = power === false ? 1.1 : power === true ? 8 + brightness * 6.2 : 18;
|
||||
activity = power === true ? `${brightness} 级亮度` : power === false ? "已关闭" : "状态估算";
|
||||
if (moving) {
|
||||
watts += 5;
|
||||
activity = "调光执行中";
|
||||
}
|
||||
}
|
||||
|
||||
const amplitude = watts < 10 ? 0.025 : device.id === "climate" ? 0.055 : 0.04;
|
||||
return {
|
||||
watts: Math.max(0, watts * deterministicWave(now, device.phase, amplitude)),
|
||||
source: "estimated",
|
||||
activity,
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
function readingForDevice(device, now) {
|
||||
const measured = monitor.measured.get(device.id);
|
||||
const reportedEnergy = monitor.reportedEnergy.get(device.id);
|
||||
if (measured && now - measured.receivedAt <= MEASURED_STALE_MS) {
|
||||
const activity = measured.source === "measured"
|
||||
? "车端功率通道"
|
||||
: measured.source === "simulated" ? "模拟功率通道" : "外部估算通道";
|
||||
return {
|
||||
watts: measured.watts,
|
||||
source: measured.source,
|
||||
activity,
|
||||
updatedAt: measured.updatedAt || measured.receivedAt,
|
||||
reportedEnergyWh: reportedEnergy?.wattsHours ?? null,
|
||||
reportedEnergyLabel: reportedEnergy?.label || "",
|
||||
reportedEnergySource: reportedEnergy?.source || "",
|
||||
};
|
||||
}
|
||||
return {
|
||||
...estimateDevicePower(device, now),
|
||||
reportedEnergyWh: reportedEnergy?.wattsHours ?? null,
|
||||
reportedEnergyLabel: reportedEnergy?.label || "",
|
||||
reportedEnergySource: reportedEnergy?.source || "",
|
||||
};
|
||||
}
|
||||
|
||||
function frameSeriesValue(frame, seriesId) {
|
||||
const exactDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === seriesId);
|
||||
const deviceIds = exactDevice
|
||||
? [exactDevice.id]
|
||||
: SERIES.find(({ id }) => id === seriesId)?.devices || SERIES[0].devices;
|
||||
return deviceIds.reduce((sum, id) => sum + (frame.values[id] || 0), 0);
|
||||
}
|
||||
|
||||
function publicSnapshot() {
|
||||
if (!monitor.latest) return null;
|
||||
return {
|
||||
timestamp: monitor.latest.timestamp,
|
||||
total_power_w: monitor.latest.total,
|
||||
total_energy_wh: Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0),
|
||||
source: monitor.latest.source,
|
||||
measured_count: monitor.latest.measuredCount,
|
||||
simulated_count: monitor.latest.simulatedCount,
|
||||
estimated_count: monitor.latest.estimatedCount,
|
||||
total_energy_scope: "monitor_power_integral_estimate",
|
||||
devices: Object.fromEntries(ENERGY_MONITORED_DEVICES.map((device) => {
|
||||
const reading = monitor.latest.readings[device.id];
|
||||
return [device.id, {
|
||||
name: device.name,
|
||||
power_w: reading.watts,
|
||||
energy_wh: monitor.energyWh[device.id],
|
||||
reported_energy_wh: reading.reportedEnergyWh,
|
||||
reported_energy_label: reading.reportedEnergyLabel,
|
||||
reported_energy_source: reading.reportedEnergySource,
|
||||
source: reading.source,
|
||||
activity: reading.activity,
|
||||
}];
|
||||
})),
|
||||
sample_count: monitor.frames.length,
|
||||
};
|
||||
}
|
||||
|
||||
function pruneExpiredControlRequests(now) {
|
||||
monitor.controlRequests.forEach((request, requestId) => {
|
||||
if (request.until <= now) monitor.controlRequests.delete(requestId);
|
||||
});
|
||||
}
|
||||
|
||||
function sampleNow({ now = Date.now(), integrate = true, render = true } = {}) {
|
||||
const timestamp = Number(now);
|
||||
if (!Number.isFinite(timestamp)) return publicSnapshot();
|
||||
pruneExpiredControlRequests(timestamp);
|
||||
const readings = {};
|
||||
const values = {};
|
||||
let total = 0;
|
||||
let measuredCount = 0;
|
||||
let simulatedCount = 0;
|
||||
|
||||
ENERGY_MONITORED_DEVICES.forEach((device) => {
|
||||
const reading = readingForDevice(device, timestamp);
|
||||
readings[device.id] = reading;
|
||||
values[device.id] = reading.watts;
|
||||
total += finitePower(reading.watts) ?? 0;
|
||||
if (reading.source === "measured") measuredCount += 1;
|
||||
if (reading.source === "simulated") simulatedCount += 1;
|
||||
});
|
||||
|
||||
const elapsedMs = monitor.lastSampleAt === null
|
||||
? 0
|
||||
: Math.max(0, timestamp - monitor.lastSampleAt);
|
||||
if (integrate && elapsedMs > 0) {
|
||||
ENERGY_MONITORED_DEVICES.forEach(({ id }) => {
|
||||
const previous = monitor.frames.at(-1)?.values?.[id] ?? values[id];
|
||||
monitor.energyWh[id] += ((previous + values[id]) / 2) * elapsedMs / 3600000;
|
||||
});
|
||||
}
|
||||
|
||||
monitor.lastSampleAt = timestamp;
|
||||
monitor.frames.push({ timestamp, values, total });
|
||||
if (monitor.frames.length > HISTORY_LIMIT) monitor.frames.splice(0, monitor.frames.length - HISTORY_LIMIT);
|
||||
const estimatedCount = ENERGY_MONITORED_DEVICES.length - measuredCount - simulatedCount;
|
||||
const source = measuredCount === ENERGY_MONITORED_DEVICES.length
|
||||
? "measured"
|
||||
: simulatedCount === ENERGY_MONITORED_DEVICES.length
|
||||
? "simulated"
|
||||
: estimatedCount === ENERGY_MONITORED_DEVICES.length
|
||||
? "estimated"
|
||||
: measuredCount && simulatedCount && estimatedCount
|
||||
? "measured_simulated_estimated"
|
||||
: measuredCount && simulatedCount
|
||||
? "measured_simulated"
|
||||
: measuredCount ? "mixed" : "simulated_estimated";
|
||||
monitor.latest = {
|
||||
timestamp,
|
||||
readings,
|
||||
total,
|
||||
measuredCount,
|
||||
simulatedCount,
|
||||
estimatedCount,
|
||||
source,
|
||||
};
|
||||
if (render && monitor.mounted) renderMonitor();
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:energy-snapshot", { detail: publicSnapshot() }));
|
||||
return publicSnapshot();
|
||||
}
|
||||
|
||||
function ingestPowerTelemetry(payload, { sample = true } = {}) {
|
||||
const receivedAt = Date.now();
|
||||
const updates = payload?.devices && typeof payload.devices === "object"
|
||||
? Object.entries(payload.devices)
|
||||
: payload?.device ? [[payload.device, payload]] : [];
|
||||
updates.forEach(([rawKey, rawTelemetry]) => {
|
||||
const deviceId = aliasToDevice.get(String(rawKey || "").trim().toLowerCase());
|
||||
if (!deviceId) return;
|
||||
const telemetry = rawTelemetry && typeof rawTelemetry === "object"
|
||||
? rawTelemetry
|
||||
: { power_w: rawTelemetry };
|
||||
const directEnergyWh = finiteReportedEnergy(telemetry.energy_wh ?? telemetry.total_energy_wh);
|
||||
const tripEnergyKwh = finiteNonNegative(telemetry.trip_energy_kwh);
|
||||
const todayEnergyKwh = finiteNonNegative(telemetry.today_energy_kwh);
|
||||
const tripEnergyWh = tripEnergyKwh === null ? null : finiteReportedEnergy(tripEnergyKwh * 1000);
|
||||
const todayEnergyWh = todayEnergyKwh === null ? null : finiteReportedEnergy(todayEnergyKwh * 1000);
|
||||
const reportedEnergyWh = directEnergyWh
|
||||
?? tripEnergyWh
|
||||
?? todayEnergyWh
|
||||
?? null;
|
||||
const reportedEnergyLabel = directEnergyWh !== null
|
||||
? String(telemetry.energy_label || "车端累计")
|
||||
: tripEnergyWh !== null ? "本次行程"
|
||||
: todayEnergyWh !== null ? "今日累计"
|
||||
: "";
|
||||
const source = telemetrySource(telemetry, payload);
|
||||
if (reportedEnergyWh !== null) {
|
||||
monitor.reportedEnergy.set(deviceId, {
|
||||
wattsHours: reportedEnergyWh,
|
||||
label: reportedEnergyLabel,
|
||||
source,
|
||||
receivedAt,
|
||||
});
|
||||
}
|
||||
const watts = finitePower(
|
||||
telemetry.power_w ?? telemetry.total_power_w ?? telemetry.current_power_w,
|
||||
);
|
||||
if (watts === null) return;
|
||||
const parsedUpdatedAt = telemetry.updated_at ? new Date(telemetry.updated_at).getTime() : NaN;
|
||||
monitor.measured.set(deviceId, {
|
||||
watts,
|
||||
receivedAt,
|
||||
updatedAt: Number.isFinite(parsedUpdatedAt) ? parsedUpdatedAt : receivedAt,
|
||||
source,
|
||||
});
|
||||
});
|
||||
return sample ? sampleNow() : publicSnapshot();
|
||||
}
|
||||
|
||||
function formatPowerParts(watts) {
|
||||
const value = finiteNonNegative(watts) || 0;
|
||||
if (value >= 1000) return { value: (value / 1000).toFixed(value >= 10000 ? 1 : 2), unit: "kW" };
|
||||
return { value: String(Math.round(value)), unit: "W" };
|
||||
}
|
||||
|
||||
function formatPower(watts, estimated = false) {
|
||||
const parts = formatPowerParts(watts);
|
||||
return `${estimated ? "≈ " : ""}${parts.value} ${parts.unit}`;
|
||||
}
|
||||
|
||||
function formatEnergy(wh, estimated = false) {
|
||||
const value = finiteNonNegative(wh) || 0;
|
||||
const prefix = estimated ? "≈ " : "";
|
||||
if (value >= 1000) return `${prefix}${(value / 1000).toFixed(3)} kWh`;
|
||||
return `${prefix}${value < 10 ? value.toFixed(2) : value.toFixed(1)} Wh`;
|
||||
}
|
||||
|
||||
function sourceLabel(source) {
|
||||
return {
|
||||
measured: "车端实测",
|
||||
simulated: "模拟数据",
|
||||
simulated_estimated: "模拟 + 估算",
|
||||
measured_simulated: "实测 + 模拟",
|
||||
measured_simulated_estimated: "实测 + 模拟 + 估算",
|
||||
mixed: "实测 + 估算",
|
||||
estimated: "状态估算",
|
||||
}[source] || "状态估算";
|
||||
}
|
||||
|
||||
function sourceShortLabel(source) {
|
||||
return source === "measured" ? "实测" : source === "simulated" ? "模拟" : "估算";
|
||||
}
|
||||
|
||||
function totalPowerQualifier(source) {
|
||||
if (source === "measured") return "实测总功率";
|
||||
if (source === "simulated") return "模拟总功率";
|
||||
if (source === "estimated") return "估算总功率";
|
||||
return "混合来源总功率";
|
||||
}
|
||||
|
||||
function niceMaximum(value) {
|
||||
const safe = Math.max(1, value);
|
||||
const magnitude = 10 ** Math.floor(Math.log10(safe));
|
||||
const normalized = safe / magnitude;
|
||||
const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
|
||||
return nice * magnitude;
|
||||
}
|
||||
|
||||
function chartPoints(values, maxValue, width = 700, height = 238, padding = { left: 42, right: 14, top: 16, bottom: 28 }) {
|
||||
if (values.length < 2) return [];
|
||||
const innerWidth = width - padding.left - padding.right;
|
||||
const innerHeight = height - padding.top - padding.bottom;
|
||||
return values.map((value, index) => ({
|
||||
x: padding.left + innerWidth * index / (values.length - 1),
|
||||
y: padding.top + innerHeight * (1 - Math.max(0, value) / maxValue),
|
||||
}));
|
||||
}
|
||||
|
||||
function polylinePoints(values, maxValue, width, height, padding) {
|
||||
return chartPoints(values, maxValue, width, height, padding)
|
||||
.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function renderMainChart() {
|
||||
const values = monitor.frames.map((frame) => frameSeriesValue(frame, monitor.activeSeries));
|
||||
const maximum = niceMaximum(Math.max(...values, 1));
|
||||
const points = chartPoints(values, maximum);
|
||||
const line = $("#energyMonitorChartLine");
|
||||
const area = $("#energyMonitorChartArea");
|
||||
const svg = $("#energyMonitorChart");
|
||||
const baseline = 210;
|
||||
line?.setAttribute("points", points.map(({ x, y }) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" "));
|
||||
area?.setAttribute("d", points.length
|
||||
? `M ${points[0].x.toFixed(1)} ${baseline} L ${points.map(({ x, y }) => `${x.toFixed(1)} ${y.toFixed(1)}`).join(" L ")} L ${points.at(-1).x.toFixed(1)} ${baseline} Z`
|
||||
: "");
|
||||
|
||||
const selectedDevice = ENERGY_MONITORED_DEVICES.find(({ id }) => id === monitor.activeSeries);
|
||||
const selectedSeries = SERIES.find(({ id }) => id === monitor.activeSeries);
|
||||
const label = selectedDevice?.name || selectedSeries?.name || "总功率";
|
||||
const current = values.at(-1) || 0;
|
||||
const currentReading = selectedDevice ? monitor.latest?.readings?.[selectedDevice.id] : null;
|
||||
const estimated = currentReading ? currentReading.source !== "measured" : monitor.latest?.source !== "measured";
|
||||
$("#energyMonitorTrendName").textContent = label;
|
||||
$("#energyMonitorTrendCurrent").textContent = formatPower(current, estimated);
|
||||
$("#energyMonitorChartMax").textContent = formatPower(maximum);
|
||||
$("#energyMonitorChartMid").textContent = formatPower(maximum / 2);
|
||||
const spanSeconds = Math.max(1, Math.round(((monitor.frames.at(-1)?.timestamp || 0) - (monitor.frames[0]?.timestamp || 0)) / 1000));
|
||||
const windowSeconds = monitor.frames.length >= HISTORY_LIMIT ? 60 : Math.min(60, spanSeconds);
|
||||
$("#energyMonitorTrendWindow").textContent = `最近 ${windowSeconds} 秒`;
|
||||
$("#energyMonitorChartStart").textContent = `${windowSeconds} 秒前`;
|
||||
if (svg) svg.setAttribute("aria-label", `${label}最近 ${windowSeconds} 秒功率趋势,当前${formatPower(current, estimated)}`);
|
||||
|
||||
$$('[data-energy-series]').forEach((button) => {
|
||||
const selected = button.dataset.energySeries === monitor.activeSeries;
|
||||
button.classList.toggle("active", selected);
|
||||
button.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
$$('[data-energy-device]').forEach((button) => {
|
||||
const selected = button.dataset.energyDevice === monitor.activeSeries;
|
||||
button.classList.toggle("active", selected);
|
||||
button.setAttribute("aria-pressed", String(selected));
|
||||
});
|
||||
}
|
||||
|
||||
function renderDeviceRows() {
|
||||
if (!monitor.latest) return;
|
||||
const total = Math.max(monitor.latest.total, 0.0001);
|
||||
ENERGY_MONITORED_DEVICES.forEach((device) => {
|
||||
const row = $(`[data-energy-device="${device.id}"]`);
|
||||
const reading = monitor.latest.readings[device.id];
|
||||
if (!row || !reading) return;
|
||||
const estimated = reading.source !== "measured";
|
||||
$("[data-energy-device-power]", row).textContent = formatPower(reading.watts, estimated);
|
||||
const hasReportedEnergy = reading.reportedEnergyWh !== null && reading.reportedEnergyWh !== undefined;
|
||||
const displayedEnergyWh = hasReportedEnergy ? reading.reportedEnergyWh : monitor.energyWh[device.id];
|
||||
const reportedEnergyEstimated = !hasReportedEnergy || reading.reportedEnergySource !== "measured";
|
||||
$("[data-energy-device-energy]", row).textContent = formatEnergy(displayedEnergyWh, reportedEnergyEstimated);
|
||||
$("[data-energy-device-energy]", row).title = hasReportedEnergy
|
||||
? `${reading.reportedEnergyLabel || "车端累计"}能耗`
|
||||
: "本次监测功率积分估算";
|
||||
$("[data-energy-device-source]", row).textContent = sourceShortLabel(reading.source);
|
||||
$("[data-energy-device-source]", row).dataset.source = reading.source;
|
||||
$("[data-energy-device-activity]", row).textContent = reading.activity;
|
||||
const share = Math.max(0, Math.min(100, reading.watts / total * 100));
|
||||
$("[data-energy-device-share]", row).textContent = `${share.toFixed(1)}%`;
|
||||
row.style.setProperty("--energy-share", `${share}%`);
|
||||
const values = monitor.frames.map((frame) => frame.values[device.id] || 0);
|
||||
const maximum = Math.max(...values, 1);
|
||||
$("[data-energy-device-sparkline]", row)?.setAttribute(
|
||||
"points",
|
||||
polylinePoints(values, maximum, 92, 28, { left: 1, right: 1, top: 3, bottom: 3 }),
|
||||
);
|
||||
const energyScope = hasReportedEnergy ? reading.reportedEnergyLabel || "车端累计" : "本次监测估算";
|
||||
row.setAttribute("aria-label", `${device.name},当前${formatPower(reading.watts, estimated)},${energyScope}${formatEnergy(displayedEnergyWh, reportedEnergyEstimated)},点击查看趋势`);
|
||||
});
|
||||
}
|
||||
|
||||
function renderMonitor() {
|
||||
if (!monitor.latest) return;
|
||||
const totalParts = formatPowerParts(monitor.latest.total);
|
||||
const estimated = monitor.latest.source !== "measured";
|
||||
$("#energyMonitorTotalPower").textContent = totalParts.value;
|
||||
$("#energyMonitorTotalUnit").textContent = totalParts.unit;
|
||||
$("#energyMonitorTotalQualifier").textContent = totalPowerQualifier(monitor.latest.source);
|
||||
$("#energyMonitorAccumulated").textContent = formatEnergy(
|
||||
Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0),
|
||||
true,
|
||||
);
|
||||
const elapsedHours = Math.max(1 / 3600, (monitor.latest.timestamp - monitor.startedAt) / 3600000);
|
||||
$("#energyMonitorAverage").textContent = formatPower(
|
||||
Object.values(monitor.energyWh).reduce((sum, value) => sum + value, 0) / elapsedHours,
|
||||
true,
|
||||
);
|
||||
$("#energyMonitorCoverage").textContent = `${ENERGY_MONITORED_DEVICES.length} / ${ENERGY_MONITORED_DEVICES.length}`;
|
||||
$("#energyMonitorSource").textContent = sourceLabel(monitor.latest.source);
|
||||
$("#energyMonitorSource").dataset.source = monitor.latest.source;
|
||||
const sourceBreakdown = [
|
||||
monitor.latest.measuredCount ? `${monitor.latest.measuredCount} 路实测` : "",
|
||||
monitor.latest.simulatedCount ? `${monitor.latest.simulatedCount} 路模拟` : "",
|
||||
monitor.latest.estimatedCount ? `${monitor.latest.estimatedCount} 路估算` : "",
|
||||
].filter(Boolean).join(" · ");
|
||||
$("#energyMonitorCoverageNote").textContent = sourceBreakdown || "等待设备采样";
|
||||
$("#energyMonitorLiveLabelText").textContent = monitor.latest.measuredCount
|
||||
? "实时监测"
|
||||
: monitor.latest.simulatedCount ? "动态模拟" : "状态估算";
|
||||
$("#energyMonitorFreshness").textContent = `采样更新 ${new Date(monitor.latest.timestamp).toLocaleTimeString("zh-CN", { hour12: false })}`;
|
||||
$("#energyMonitorPage").dataset.revision = String(Number($("#energyMonitorPage").dataset.revision || 0) + 1);
|
||||
$("#energyMonitorPage").dataset.sampleCount = String(monitor.frames.length);
|
||||
renderMainChart();
|
||||
renderDeviceRows();
|
||||
}
|
||||
|
||||
function deviceRowsMarkup() {
|
||||
return ENERGY_MONITORED_DEVICES.map((device) => `
|
||||
<button class="energy-device-row" type="button" data-energy-device="${device.id}" aria-pressed="false">
|
||||
<span class="energy-device-icon"><i data-lucide="${device.icon}"></i></span>
|
||||
<span class="energy-device-identity"><strong>${device.name}</strong><small><b data-energy-device-source data-source="estimated">估算</b><i></i><span data-energy-device-activity>等待采样</span></small></span>
|
||||
<span class="energy-device-metrics"><strong data-energy-device-power>-- W</strong><small data-energy-device-energy>-- Wh</small></span>
|
||||
<svg class="energy-device-sparkline" viewBox="0 0 92 28" preserveAspectRatio="none" aria-hidden="true"><polyline data-energy-device-sparkline points=""></polyline></svg>
|
||||
<span class="energy-device-share"><b data-energy-device-share>0%</b><i><em></em></i></span>
|
||||
</button>`).join("");
|
||||
}
|
||||
|
||||
function createLayer() {
|
||||
const trigger = document.createElement("button");
|
||||
trigger.id = "energyMonitorToggle";
|
||||
trigger.className = "energy-monitor-toggle v2-panel-control-button";
|
||||
trigger.type = "button";
|
||||
trigger.setAttribute("aria-controls", "energyMonitorLayer");
|
||||
trigger.setAttribute("aria-pressed", "false");
|
||||
trigger.setAttribute("aria-expanded", "false");
|
||||
trigger.setAttribute("aria-label", "打开整车能耗监测");
|
||||
trigger.title = "打开整车能耗监测";
|
||||
trigger.innerHTML = '<i data-lucide="activity"></i><span class="visually-hidden">整车能耗监测</span><b class="energy-monitor-toggle-live" aria-hidden="true"></b>';
|
||||
|
||||
const layer = document.createElement("div");
|
||||
layer.id = "energyMonitorLayer";
|
||||
layer.className = "energy-monitor-layer";
|
||||
layer.hidden = true;
|
||||
layer.inert = true;
|
||||
layer.setAttribute("role", "dialog");
|
||||
layer.setAttribute("aria-modal", "true");
|
||||
layer.setAttribute("aria-labelledby", "energyMonitorTitle");
|
||||
layer.innerHTML = `
|
||||
<section class="energy-monitor-page" id="energyMonitorPage" data-revision="0" data-sample-count="0">
|
||||
<header class="energy-monitor-header">
|
||||
<div class="energy-monitor-heading">
|
||||
<span><i data-lucide="gauge"></i></span>
|
||||
<div><h2 id="energyMonitorTitle">整车能耗监测</h2><p>实时追踪各可控车端设备功率与累计能耗</p></div>
|
||||
</div>
|
||||
<div class="energy-monitor-live-state">
|
||||
<span class="energy-monitor-live-label"><i></i><b id="energyMonitorLiveLabelText">状态估算</b></span>
|
||||
<span id="energyMonitorFreshness">最后更新 --:--:--</span>
|
||||
<span class="energy-monitor-source" id="energyMonitorSource" data-source="estimated">状态估算</span>
|
||||
<button id="energyMonitorClose" type="button" aria-label="关闭整车能耗监测" title="关闭"><i data-lucide="x"></i></button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="energy-monitor-content">
|
||||
<section class="energy-monitor-kpis" aria-label="整车能耗摘要">
|
||||
<article class="energy-kpi energy-kpi-primary"><small>实时总功率</small><strong><b id="energyMonitorTotalPower">--</b><span id="energyMonitorTotalUnit">W</span></strong><em id="energyMonitorTotalQualifier">等待采样</em><i data-lucide="zap"></i></article>
|
||||
<article class="energy-kpi"><small>本次监测累计能耗</small><strong id="energyMonitorAccumulated">-- Wh</strong><em>按实时功率积分</em></article>
|
||||
<article class="energy-kpi"><small>本次监测平均功率</small><strong id="energyMonitorAverage">-- W</strong><em>自页面加载开始</em></article>
|
||||
<article class="energy-kpi"><small>监测设备</small><strong id="energyMonitorCoverage">7 / 7</strong><em id="energyMonitorCoverageNote">等待设备采样</em></article>
|
||||
</section>
|
||||
<div class="energy-monitor-dashboard">
|
||||
<section class="energy-trend-card" aria-labelledby="energyTrendTitle">
|
||||
<header>
|
||||
<div><h3 id="energyTrendTitle">功率趋势</h3><p><span id="energyMonitorTrendName">总功率</span> · <span id="energyMonitorTrendWindow">最近 24 秒</span></p></div>
|
||||
<strong id="energyMonitorTrendCurrent">-- W</strong>
|
||||
</header>
|
||||
<div class="energy-trend-tabs" role="group" aria-label="选择功率趋势">
|
||||
${SERIES.map((series) => `<button type="button" data-energy-series="${series.id}" aria-pressed="${String(series.id === "total")}" class="${series.id === "total" ? "active" : ""}">${series.name}</button>`).join("")}
|
||||
</div>
|
||||
<div class="energy-chart-wrap">
|
||||
<span class="energy-chart-y energy-chart-y-top" id="energyMonitorChartMax">-- W</span>
|
||||
<span class="energy-chart-y energy-chart-y-mid" id="energyMonitorChartMid">-- W</span>
|
||||
<span class="energy-chart-y energy-chart-y-bottom">0</span>
|
||||
<svg id="energyMonitorChart" class="energy-monitor-chart" viewBox="0 0 700 238" preserveAspectRatio="none" role="img" aria-label="功率趋势">
|
||||
<defs><linearGradient id="energyMonitorAreaGradient" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="currentColor" stop-opacity=".22"></stop><stop offset="1" stop-color="currentColor" stop-opacity=".015"></stop></linearGradient></defs>
|
||||
<g class="energy-chart-grid"><line x1="42" y1="16" x2="686" y2="16"></line><line x1="42" y1="113" x2="686" y2="113"></line><line x1="42" y1="210" x2="686" y2="210"></line></g>
|
||||
<path id="energyMonitorChartArea" class="energy-chart-area" d=""></path>
|
||||
<polyline id="energyMonitorChartLine" class="energy-chart-line" points=""></polyline>
|
||||
</svg>
|
||||
<div class="energy-chart-x"><span id="energyMonitorChartStart">60 秒前</span><span>现在</span></div>
|
||||
</div>
|
||||
<footer><span><i></i>当前选中曲线</span><p>点击右侧设备可查看其独立趋势</p></footer>
|
||||
</section>
|
||||
<section class="energy-devices-card" aria-labelledby="energyDevicesTitle">
|
||||
<header><div><h3 id="energyDevicesTitle">设备实时功率</h3><p>功率 · 累计能耗 · 占比</p></div><span>${ENERGY_MONITORED_DEVICES.length} 个可控设备</span></header>
|
||||
<div class="energy-device-list" id="energyMonitorDeviceList">${deviceRowsMarkup()}</div>
|
||||
<footer><i data-lucide="info"></i><span>优先读取车端功率通道;未接入独立电表的设备显示明确标注的状态估算值。</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>`;
|
||||
|
||||
$("#v2PanelControlDock").append(trigger);
|
||||
document.body.append(layer);
|
||||
trigger.addEventListener("click", () => setOpen(!monitor.open));
|
||||
$("#energyMonitorClose", layer).addEventListener("click", () => setOpen(false));
|
||||
layer.addEventListener("click", (event) => {
|
||||
if (event.target === layer) setOpen(false);
|
||||
});
|
||||
layer.addEventListener("click", (event) => {
|
||||
const seriesButton = event.target.closest("[data-energy-series]");
|
||||
const deviceButton = event.target.closest("[data-energy-device]");
|
||||
if (seriesButton) selectSeries(seriesButton.dataset.energySeries);
|
||||
if (deviceButton) selectSeries(deviceButton.dataset.energyDevice);
|
||||
});
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
}
|
||||
|
||||
function focusableElements() {
|
||||
return $$('button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', $("#energyMonitorLayer"))
|
||||
.filter((element) => !element.hidden && element.offsetParent !== null);
|
||||
}
|
||||
|
||||
function setBackgroundInert(inert) {
|
||||
const layer = $("#energyMonitorLayer");
|
||||
if (!layer || !document.body) return;
|
||||
if (inert) {
|
||||
monitor.backgroundInert = [...document.body.children]
|
||||
.filter((element) => element !== layer && !["SCRIPT", "STYLE", "LINK"].includes(element.tagName))
|
||||
.map((element) => ({ element, wasInert: element.hasAttribute("inert") }));
|
||||
monitor.backgroundInert.forEach(({ element }) => { element.toggleAttribute("inert", true); });
|
||||
return;
|
||||
}
|
||||
monitor.backgroundInert.forEach(({ element, wasInert }) => { element.toggleAttribute("inert", wasInert); });
|
||||
monitor.backgroundInert = [];
|
||||
}
|
||||
|
||||
function captureModalKeys(event) {
|
||||
if (!monitor.open) return;
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
setOpen(false);
|
||||
return;
|
||||
}
|
||||
if (event.key !== "Tab") return;
|
||||
const focusable = focusableElements();
|
||||
if (!focusable.length) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable.at(-1);
|
||||
const layer = $("#energyMonitorLayer");
|
||||
if (!layer?.contains(document.activeElement)) {
|
||||
event.preventDefault();
|
||||
(event.shiftKey ? last : first).focus();
|
||||
} else if (event.shiftKey && document.activeElement === first) {
|
||||
event.preventDefault();
|
||||
last.focus();
|
||||
} else if (!event.shiftKey && document.activeElement === last) {
|
||||
event.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function setOpen(open) {
|
||||
const next = Boolean(open);
|
||||
const layer = $("#energyMonitorLayer");
|
||||
const trigger = $("#energyMonitorToggle");
|
||||
if (!layer || !trigger || monitor.open === next) return;
|
||||
monitor.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) {
|
||||
monitor.returnFocus = document.activeElement;
|
||||
layer.hidden = false;
|
||||
layer.inert = false;
|
||||
layer.dataset.open = "true";
|
||||
setBackgroundInert(true);
|
||||
renderMonitor();
|
||||
requestAnimationFrame(() => $("#energyMonitorClose", layer)?.focus());
|
||||
} else {
|
||||
layer.dataset.open = "false";
|
||||
layer.inert = true;
|
||||
layer.hidden = true;
|
||||
setBackgroundInert(false);
|
||||
const target = monitor.returnFocus instanceof HTMLElement && monitor.returnFocus.isConnected
|
||||
? monitor.returnFocus
|
||||
: trigger;
|
||||
monitor.returnFocus = null;
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function selectSeries(seriesId) {
|
||||
const valid = SERIES.some(({ id }) => id === seriesId)
|
||||
|| ENERGY_MONITORED_DEVICES.some(({ id }) => id === seriesId);
|
||||
if (!valid) return;
|
||||
monitor.activeSeries = seriesId;
|
||||
renderMainChart();
|
||||
}
|
||||
|
||||
function deviceIdsForControlDomain(domain, context = "") {
|
||||
const normalizedDomain = normalizeSource(domain).replace(/^native_/, "");
|
||||
const normalizedContext = normalizeSource(context).replace(/[^a-z0-9\u4e00-\u9fff]+/g, "_");
|
||||
if (normalizedDomain === "seat" || /(^|_)seat(_|$)/.test(normalizedContext)) {
|
||||
if (/(^|_)(driver|front_left|left_front)(_|$)|主驾/.test(normalizedContext)) return ["driver"];
|
||||
if (/(^|_)(passenger|front_right|right_front)(_|$)|副驾/.test(normalizedContext)) return ["passenger"];
|
||||
return ["passenger"];
|
||||
}
|
||||
if (["window", "windows"].includes(normalizedDomain) || /(^|_)window(s)?(_|$)/.test(normalizedContext)) return ["windows"];
|
||||
if (["roof", "shade", "sunshade"].includes(normalizedDomain) || /(^|_)(roof|shade|sunshade)(_|$)/.test(normalizedContext)) return ["shade"];
|
||||
if (["climate", "hvac", "outlet"].includes(normalizedDomain) || /(^|_)(climate|hvac|outlet|ac)(_|$)/.test(normalizedContext)) return ["climate"];
|
||||
if (["interior_light", "reading", "reading_light"].includes(normalizedDomain) || /(^|_)(interior|reading)_?light(_|$)/.test(normalizedContext)) return ["reading"];
|
||||
if (["ambient", "ambient_light"].includes(normalizedDomain) || /(^|_)ambient_?light(_|$)/.test(normalizedContext)) return ["ambient"];
|
||||
return [];
|
||||
}
|
||||
|
||||
function controlActivityDevices(payload = {}) {
|
||||
const wrappedResult = payload?.result && typeof payload.result === "object";
|
||||
const eventPayload = wrappedResult ? payload.result : payload;
|
||||
const status = String(eventPayload?.status || "");
|
||||
if (payload?.error) return [];
|
||||
if (wrappedResult && !["succeeded", "partially_succeeded"].includes(status)) return [];
|
||||
if (!wrappedResult && status && !["processing", "succeeded", "partially_succeeded", "submitted"].includes(status)) return [];
|
||||
if (eventPayload?.preview_only === true || /_preview$/.test(String(eventPayload?.orchestration_route || ""))) {
|
||||
return [];
|
||||
}
|
||||
const deviceIds = new Set();
|
||||
const directId = aliasToDevice.get(normalizeSource(eventPayload?.device_id))
|
||||
|| normalizeSource(eventPayload?.device_id);
|
||||
if (ENERGY_MONITORED_DEVICES.some(({ id }) => id === directId)) deviceIds.add(directId);
|
||||
|
||||
const actions = [
|
||||
...(Array.isArray(eventPayload?.actions) ? eventPayload.actions : []),
|
||||
...(Array.isArray(eventPayload?.validated_actions) ? eventPayload.validated_actions : []),
|
||||
...(Array.isArray(eventPayload?.intent_ir?.spec?.actions) ? eventPayload.intent_ir.spec.actions : []),
|
||||
];
|
||||
actions.forEach((action) => {
|
||||
const capabilityDomain = String(action?.capability || "").split(".")[0];
|
||||
const context = JSON.stringify({
|
||||
capability: action?.capability,
|
||||
target: action?.target,
|
||||
});
|
||||
deviceIdsForControlDomain(action?.domain || capabilityDomain, context).forEach((id) => deviceIds.add(id));
|
||||
});
|
||||
|
||||
const functionConfigs = Object.entries(eventPayload?.function_configs || {});
|
||||
if (eventPayload?.function_config && typeof eventPayload.function_config === "object") {
|
||||
functionConfigs.push([eventPayload.function_config?.spec?.domain || "function", eventPayload.function_config]);
|
||||
}
|
||||
functionConfigs.forEach(([configKey, config]) => {
|
||||
const domain = config?.spec?.domain || config?.domain || configKey;
|
||||
const operations = config?.spec?.operations || config?.operations || [];
|
||||
if (!operations.length) {
|
||||
deviceIdsForControlDomain(domain, configKey).forEach((id) => deviceIds.add(id));
|
||||
return;
|
||||
}
|
||||
operations.forEach((operation) => {
|
||||
const context = JSON.stringify({
|
||||
capability: operation?.capability,
|
||||
target: operation?.target,
|
||||
});
|
||||
deviceIdsForControlDomain(operation?.domain || domain, context).forEach((id) => deviceIds.add(id));
|
||||
});
|
||||
});
|
||||
|
||||
Object.entries(eventPayload?.command_plans || {}).forEach(([planKey, plan]) => {
|
||||
const domain = plan?.spec?.domain || plan?.domain || planKey;
|
||||
const commands = plan?.spec?.commands || plan?.commands || [];
|
||||
const context = commands.length ? JSON.stringify(commands) : planKey;
|
||||
deviceIdsForControlDomain(domain, context).forEach((id) => deviceIds.add(id));
|
||||
});
|
||||
return [...deviceIds];
|
||||
}
|
||||
|
||||
function markControlActivity(payload) {
|
||||
const until = Date.now() + 4000;
|
||||
const requestId = controlRequestId(payload);
|
||||
const currentDeviceIds = controlActivityDevices(payload);
|
||||
const existing = requestId ? monitor.controlRequests.get(requestId) : null;
|
||||
const deviceIds = [...new Set([...(existing?.deviceIds || []), ...currentDeviceIds])];
|
||||
deviceIds.forEach((deviceId) => {
|
||||
monitor.motionUntil.set(deviceId, Math.max(monitor.motionUntil.get(deviceId) || 0, until));
|
||||
});
|
||||
if (requestId && deviceIds.length) {
|
||||
monitor.controlRequests.set(requestId, { deviceIds, until });
|
||||
}
|
||||
return currentDeviceIds;
|
||||
}
|
||||
|
||||
function controlRequestId(payload = {}) {
|
||||
const eventPayload = payload?.result && typeof payload.result === "object"
|
||||
? payload.result
|
||||
: payload;
|
||||
return String(eventPayload?.request_id || payload?.request_id || "").trim();
|
||||
}
|
||||
|
||||
function completeControlActivity(payload = {}) {
|
||||
const eventPayload = payload?.result && typeof payload.result === "object"
|
||||
? payload.result
|
||||
: payload;
|
||||
const requestId = controlRequestId(payload);
|
||||
const tracked = requestId ? monitor.controlRequests.get(requestId) : null;
|
||||
const succeeded = !payload?.error
|
||||
&& ["succeeded", "partially_succeeded"].includes(String(eventPayload?.status || ""));
|
||||
|
||||
if (tracked) {
|
||||
monitor.controlRequests.delete(requestId);
|
||||
if (!succeeded) {
|
||||
const now = Date.now();
|
||||
tracked.deviceIds.forEach((deviceId) => {
|
||||
if ((monitor.motionUntil.get(deviceId) || 0) > tracked.until) return;
|
||||
const remainingUntil = [...monitor.controlRequests.values()]
|
||||
.filter((request) => request.deviceIds.includes(deviceId))
|
||||
.reduce((maximum, request) => Math.max(maximum, request.until), now);
|
||||
monitor.motionUntil.set(deviceId, remainingUntil);
|
||||
});
|
||||
}
|
||||
return tracked.deviceIds;
|
||||
}
|
||||
return succeeded ? markControlActivity(payload) : [];
|
||||
}
|
||||
|
||||
function seedHistory() {
|
||||
const now = Date.now();
|
||||
for (let offset = 23; offset >= 1; offset -= 1) {
|
||||
sampleNow({ now: now - offset * SAMPLE_INTERVAL_MS, integrate: false, render: false });
|
||||
}
|
||||
monitor.lastSampleAt = now - SAMPLE_INTERVAL_MS;
|
||||
sampleNow({ now, integrate: false, render: false });
|
||||
monitor.startedAt = now;
|
||||
}
|
||||
|
||||
function mount(attempt = 0) {
|
||||
if (monitor.mounted) return;
|
||||
if (!$("#v2PanelControlDock")) {
|
||||
if (attempt < 20) window.setTimeout(() => mount(attempt + 1), 50);
|
||||
return;
|
||||
}
|
||||
monitor.mounted = true;
|
||||
createLayer();
|
||||
seedHistory();
|
||||
renderMonitor();
|
||||
monitor.timer = window.setInterval(
|
||||
() => sampleNow({ render: monitor.open }),
|
||||
SAMPLE_INTERVAL_MS,
|
||||
);
|
||||
document.addEventListener("keydown", captureModalKeys, true);
|
||||
window.addEventListener("cognitive-agent:power-telemetry", (event) => ingestPowerTelemetry(event.detail || {}, { sample: false }));
|
||||
const existingUpdater = window.updateDevicePowerTelemetry;
|
||||
if (typeof existingUpdater === "function" && !existingUpdater.energyMonitorLinked) {
|
||||
const linkedUpdater = (payload) => {
|
||||
const result = existingUpdater(payload);
|
||||
ingestPowerTelemetry(payload, { sample: false });
|
||||
return result;
|
||||
};
|
||||
linkedUpdater.energyMonitorLinked = true;
|
||||
window.updateDevicePowerTelemetry = linkedUpdater;
|
||||
}
|
||||
window.addEventListener("cognitive-agent:vehicle-can-control-request", (event) => markControlActivity(event.detail || {}));
|
||||
window.addEventListener("cognitive-agent:validated-can-commands", (event) => markControlActivity(event.detail || {}));
|
||||
window.addEventListener("cognitive-agent:vehicle-control-activity", (event) => markControlActivity(event.detail || {}));
|
||||
window.addEventListener("cognitive-agent:request-complete", (event) => completeControlActivity(event.detail || {}));
|
||||
}
|
||||
|
||||
window.CognitiveAgentEnergyMonitor = {
|
||||
close: () => setOpen(false),
|
||||
getDevices: () => ENERGY_MONITORED_DEVICES.map(({ id, name, telemetryKeys }) => ({ id, name, telemetryKeys: [...telemetryKeys] })),
|
||||
getSnapshot: publicSnapshot,
|
||||
ingestTelemetry: ingestPowerTelemetry,
|
||||
isOpen: () => monitor.open,
|
||||
completeControlActivity,
|
||||
notifyControlActivity: markControlActivity,
|
||||
open: () => setOpen(true),
|
||||
sampleNow,
|
||||
selectSeries,
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => mount());
|
||||
}());
|
||||
@@ -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,
|
||||
});
|
||||
})();
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """)
|
||||
.replaceAll("'", "'");
|
||||
}
|
||||
|
||||
async function api(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json", ...(options.headers || {}) },
|
||||
...options,
|
||||
});
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
const payload = contentType.includes("application/json") ? await response.json() : null;
|
||||
if (!response.ok) throw new Error(payload?.message || `请求失败(${response.status})`);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function selectedTask() {
|
||||
return model.snapshot.tasks.find((task) => task.id === model.selectedTaskId)
|
||||
|| model.snapshot.tasks[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
function baselineCapabilities() {
|
||||
const baseline = model.snapshot.baselines?.find((item) => item.id === ui.baseline.value);
|
||||
return baseline?.capabilities || FALLBACK_BASELINES[ui.baseline.value] || FALLBACK_BASELINES.time_only;
|
||||
}
|
||||
|
||||
function parseInstructionCapabilities(instruction) {
|
||||
const lowered = String(instruction || "").toLowerCase();
|
||||
const aliases = {
|
||||
"时间控制": ["时间", "定时", "timed"],
|
||||
"位置控制": ["位置", "position"],
|
||||
"座椅通风": ["通风", "vent"],
|
||||
"座椅加热": ["加热", "heater", "heat"],
|
||||
};
|
||||
return CAPABILITY_ORDER.filter((capability) => aliases[capability].some((alias) => lowered.includes(alias)));
|
||||
}
|
||||
|
||||
function updateCapabilityPreview(task = null) {
|
||||
const baseline = baselineCapabilities();
|
||||
const taskMatchesDraft = task
|
||||
&& task.instruction === ui.instruction.value.trim()
|
||||
&& (task.baseline_id || "time_only") === ui.baseline.value;
|
||||
const target = taskMatchesDraft
|
||||
? task.target_capabilities
|
||||
: CAPABILITY_ORDER.filter((item) => baseline.includes(item) || parseInstructionCapabilities(ui.instruction.value).includes(item));
|
||||
ui.baselineCapabilities.textContent = baseline.join("+");
|
||||
ui.targetCapabilities.textContent = (target?.length ? target : baseline).join("+");
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return "—";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||
hour12: false,
|
||||
}).format(date).replaceAll("/", "-");
|
||||
}
|
||||
|
||||
function formatClock(value) {
|
||||
if (!value) return "--:--:--";
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "--:--:--";
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
function formatDuration(milliseconds) {
|
||||
if (milliseconds === null || milliseconds === undefined) return "—";
|
||||
if (milliseconds < 1000) return `${milliseconds} 毫秒`;
|
||||
const seconds = milliseconds / 1000;
|
||||
return seconds < 60 ? `${seconds.toFixed(seconds < 10 ? 1 : 0)} 秒` : `${Math.floor(seconds / 60)} 分 ${Math.round(seconds % 60)} 秒`;
|
||||
}
|
||||
|
||||
function formatSize(bytes) {
|
||||
const value = Number(bytes || 0);
|
||||
if (value < 1024) return `${value} 字节`;
|
||||
if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} 千字节`;
|
||||
return `${(value / 1024 / 1024).toFixed(2)} 兆字节`;
|
||||
}
|
||||
|
||||
function toast(message, type = "info") {
|
||||
const element = document.createElement("div");
|
||||
element.className = `toast ${type}`;
|
||||
element.textContent = message;
|
||||
ui.toasts.append(element);
|
||||
window.setTimeout(() => element.remove(), 4200);
|
||||
}
|
||||
|
||||
function refreshIcons() {
|
||||
if (window.lucide) window.lucide.createIcons({ attrs: { "stroke-width": 1.8 } });
|
||||
}
|
||||
|
||||
function renderPipeline(task) {
|
||||
const stages = task?.stages || model.snapshot.pipeline?.map((stage) => ({ ...stage, status: "waiting" })) || FALLBACK_PIPELINE;
|
||||
ui.pipeline.innerHTML = stages.map((stage) => `
|
||||
<li class="pipeline-stage" data-status="${escapeHtml(stage.status || "waiting")}" title="${escapeHtml(stage.summary || "")}">
|
||||
<span class="stage-node"><i data-lucide="${STAGE_ICONS[stage.id] || "circle"}"></i></span>
|
||||
<strong class="stage-name">${escapeHtml(stage.name)}</strong>
|
||||
<span class="stage-state">${escapeHtml(STATUS_LABELS[stage.status] || stage.status || "等待")}${stage.duration_ms !== null && stage.duration_ms !== undefined ? ` · ${escapeHtml(formatDuration(stage.duration_ms))}` : ""}</span>
|
||||
</li>`).join("");
|
||||
}
|
||||
|
||||
function renderRelease(task) {
|
||||
let icon = "cloud-upload";
|
||||
let label = "尚未开始";
|
||||
let state = "idle";
|
||||
if (task) {
|
||||
label = task.message || STATUS_LABELS[task.status] || task.status;
|
||||
state = task.status;
|
||||
if (task.status === "success") { icon = "circle-check-big"; state = "success"; }
|
||||
else if (task.status === "running") icon = "loader-circle";
|
||||
else if (task.status === "failed") icon = "circle-x";
|
||||
else if (task.status === "canceled") icon = "ban";
|
||||
}
|
||||
ui.release.dataset.state = state;
|
||||
ui.release.innerHTML = `<span class="result-icon"><i data-lucide="${icon}"></i></span><div><small>当前交付状态</small><strong>${escapeHtml(label)}</strong></div>`;
|
||||
}
|
||||
|
||||
function renderLogs(task) {
|
||||
const logs = task?.logs || [];
|
||||
if (!logs.length) {
|
||||
ui.logs.innerHTML = `<div class="empty-state"><i data-lucide="terminal-square"></i><strong>暂无执行记录</strong><span>创建开发任务后,这里将实时显示各阶段输出。</span></div>`;
|
||||
model.lastLogCount = 0;
|
||||
return;
|
||||
}
|
||||
const nearBottom = ui.logs.scrollHeight - ui.logs.scrollTop - ui.logs.clientHeight < 80;
|
||||
ui.logs.innerHTML = logs.map((entry) => `
|
||||
<article class="log-entry" data-level="${escapeHtml(entry.level)}">
|
||||
<time class="log-time">${escapeHtml(formatClock(entry.time))}</time>
|
||||
<span class="log-dot"></span>
|
||||
<div class="log-copy"><strong>${escapeHtml(entry.title)}</strong><p>${escapeHtml(entry.message)}</p></div>
|
||||
</article>`).join("");
|
||||
if (nearBottom || logs.length > model.lastLogCount) ui.logs.scrollTop = ui.logs.scrollHeight;
|
||||
model.lastLogCount = logs.length;
|
||||
}
|
||||
|
||||
function renderInspector(task) {
|
||||
const status = task?.status || "waiting";
|
||||
ui.status.dataset.status = status;
|
||||
ui.status.textContent = STATUS_LABELS[status] || status;
|
||||
const fileCount = task?.file_changes?.length || 0;
|
||||
ui.metrics.innerHTML = `
|
||||
<div><dt>开始时间</dt><dd>${escapeHtml(formatDate(task?.started_at))}</dd></div>
|
||||
<div><dt>结束时间</dt><dd>${escapeHtml(formatDate(task?.finished_at))}</dd></div>
|
||||
<div><dt>实际耗时</dt><dd>${escapeHtml(formatDuration(task?.duration_ms))}</dd></div>
|
||||
<div><dt>文件变更</dt><dd>${fileCount ? `${fileCount} 个文件` : "—"}</dd></div>`;
|
||||
document.querySelectorAll("[data-inspector-tab]").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.inspectorTab === model.inspectorTab);
|
||||
});
|
||||
if (!task) {
|
||||
ui.inspector.innerHTML = `<div class="empty-inline">暂无任务数据</div>`;
|
||||
return;
|
||||
}
|
||||
if (model.inspectorTab === "build") {
|
||||
ui.inspector.innerHTML = task.build_output?.length
|
||||
? `<pre class="build-output">${escapeHtml(task.build_output.join("\n"))}</pre>`
|
||||
: `<div class="empty-inline">尚无编译输出</div>`;
|
||||
return;
|
||||
}
|
||||
if (model.inspectorTab === "tests") {
|
||||
ui.inspector.innerHTML = task.test_results?.length
|
||||
? task.test_results.map((test) => `<div class="test-item"><span class="test-mark"><i data-lucide="check"></i></span><div class="test-copy"><strong>${escapeHtml(test.name)}</strong><span>${escapeHtml(test.summary)}</span></div></div>`).join("")
|
||||
: `<div class="empty-inline">测试尚未执行</div>`;
|
||||
return;
|
||||
}
|
||||
ui.inspector.innerHTML = task.file_changes?.length
|
||||
? task.file_changes.map((change) => `<div class="change-item"><span class="change-type">${change.type === "added" ? "新增" : "修改"}</span><div class="change-copy"><strong>${escapeHtml(change.path)}</strong><span>${escapeHtml(change.summary)}</span></div></div>`).join("")
|
||||
: `<div class="empty-inline">尚无文件变更</div>`;
|
||||
}
|
||||
|
||||
function renderArtifacts() {
|
||||
const artifacts = model.snapshot.artifacts || [];
|
||||
if (!artifacts.length) {
|
||||
ui.artifacts.innerHTML = `<tr><td colspan="6"><div class="empty-table">尚未生成固件产物</div></td></tr>`;
|
||||
return;
|
||||
}
|
||||
ui.artifacts.innerHTML = artifacts.map((artifact) => `
|
||||
<tr>
|
||||
<td><div class="software-name"><strong>${escapeHtml(artifact.software_name)}</strong><span>${escapeHtml(artifact.version)}</span></div></td>
|
||||
<td><div class="cap-tags">${(artifact.capabilities || []).map((capability) => `<span>${escapeHtml(capability)}</span>`).join("")}</div></td>
|
||||
<td>${escapeHtml(formatDate(artifact.build_time))}</td>
|
||||
<td><div class="file-cell"><strong>${escapeHtml(artifact.firmware_filename)}</strong><span>${escapeHtml(formatSize(artifact.size_bytes))} · <span class="hash-value" title="${escapeHtml(artifact.sha256)}">${escapeHtml(artifact.sha256)}</span></span></div></td>
|
||||
<td><span class="status-badge" data-status="${escapeHtml(artifact.status)}">${escapeHtml(STATUS_LABELS[artifact.status] || artifact.status)}</span></td>
|
||||
<td><div class="table-actions">
|
||||
<button type="button" title="查看详情" data-artifact-action="detail" data-artifact-id="${artifact.id}">详情</button>
|
||||
<button type="button" title="查看能力差异" data-artifact-action="capabilities" data-artifact-id="${artifact.id}">能力</button>
|
||||
<button type="button" title="查看代码差异" data-artifact-action="code" data-artifact-id="${artifact.id}">代码</button>
|
||||
<button type="button" title="查看验证记录" data-artifact-action="validation" data-artifact-id="${artifact.id}">验证</button>
|
||||
<a href="${escapeHtml(artifact.download_url)}" title="下载固件" aria-label="下载固件" download><i data-lucide="download"></i></a>
|
||||
<button class="publish-action" type="button" title="发布到服务器" data-artifact-action="publish" data-artifact-id="${artifact.id}"><i data-lucide="cloud-upload"></i></button>
|
||||
${artifact.is_baseline ? `<span class="baseline-protection" title="时间控制基线固件受保护"><i data-lucide="shield-check"></i>受保护</span>` : `<button class="delete-action" type="button" title="删除固件产物" data-artifact-action="delete" data-artifact-id="${artifact.id}"><i data-lucide="trash-2"></i></button>`}
|
||||
${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();
|
||||
})();
|
||||
@@ -1,6 +1,6 @@
|
||||
const VISION_VERSION = "0.10.14";
|
||||
const VISION_MODULE = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/vision_bundle.mjs`;
|
||||
const VISION_WASM = `https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@${VISION_VERSION}/wasm`;
|
||||
const VISION_MODULE = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/vision_bundle.mjs`;
|
||||
const VISION_WASM = `/vendor/mediapipe/tasks-vision/${VISION_VERSION}/wasm`;
|
||||
|
||||
const CONNECTIONS = [
|
||||
[0, 1], [1, 2], [2, 3], [3, 4],
|
||||
@@ -55,6 +55,12 @@ const state = {
|
||||
customMapping: loadCustomMapping(),
|
||||
customizeOpen: false,
|
||||
engine: null,
|
||||
runtimeReady: false,
|
||||
enabled: true,
|
||||
disabledReason: "",
|
||||
cameraStatus: "checking",
|
||||
cameraMessage: "正在检测摄像头…",
|
||||
cameraCheckSequence: 0,
|
||||
};
|
||||
|
||||
const $ = (selector) => document.querySelector(selector);
|
||||
@@ -80,6 +86,92 @@ function notify(message) {
|
||||
window.setTimeout(() => toast.classList.remove("visible"), 2600);
|
||||
}
|
||||
|
||||
function cameraFailureDetails(error) {
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
return window.isSecureContext === false
|
||||
? { status: "unsupported", message: "当前页面不是安全连接,浏览器已阻止摄像头访问" }
|
||||
: { status: "unsupported", message: "当前浏览器不支持摄像头访问" };
|
||||
}
|
||||
switch (error?.name) {
|
||||
case "NotFoundError":
|
||||
case "DevicesNotFoundError":
|
||||
return { status: "unavailable", message: "未检测到可用摄像头" };
|
||||
case "NotAllowedError":
|
||||
case "PermissionDeniedError":
|
||||
case "SecurityError":
|
||||
return { status: "blocked", message: "摄像头权限被拒绝,请在浏览器设置中允许访问" };
|
||||
case "NotReadableError":
|
||||
case "TrackStartError":
|
||||
return { status: "busy", message: "摄像头正被其他程序占用或暂时不可用" };
|
||||
case "OverconstrainedError":
|
||||
case "ConstraintNotSatisfiedError":
|
||||
return { status: "incompatible", message: "摄像头不支持当前画面参数" };
|
||||
case "AbortError":
|
||||
return { status: "unknown", message: "摄像头启动被中断,请重试" };
|
||||
default:
|
||||
return { status: "unknown", message: error?.message || "摄像头启动失败" };
|
||||
}
|
||||
}
|
||||
|
||||
function syncCameraButtonAvailability() {
|
||||
const button = $("#gestureStartButton");
|
||||
if (!button || state.stream) return;
|
||||
button.disabled = !state.enabled
|
||||
|| !state.runtimeReady
|
||||
|| ["checking", "unavailable", "unsupported"].includes(state.cameraStatus);
|
||||
}
|
||||
|
||||
function setCameraAvailability(status, message, { announce = true } = {}) {
|
||||
state.cameraStatus = status;
|
||||
state.cameraMessage = message;
|
||||
$("#gestureDrawer")?.setAttribute("data-camera-status", status);
|
||||
syncCameraButtonAvailability();
|
||||
if (announce && !state.stream) $("#gestureSafetyText").textContent = message;
|
||||
}
|
||||
|
||||
async function refreshCameraAvailability({ announce = true } = {}) {
|
||||
if (!navigator.mediaDevices?.enumerateDevices || !navigator.mediaDevices?.getUserMedia) {
|
||||
const failure = cameraFailureDetails();
|
||||
setCameraAvailability(failure.status, failure.message, { announce });
|
||||
return false;
|
||||
}
|
||||
|
||||
const checkSequence = ++state.cameraCheckSequence;
|
||||
if (!state.stream) setCameraAvailability("checking", "正在检测摄像头…", { announce });
|
||||
try {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
if (checkSequence !== state.cameraCheckSequence) return state.cameraStatus === "available";
|
||||
const available = devices.some((device) => device.kind === "videoinput");
|
||||
setCameraAvailability(
|
||||
available ? "available" : "unavailable",
|
||||
available ? "已检测到摄像头,点击开启开始识别" : "未检测到可用摄像头",
|
||||
{ announce },
|
||||
);
|
||||
return available;
|
||||
} catch (error) {
|
||||
if (checkSequence !== state.cameraCheckSequence) return state.cameraStatus === "available";
|
||||
const failure = cameraFailureDetails(error);
|
||||
const message = failure.status === "blocked"
|
||||
? "尚未获得摄像头权限,可点击开启并在浏览器中授权"
|
||||
: "暂时无法检测摄像头,可点击开启时再次确认";
|
||||
setCameraAvailability("unknown", message, { announce });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCameraDeviceChange() {
|
||||
const streamWasActive = Boolean(state.stream);
|
||||
const available = await refreshCameraAvailability({ announce: !streamWasActive });
|
||||
const streamStillLive = state.stream?.getVideoTracks().some((track) => track.readyState === "live");
|
||||
if (streamWasActive && (available === false || !streamStillLive)) {
|
||||
const message = available === false
|
||||
? "摄像头连接已断开,未检测到其他可用摄像头"
|
||||
: "当前摄像头连接已断开,请重新开启摄像头";
|
||||
stopCamera({ message });
|
||||
notify(message);
|
||||
}
|
||||
}
|
||||
|
||||
function distance(a, b) {
|
||||
return Math.hypot(a.x - b.x, a.y - b.y);
|
||||
}
|
||||
@@ -386,7 +478,6 @@ function updateEngineUI(engine, message) {
|
||||
pill.className = `gesture-state-pill ${engine.armed ? (engine.currentAction ? "moving" : "armed") : "idle"}`;
|
||||
pill.querySelector("b").textContent = engine.armed ? (engine.currentAction ? "执行中" : "已激活") : "已锁止";
|
||||
$("#gestureSafetyText").textContent = message;
|
||||
$("#gestureArmButton").textContent = engine.armed ? "已激活" : "激活控制";
|
||||
}
|
||||
|
||||
function executeGestureAction(action) {
|
||||
@@ -464,7 +555,7 @@ async function recognizeLoop() {
|
||||
}
|
||||
|
||||
async function loadGestureRuntime() {
|
||||
const response = await fetch("/api/gesture/runtime", { cache: "no-store" });
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/gesture/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error("无法读取手势能力配置");
|
||||
state.runtime = await response.json();
|
||||
state.engine = new GestureEngine(state.runtime.safety, executeGestureAction);
|
||||
@@ -475,9 +566,12 @@ async function loadGestureRuntime() {
|
||||
async function loadRecognizer() {
|
||||
if (state.recognizer) return;
|
||||
$("#gestureSafetyText").textContent = "正在加载手势模型…";
|
||||
const { FilesetResolver, GestureRecognizer } = await import(VISION_MODULE);
|
||||
const { FilesetResolver, GestureRecognizer } = await import(/* @vite-ignore */ VISION_MODULE);
|
||||
const fileset = await FilesetResolver.forVisionTasks(VISION_WASM);
|
||||
const modelResponse = await fetch("/api/gesture/model", { cache: "force-cache" });
|
||||
const modelResponse = await window.CognitiveAgentNetwork.request("/api/gesture/model", {
|
||||
cache: "force-cache",
|
||||
timeoutMs: 60000,
|
||||
});
|
||||
if (!modelResponse.ok) throw new Error("内置手势模型加载失败");
|
||||
const modelAssetBuffer = new Uint8Array(await modelResponse.arrayBuffer());
|
||||
state.recognizer = await GestureRecognizer.createFromOptions(fileset, {
|
||||
@@ -494,6 +588,8 @@ async function startCamera() {
|
||||
const button = $("#gestureStartButton");
|
||||
button.disabled = true;
|
||||
button.querySelector("span").textContent = "正在启动…";
|
||||
button.setAttribute("aria-label", "正在启动摄像头");
|
||||
button.setAttribute("title", "正在启动摄像头");
|
||||
try {
|
||||
if (!state.runtime) await loadGestureRuntime();
|
||||
await loadRecognizer();
|
||||
@@ -501,27 +597,30 @@ async function startCamera() {
|
||||
video: { width: { ideal: 1280 }, height: { ideal: 720 }, facingMode: "user" },
|
||||
audio: false,
|
||||
});
|
||||
setCameraAvailability("available", "摄像头可用,点击开启开始识别", { announce: false });
|
||||
const video = $("#gestureVideo");
|
||||
video.srcObject = state.stream;
|
||||
await video.play();
|
||||
$("#gestureCameraEmpty").hidden = true;
|
||||
$("#gestureLiveBadge").hidden = false;
|
||||
$("#gestureArmButton").disabled = false;
|
||||
$("#gestureStopButton").disabled = false;
|
||||
button.classList.add("active");
|
||||
button.querySelector("span").textContent = "关闭摄像头";
|
||||
button.setAttribute("aria-label", "关闭摄像头");
|
||||
button.setAttribute("title", "关闭摄像头");
|
||||
$("#gestureSafetyText").textContent = "张开手掌并保持 1 秒以激活";
|
||||
state.frameRequest = requestAnimationFrame(recognizeLoop);
|
||||
} catch (error) {
|
||||
$("#gestureSafetyText").textContent = error.message || "摄像头启动失败";
|
||||
notify(error.message || "摄像头启动失败");
|
||||
stopCamera();
|
||||
const failure = cameraFailureDetails(error);
|
||||
setCameraAvailability(failure.status, failure.message, { announce: false });
|
||||
stopCamera({ message: failure.message });
|
||||
notify(failure.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
syncCameraButtonAvailability();
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
function stopCamera({ message = null } = {}) {
|
||||
if (state.frameRequest) cancelAnimationFrame(state.frameRequest);
|
||||
state.frameRequest = null;
|
||||
state.stream?.getTracks().forEach((track) => track.stop());
|
||||
@@ -533,15 +632,17 @@ function stopCamera() {
|
||||
canvas?.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
|
||||
$("#gestureCameraEmpty").hidden = false;
|
||||
$("#gestureLiveBadge").hidden = true;
|
||||
$("#gestureArmButton").disabled = true;
|
||||
$("#gestureStopButton").disabled = true;
|
||||
const button = $("#gestureStartButton");
|
||||
button.classList.remove("active");
|
||||
button.querySelector("span").textContent = "开启摄像头";
|
||||
button.setAttribute("aria-label", "开启摄像头");
|
||||
button.setAttribute("title", "开启摄像头");
|
||||
state.engine?.setArmed(false);
|
||||
$("#gestureDetected").textContent = "—";
|
||||
$("#gestureExpected").textContent = "等待启动";
|
||||
$("#gestureSafetyText").textContent = "摄像头尚未开启";
|
||||
$("#gestureSafetyText").textContent = message || state.cameraMessage || "摄像头尚未开启";
|
||||
syncCameraButtonAvailability();
|
||||
}
|
||||
|
||||
function toggleCamera() {
|
||||
@@ -595,63 +696,70 @@ function resetMapping() {
|
||||
}
|
||||
|
||||
function openDrawer() {
|
||||
$("#gestureDrawer").classList.add("open");
|
||||
$("#gestureDrawer").setAttribute("aria-hidden", "false");
|
||||
$("#gestureBackdrop").hidden = false;
|
||||
requestAnimationFrame(() => $("#gestureBackdrop").classList.add("visible"));
|
||||
$("#gestureToggleButton").setAttribute("aria-expanded", "true");
|
||||
if (!state.enabled) {
|
||||
notify(state.disabledReason || "当前运行模式未启用手势控制");
|
||||
return false;
|
||||
}
|
||||
const drawer = $("#gestureDrawer");
|
||||
if (!drawer) return false;
|
||||
drawer.classList.add("open");
|
||||
drawer.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("gesture-open");
|
||||
return true;
|
||||
}
|
||||
|
||||
function closeDrawer() {
|
||||
stopCamera();
|
||||
$("#gestureDrawer").classList.remove("open");
|
||||
$("#gestureDrawer").setAttribute("aria-hidden", "true");
|
||||
$("#gestureBackdrop").classList.remove("visible");
|
||||
window.setTimeout(() => { $("#gestureBackdrop").hidden = true; }, 240);
|
||||
$("#gestureToggleButton").setAttribute("aria-expanded", "false");
|
||||
const drawer = $("#gestureDrawer");
|
||||
drawer?.classList.remove("open");
|
||||
drawer?.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("gesture-open");
|
||||
}
|
||||
|
||||
function initModeRail() {
|
||||
document.querySelectorAll("[data-mode-command]").forEach((button) => button.addEventListener("click", () => {
|
||||
window.CognitiveAgentConsole?.executeUtterance?.(button.dataset.modeCommand, { autoSubmit: true });
|
||||
}));
|
||||
$("#saveModeToggle")?.addEventListener("click", () => {
|
||||
const composer = $("#saveModeComposer");
|
||||
composer.hidden = !composer.hidden;
|
||||
if (!composer.hidden) $("#saveModeName").focus();
|
||||
});
|
||||
$("#saveModeConfirm")?.addEventListener("click", () => {
|
||||
const name = $("#saveModeName").value.trim();
|
||||
if (!name) return notify("请先输入模式名称");
|
||||
window.CognitiveAgentConsole?.executeUtterance?.(`将当前副驾驶座椅姿态保存为${name}模式`, { autoSubmit: true });
|
||||
$("#saveModeComposer").hidden = true;
|
||||
$("#saveModeName").value = "";
|
||||
});
|
||||
function setEnabled(enabled, { reason = "" } = {}) {
|
||||
state.enabled = Boolean(enabled);
|
||||
state.disabledReason = state.enabled ? "" : String(reason || "当前运行模式未启用手势控制");
|
||||
if (!state.enabled) closeDrawer();
|
||||
syncCameraButtonAvailability();
|
||||
const drawer = $("#gestureDrawer");
|
||||
if (drawer) {
|
||||
drawer.dataset.enabled = String(state.enabled);
|
||||
drawer.title = state.disabledReason;
|
||||
}
|
||||
}
|
||||
|
||||
window.CognitiveAgentGesture = {
|
||||
open: openDrawer,
|
||||
close: closeDrawer,
|
||||
isOpen: () => $("#gestureDrawer")?.classList.contains("open") === true,
|
||||
setEnabled,
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
$("#gestureToggleButton")?.addEventListener("click", openDrawer);
|
||||
$("#gestureStartButton").disabled = true;
|
||||
$("#gestureCloseButton")?.addEventListener("click", closeDrawer);
|
||||
$("#gestureBackdrop")?.addEventListener("click", closeDrawer);
|
||||
$("#gestureStartButton")?.addEventListener("click", toggleCamera);
|
||||
$("#gestureArmButton")?.addEventListener("click", () => state.engine?.setArmed(!state.engine.armed));
|
||||
$("#gestureStopButton")?.addEventListener("click", () => state.engine?.setArmed(false));
|
||||
$("#gestureCustomizeButton")?.addEventListener("click", toggleCustomize);
|
||||
$("#gestureBindButton")?.addEventListener("click", bindCurrentGesture);
|
||||
$("#gestureResetMapping")?.addEventListener("click", resetMapping);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && $("#gestureDrawer")?.classList.contains("open")) closeDrawer();
|
||||
if (event.key !== "Escape") return;
|
||||
if ($("#gestureDrawer")?.classList.contains("open")) closeDrawer();
|
||||
});
|
||||
document.addEventListener("visibilitychange", () => { if (document.hidden && state.stream) stopCamera(); });
|
||||
window.addEventListener("beforeunload", stopCamera);
|
||||
initModeRail();
|
||||
navigator.mediaDevices?.addEventListener?.("devicechange", handleCameraDeviceChange);
|
||||
let runtimeError = null;
|
||||
try {
|
||||
await loadGestureRuntime();
|
||||
$("#gestureSafetyText").textContent = state.runtime.available ? "能力就绪,开启摄像头开始识别" : "内置手势模型不可用";
|
||||
state.runtimeReady = Boolean(state.runtime.available && state.runtime.model_available);
|
||||
if (!state.runtimeReady) runtimeError = new Error("内置手势模型不可用");
|
||||
} catch (error) {
|
||||
$("#gestureSafetyText").textContent = error.message;
|
||||
$("#gestureStartButton").disabled = true;
|
||||
runtimeError = error;
|
||||
state.runtimeReady = false;
|
||||
}
|
||||
await refreshCameraAvailability({ announce: !runtimeError });
|
||||
if (runtimeError) $("#gestureSafetyText").textContent = runtimeError.message;
|
||||
syncCameraButtonAvailability();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: oklch(0.968 0.019 244);
|
||||
--foreground: oklch(0.29 0.042 252);
|
||||
--card: oklch(0.993 0.007 245);
|
||||
--card-foreground: oklch(0.34 0.032 250);
|
||||
--primary: oklch(0.57 0.19 258);
|
||||
--primary-foreground: oklch(0.99 0.004 245);
|
||||
--secondary: oklch(0.94 0.03 244);
|
||||
--secondary-foreground: oklch(0.38 0.045 250);
|
||||
--muted: oklch(0.955 0.017 245);
|
||||
--muted-foreground: oklch(0.53 0.035 248);
|
||||
--accent: oklch(0.918 0.045 249);
|
||||
--destructive: oklch(0.61 0.19 24);
|
||||
--border: oklch(0.85 0.04 245);
|
||||
--ring: oklch(0.65 0.14 256);
|
||||
--radius: 0.875rem;
|
||||
font-family: Inter, "Microsoft YaHei", system-ui, sans-serif;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
}
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--background: oklch(0.2 0.035 252);
|
||||
--foreground: oklch(0.94 0.02 245);
|
||||
--card: oklch(0.25 0.035 251);
|
||||
--card-foreground: oklch(0.91 0.022 245);
|
||||
--primary: oklch(0.7 0.15 252);
|
||||
--primary-foreground: oklch(0.19 0.04 253);
|
||||
--secondary: oklch(0.285 0.045 250);
|
||||
--secondary-foreground: oklch(0.92 0.02 245);
|
||||
--muted: oklch(0.275 0.032 250);
|
||||
--muted-foreground: oklch(0.72 0.04 245);
|
||||
--accent: oklch(0.32 0.065 250);
|
||||
--destructive: oklch(0.7 0.18 24);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--ring: oklch(0.68 0.13 252);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; background: radial-gradient(circle at 18% 0%, color-mix(in oklch, var(--primary) 12%, transparent) 0, transparent 35%), var(--background); }
|
||||
.shell { width: min(1040px, calc(100% - 40px)); margin: 0 auto; padding: 54px 0 80px; }
|
||||
header, .section-title, .policy, .request, .lease-details { display: flex; align-items: center; justify-content: space-between; gap: 20px; }
|
||||
header { margin-bottom: 34px; }
|
||||
h1, h2, p { margin: 0; }
|
||||
h1 { font-size: clamp(30px, 5vw, 48px); letter-spacing: -.04em; }
|
||||
h2 { font-size: 22px; }
|
||||
.eyebrow { color: var(--primary); font-size: 12px; letter-spacing: .16em; font-weight: 800; margin-bottom: 8px; }
|
||||
.lead, small { color: var(--muted-foreground); }
|
||||
.lead { margin-top: 12px; }
|
||||
a, button { font: inherit; }
|
||||
a { color: var(--primary); text-decoration: none; border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 14px; }
|
||||
.policy, .card { color: var(--card-foreground); background: color-mix(in oklch, var(--card) 94%, transparent); border: 1px solid var(--border); border-radius: calc(var(--radius) + 8px); box-shadow: 0 20px 60px oklch(0.29 0.042 252 / 12%); }
|
||||
.policy { padding: 18px 22px; justify-content: flex-start; margin-bottom: 18px; }
|
||||
.policy div { display: grid; gap: 3px; flex: 1; }
|
||||
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 16px color-mix(in oklch, var(--primary) 55%, transparent); }
|
||||
#clock { color: var(--muted-foreground); font-variant-numeric: tabular-nums; }
|
||||
.card { padding: 24px; margin-top: 18px; }
|
||||
.active-card { border-color: var(--ring); }
|
||||
.section-title { margin-bottom: 20px; }
|
||||
.count { min-width: 30px; height: 30px; display: grid; place-items: center; border-radius: 999px; background: var(--accent); color: var(--primary); font-weight: 800; }
|
||||
.empty { padding: 26px 4px; color: var(--muted-foreground); text-align: center; }
|
||||
.request-list { display: grid; gap: 12px; }
|
||||
.request { padding: 16px; border-radius: calc(var(--radius) + 3px); border: 1px solid var(--border); background: var(--muted); }
|
||||
.identity { display: grid; gap: 5px; min-width: 0; }
|
||||
.identity strong { font-size: 17px; }
|
||||
.identity code, .identity span { color: var(--muted-foreground); font-size: 12px; overflow-wrap: anywhere; }
|
||||
.actions { display: flex; gap: 8px; }
|
||||
button { color: var(--primary-foreground); background: var(--primary); border: 0; border-radius: var(--radius); padding: 10px 14px; cursor: pointer; font-weight: 750; }
|
||||
button:hover { filter: brightness(1.12); }
|
||||
button:disabled { opacity: .45; cursor: wait; }
|
||||
button.secondary { color: var(--secondary-foreground); background: var(--secondary); }
|
||||
button.danger { color: var(--primary-foreground); background: var(--destructive); }
|
||||
.lease-details { justify-content: flex-start; flex-wrap: wrap; padding: 18px; border-radius: calc(var(--radius) + 3px); background: var(--muted); border: 1px solid var(--ring); }
|
||||
.lease-details .timer { margin-left: auto; color: var(--primary); font-weight: 850; font-size: 22px; font-variant-numeric: tabular-nums; }
|
||||
.message { margin-top: 18px; min-height: 24px; color: var(--primary); }
|
||||
@media (max-width: 680px) { .shell { width: min(100% - 24px, 1040px); padding-top: 26px; } header, .request, .lease-details { align-items: flex-start; flex-direction: column; } .actions { width: 100%; } .actions button { flex: 1; } .lease-details .timer { margin-left: 0; } }
|
||||
@@ -0,0 +1,48 @@
|
||||
<!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="oklch(0.57 0.19 258)">
|
||||
<title>真实台架本地决策台</title>
|
||||
<link rel="stylesheet" href="/hardware-control.css?v=ai-blue-2">
|
||||
</head>
|
||||
<body>
|
||||
<main class="shell">
|
||||
<header>
|
||||
<div>
|
||||
<p class="eyebrow">LOCAL BENCH AUTHORITY</p>
|
||||
<h1>真实台架本地决策台</h1>
|
||||
<p class="lead">只有本机可以批准、拒绝或停止终端的真实台架使用权。</p>
|
||||
</div>
|
||||
<a href="/">返回控制台</a>
|
||||
</header>
|
||||
|
||||
<section class="policy">
|
||||
<span class="dot"></span>
|
||||
<div><strong>单终端租约</strong><small>获批终端可连续测试;最后一次真实执行后空闲 180 秒自动停止。</small></div>
|
||||
<span id="clock">正在连接…</span>
|
||||
</section>
|
||||
|
||||
<section class="card active-card">
|
||||
<div class="section-title">
|
||||
<div><p class="eyebrow">ACTIVE LEASE</p><h2>当前使用者</h2></div>
|
||||
<button id="revokeButton" class="danger" type="button" hidden>立即停止使用</button>
|
||||
</div>
|
||||
<div id="activeLease" class="empty">当前无人获批使用真实台架。</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="section-title">
|
||||
<div><p class="eyebrow">PENDING REQUESTS</p><h2>等待审批</h2></div>
|
||||
<span id="pendingCount" class="count">0</span>
|
||||
</div>
|
||||
<div id="pendingRequests" class="request-list empty">暂时没有申请。</div>
|
||||
</section>
|
||||
|
||||
<p id="message" class="message" role="status"></p>
|
||||
</main>
|
||||
<script src="/hardware-control.js" defer></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,104 @@
|
||||
const activeLease = document.querySelector("#activeLease");
|
||||
const pendingRequests = document.querySelector("#pendingRequests");
|
||||
const pendingCount = document.querySelector("#pendingCount");
|
||||
const revokeButton = document.querySelector("#revokeButton");
|
||||
const message = document.querySelector("#message");
|
||||
let snapshot = null;
|
||||
|
||||
function escapeHtml(value) {
|
||||
const node = document.createElement("span");
|
||||
node.textContent = String(value ?? "");
|
||||
return node.innerHTML;
|
||||
}
|
||||
|
||||
function showMessage(value, error = false) {
|
||||
message.textContent = value;
|
||||
message.style.color = error ? "#ff8797" : "#7fbbff";
|
||||
}
|
||||
|
||||
async function api(path, body) {
|
||||
const response = await fetch(path, {
|
||||
method: body ? "POST" : "GET",
|
||||
credentials: "same-origin",
|
||||
headers: body ? { "Content-Type": "application/json" } : {},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (response.status === 401) {
|
||||
location.replace(`/login?next=${encodeURIComponent(location.pathname)}`);
|
||||
throw new Error("请先登录本机服务");
|
||||
}
|
||||
const payload = await response.json();
|
||||
if (!response.ok) throw new Error(payload.message || payload.error);
|
||||
return payload;
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
snapshot = data;
|
||||
pendingCount.textContent = String(data.pending.length);
|
||||
if (data.active) {
|
||||
const lease = data.active;
|
||||
activeLease.className = "lease-details";
|
||||
activeLease.innerHTML = `
|
||||
<div class="identity"><strong>${escapeHtml(lease.label)}</strong><code>${escapeHtml(lease.client_id)}</code><span>来源 ${escapeHtml(lease.client_address)} · 最近执行 ${escapeHtml(lease.last_activity_at)}</span></div>
|
||||
<div class="timer">${Number(lease.idle_remaining_seconds)}s</div>`;
|
||||
revokeButton.hidden = false;
|
||||
revokeButton.dataset.clientId = lease.client_id;
|
||||
} else {
|
||||
activeLease.className = "empty";
|
||||
activeLease.textContent = "当前无人获批使用真实台架。";
|
||||
revokeButton.hidden = true;
|
||||
}
|
||||
if (!data.pending.length) {
|
||||
pendingRequests.className = "request-list empty";
|
||||
pendingRequests.textContent = "暂时没有申请。";
|
||||
return;
|
||||
}
|
||||
pendingRequests.className = "request-list";
|
||||
pendingRequests.innerHTML = data.pending.map((request) => `
|
||||
<article class="request">
|
||||
<div class="identity"><strong>${escapeHtml(request.label)}</strong><code>${escapeHtml(request.client_id)}</code><span>来源 ${escapeHtml(request.client_address)} · ${escapeHtml(request.requested_at)}</span></div>
|
||||
<div class="actions">
|
||||
<button type="button" data-action="approve" data-client-id="${escapeHtml(request.client_id)}">同意</button>
|
||||
<button type="button" class="secondary" data-action="deny" data-client-id="${escapeHtml(request.client_id)}">拒绝</button>
|
||||
</div>
|
||||
</article>`).join("");
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
try {
|
||||
render(await api("/api/local/hardware-access"));
|
||||
document.querySelector("#clock").textContent = new Date().toLocaleTimeString("zh-CN", { hour12: false });
|
||||
} catch (error) {
|
||||
showMessage(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
pendingRequests.addEventListener("click", async (event) => {
|
||||
const button = event.target.closest("button[data-action]");
|
||||
if (!button) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
const action = button.dataset.action;
|
||||
render(await api(`/api/local/hardware-access/${action}`, { client_id: button.dataset.clientId }));
|
||||
showMessage(action === "approve" ? "已授予该终端真实台架使用权。" : "已拒绝该终端的申请。");
|
||||
} catch (error) {
|
||||
showMessage(error.message, true);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
revokeButton.addEventListener("click", async () => {
|
||||
revokeButton.disabled = true;
|
||||
try {
|
||||
render(await api("/api/local/hardware-access/revoke", { client_id: revokeButton.dataset.clientId }));
|
||||
showMessage("已停止该终端的真实台架使用权;再次使用必须重新申请。");
|
||||
} catch (error) {
|
||||
showMessage(error.message, true);
|
||||
} finally {
|
||||
revokeButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
refresh();
|
||||
setInterval(refresh, 2000);
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
@@ -2,496 +2,56 @@
|
||||
<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" />
|
||||
<title>智舱随行 · 一句话与手势控制</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
<script src="https://unpkg.com/lucide@0.468.0/dist/umd/lucide.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/build/three.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/libs/fflate.min.js" defer></script>
|
||||
<script src="https://unpkg.com/three@0.147.0/examples/js/loaders/FBXLoader.js" defer></script>
|
||||
<script src="/app.js?v=unified-cabin-22" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=gesture-core-3"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#17182b" id="themeColorMeta" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<script>
|
||||
(() => {
|
||||
const key = "cognitive-agent.glass-theme";
|
||||
let theme = "dark";
|
||||
try {
|
||||
const stored = localStorage.getItem(key);
|
||||
if (stored === "light" || stored === "dark") theme = stored;
|
||||
} catch (_error) {
|
||||
// Storage can be unavailable in hardened or private browser contexts.
|
||||
}
|
||||
try {
|
||||
const cookieTheme = document.cookie
|
||||
.split(";")
|
||||
.map((part) => part.trim())
|
||||
.find((part) => part.startsWith(`${key}=`))
|
||||
?.slice(key.length + 1);
|
||||
if (cookieTheme === "light" || cookieTheme === "dark") theme = cookieTheme;
|
||||
} catch (_error) {
|
||||
// The default still applies when cookies are disabled.
|
||||
}
|
||||
document.documentElement.dataset.glassTheme = theme;
|
||||
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-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-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>
|
||||
<header class="topbar">
|
||||
<div class="brand-block">
|
||||
<span class="brand-mark" aria-hidden="true"><i data-lucide="brain-circuit"></i></span>
|
||||
<div>
|
||||
<h1>Cognitive Agent</h1>
|
||||
<p>CABIN ORCHESTRATION <span>/</span> MULTI-DOMAIN</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="runtime-badges" aria-label="运行环境">
|
||||
<button class="gesture-entry" id="gestureToggleButton" type="button" aria-controls="gestureDrawer" aria-expanded="false">
|
||||
<span class="gesture-entry-dot"></span><i data-lucide="scan-face"></i>手势控制
|
||||
</button>
|
||||
<span class="status-badge neutral" id="executionBadge"><i data-lucide="flask-conical"></i>DRY-RUN</span>
|
||||
<span class="status-badge" id="modelBadge"><i data-lucide="sparkles"></i>模型检测中</span>
|
||||
<span class="status-badge danger" id="hardwareBadge"><i data-lucide="shield-off"></i>硬件检测中</span>
|
||||
<button class="status-badge auth-logout" id="logoutButton" type="button" title="退出登录" aria-label="退出登录" hidden>
|
||||
<i data-lucide="log-out"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="workspace">
|
||||
<aside class="command-panel">
|
||||
<div class="panel-heading">
|
||||
<span class="section-kicker">NATURAL CABIN CONTROL</span>
|
||||
<h2>说一句,就调好了</h2>
|
||||
<p class="panel-intro">座椅、空调、天幕与氛围灯,一句话联动完成。</p>
|
||||
</div>
|
||||
|
||||
<div class="mode-rail" aria-label="快捷功能模式">
|
||||
<div class="mode-rail-heading"><span>快捷模式</span><button id="saveModeToggle" type="button"><i data-lucide="bookmark-plus"></i>保存当前</button></div>
|
||||
<div class="mode-chips">
|
||||
<button type="button" data-mode-command="进入观影模式:副驾驶调整为舒适观影姿态,空调24度、风量2档,天幕调暗,氛围灯切换成暖橙色"><i data-lucide="clapperboard"></i>观影</button>
|
||||
<button type="button" data-mode-command="进入午休模式:副驾驶靠背后倾,腿托伸出,空调25度,天幕最暗,氛围灯切换为柔和暖色"><i data-lucide="moon-star"></i>午休</button>
|
||||
<button type="button" data-mode-command="副驾驶调整为零重力模式"><i data-lucide="orbit"></i>零重力</button>
|
||||
</div>
|
||||
<div class="save-mode-composer" id="saveModeComposer" hidden>
|
||||
<input id="saveModeName" maxlength="24" placeholder="给当前模式起个名字" />
|
||||
<button id="saveModeConfirm" type="button">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="requestForm">
|
||||
<div class="field-label-row">
|
||||
<label for="utterance">自然语言指令</label>
|
||||
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
|
||||
<i data-lucide="message-square-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="command-composer">
|
||||
<textarea id="utterance" name="utterance" maxlength="1000" placeholder="例如:有点热,把副驾驶调舒服些,再把天幕调暗">进入观影状态:空调24度、风量2档,天幕调暗,氛围灯换成暖橙色</textarea>
|
||||
<span class="composer-hint">Enter 执行 · Shift + Enter 换行</span>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">意图解析器</span>
|
||||
<div class="segmented" role="group" aria-label="意图解析器">
|
||||
<button type="button" class="segment active" data-provider="openai">
|
||||
<i data-lucide="sparkles"></i>GPT
|
||||
</button>
|
||||
<button type="button" class="segment" data-provider="local">
|
||||
<i data-lucide="braces"></i>本地规则
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented" role="group" aria-label="执行目标">
|
||||
<button type="button" class="mode-option active" data-execution-mode="dry_run">
|
||||
<i data-lucide="flask-conical"></i>虚拟 CAN
|
||||
</button>
|
||||
<button type="button" class="mode-option" id="hardwareMode" data-execution-mode="hardware" disabled>
|
||||
<i data-lucide="radio-tower"></i>真实台架
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="hardwareNote">正在检查 CAN 适配器与 IAR 固件配置</p>
|
||||
<p class="provider-note" id="hardwareAliasNote">主驾和副驾暂时共享同一组实体电机、加热与通风执行器</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
<label class="field-label" for="vehicleSelect">车型配置</label>
|
||||
<div class="vehicle-select-control">
|
||||
<i data-lucide="car-front"></i>
|
||||
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
|
||||
<option value="">加载车型配置</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group approval-field" id="approvalField" hidden>
|
||||
<label class="field-label" for="approvalToken">硬件执行授权令牌</label>
|
||||
<input id="approvalToken" name="approval_token" type="password" inputmode="numeric" maxlength="6" autocomplete="off" spellcheck="false" placeholder="请输入 123456" />
|
||||
<p class="provider-note">演示授权令牌为 123456,仅用于本次安全校验,不会写入日志、会话或执行制品。</p>
|
||||
<p class="provider-note" id="autoFlashNote">正在检查自动烧录配置</p>
|
||||
</div>
|
||||
|
||||
<button class="primary-action" id="runButton" type="submit">
|
||||
<i data-lucide="play"></i>
|
||||
<span>生成并执行</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="example-group">
|
||||
<span class="field-label">示例请求</span>
|
||||
<button type="button" class="example-command" data-example="空调打开,车内温度调到24度,风量3档">
|
||||
<i data-lucide="fan"></i><span>空调 24℃ · 风量 3 档</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="天幕透光率调到30%,遮阳帘打开一半">
|
||||
<i data-lucide="panels-top-left"></i><span>天幕透光 30% · 遮阳帘 50%</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="打开氛围灯,调成冰蓝色,亮度60%">
|
||||
<i data-lucide="lamp-ceiling"></i><span>冰蓝氛围灯 · 亮度 60%</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command featured-example" data-example="进入观影状态:副驾调整为舒适观影姿态,空调24度、风量2档,天幕调暗,氛围灯换成暖橙色">
|
||||
<i data-lucide="clapperboard"></i><span>跨域观影场景</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="副驾有点热,有点闷">
|
||||
<i data-lucide="wind"></i><span>副驾有点热,有点闷</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="副驾靠背向后移动5度">
|
||||
<i data-lucide="move-horizontal"></i><span>副驾靠背向后 5°</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="副驾调整为零重力模式,要求靠背座椅夹角128度,坐垫脚托夹角133度">
|
||||
<i data-lucide="compass"></i><span>副驾零重力精确夹角</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command" data-example="将当前状态保存为零重力模式">
|
||||
<i data-lucide="bookmark-plus"></i><span>保存当前座椅姿态</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
<button type="button" class="example-command warning-example" data-example="副驾座椅加热开到三档">
|
||||
<i data-lucide="triangle-alert"></i><span>三档请求(档位虚拟)</span><i data-lucide="chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="safety-strip" id="safetyStrip">
|
||||
<i data-lucide="shield-check"></i>
|
||||
<div>
|
||||
<strong id="safetyTitle">虚拟执行边界</strong>
|
||||
<span id="safetyText">四域 CAN 均为 dry-run,实车发送被强制关闭。</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="main-panel">
|
||||
<section class="request-header">
|
||||
<div>
|
||||
<span class="section-kicker">ORCHESTRATION RUN</span>
|
||||
<div class="request-title-row">
|
||||
<h2 id="resultTitle">等待请求</h2>
|
||||
<span class="run-status idle" id="runStatus">IDLE</span>
|
||||
</div>
|
||||
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
|
||||
</div>
|
||||
<div class="matrix-stat">
|
||||
<i data-lucide="table-properties"></i>
|
||||
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="pipeline" aria-label="执行链路">
|
||||
<div class="pipeline-step" data-stage="intent_parse"><span>01</span><i data-lucide="message-square-text"></i><b>意图抽取</b></div>
|
||||
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
|
||||
<div class="pipeline-step" data-stage="function_generate"><span>02</span><i data-lucide="file-code-2"></i><b>配置生成</b></div>
|
||||
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
|
||||
<div class="pipeline-step" data-stage="domain_validate"><span>03</span><i data-lucide="badge-check"></i><b>双层校验</b></div>
|
||||
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
|
||||
<div class="pipeline-step" data-stage="compile"><span>04</span><i data-lucide="binary"></i><b>CAN 编译</b></div>
|
||||
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
|
||||
<div class="pipeline-step" data-stage="safety_check"><span>05</span><i data-lucide="shield-check"></i><b>安全门禁</b></div>
|
||||
<i class="pipeline-arrow" data-lucide="chevron-right"></i>
|
||||
<div class="pipeline-step" data-stage="execute"><span>06</span><i data-lucide="radio-tower"></i><b id="executeStageLabel">虚拟执行</b></div>
|
||||
</section>
|
||||
|
||||
<section class="cabin-band" aria-labelledby="cabinStateTitle">
|
||||
<div class="section-title-row cabin-heading">
|
||||
<div>
|
||||
<span class="section-kicker">JOINT DEVICE STATE</span>
|
||||
<h3 id="cabinStateTitle">联合设备状态</h3>
|
||||
</div>
|
||||
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
<div class="domain-grid">
|
||||
<article class="domain-panel climate-panel" data-domain-panel="climate">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="fan"></i></span>
|
||||
<div><strong>空调</strong><small>CLIMATE · 0x064</small></div>
|
||||
<span class="device-state" id="climatePower">OFF</span>
|
||||
</header>
|
||||
<div class="climate-display">
|
||||
<div class="zone-temperature">
|
||||
<span>主驾</span><strong id="driverTemperature">24<sup>℃</sup></strong>
|
||||
</div>
|
||||
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<div class="zone-temperature passenger-temperature">
|
||||
<span>副驾</span><strong id="passengerTemperature">24<sup>℃</sup></strong>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>风量</dt><dd id="fanLevel">0 档</dd></div>
|
||||
<div><dt>循环</dt><dd id="cycleMode">外循环</dd></div>
|
||||
<div><dt>A/C</dt><dd id="acState">关闭</dd></div>
|
||||
<div><dt>前除霜</dt><dd id="defrostState">关闭</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel roof-panel" data-domain-panel="roof">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
|
||||
<div><strong>智能天幕</strong><small>ROOF · 0x4C1 / 0x3E6</small></div>
|
||||
<span class="device-state" id="roofState">READY</span>
|
||||
</header>
|
||||
<div class="roof-visual" aria-label="天幕透光率与遮阳帘位置">
|
||||
<div class="roof-glass" id="roofGlass">
|
||||
<span class="roof-shade" id="roofShade"></span>
|
||||
<i></i><i></i><i></i>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>透光率</dt><dd id="transmittanceValue">100%</dd></div>
|
||||
<div><dt>遮阳帘</dt><dd id="shadePositionValue">0%</dd></div>
|
||||
<div><dt>自动调光</dt><dd id="automaticDimming">关闭</dd></div>
|
||||
<div><dt>电机反馈</dt><dd id="roofMotorState">已停止</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
|
||||
<div><strong>氛围灯</strong><small>AMBIENT · RGB 16.8M</small></div>
|
||||
<span class="device-state" id="ambientPower">OFF</span>
|
||||
</header>
|
||||
<div class="ambient-visual" id="ambientVisual" aria-label="副驾手套箱上方九段氛围灯排灯">
|
||||
<div class="ambient-dashboard-sketch" aria-hidden="true">
|
||||
<span class="ambient-dashboard-top"></span>
|
||||
<span class="ambient-center-screen"></span>
|
||||
<span class="ambient-glovebox"></span>
|
||||
<span class="ambient-glovebox-lamp">
|
||||
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
|
||||
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
|
||||
<i class="ambient-line"></i><i class="ambient-line"></i><i class="ambient-line"></i>
|
||||
</span>
|
||||
</div>
|
||||
<small class="ambient-location-label">副驾 · 手套箱上方排灯</small>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>颜色</dt><dd><i class="color-swatch" id="ambientSwatch"></i><span id="ambientColor">#74D9FF</span></dd></div>
|
||||
<div><dt>亮度</dt><dd id="ambientBrightness">0%</dd></div>
|
||||
<div><dt>模式</dt><dd id="ambientMode">关闭</dd></div>
|
||||
<div><dt>控制精度</dt><dd>24-bit RGB</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="twin-band" aria-labelledby="twinTitle">
|
||||
<div class="twin-heading">
|
||||
<div>
|
||||
<span class="section-kicker">FULL VEHICLE DIGITAL TWIN</span>
|
||||
<h3 id="twinTitle">整车孪生界面</h3>
|
||||
<div class="preset-indicator" id="presetIndicator" hidden>
|
||||
<i data-lucide="bookmark-check"></i><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-toolbar">
|
||||
<span class="twin-focus-badge"><i data-lucide="scan"></i>透明车体 · 实体设备</span>
|
||||
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
|
||||
<i data-lucide="rotate-3d"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="具有前后遮挡明暗层次的半透明整车外壳、始终清晰显示的动态副驾、全景天幕、仪表台空调出风口与副驾手套箱上方氛围灯三维状态"></canvas>
|
||||
<div class="vehicle-system-hud" aria-label="整车孪生系统状态">
|
||||
<span data-twin-system="roof"><i data-lucide="panels-top-left"></i><small>全景天幕</small><b id="twinRoofState">透光 100%</b></span>
|
||||
<span data-twin-system="climate"><i data-lucide="wind"></i><small>仪表台 HVAC</small><b id="twinClimateState">关闭</b></span>
|
||||
<span data-twin-system="ambient"><i data-lucide="lamp-ceiling"></i><small>手套箱氛围灯</small><b id="twinAmbientState">关闭</b></span>
|
||||
</div>
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾动态座椅</span><small>ACTIVE · FRONT RIGHT</small></div>
|
||||
<div class="twin-callout-layer" aria-hidden="true">
|
||||
<div class="twin-callout" data-callout="longitudinal"><span>水平位置</span><b>50%</b><small>导轨</small></div>
|
||||
<div class="twin-callout" data-callout="cushion_tilt"><span>坐垫俯仰</span><b>7°</b><small>50%</small></div>
|
||||
<div class="twin-callout" data-callout="backrest_recline"><span>靠背夹角</span><b>20.5°</b><small>20% · 相对竖直</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_height"><span>腿托夹角</span><b>78.5°</b><small>0% · 相对地板</small></div>
|
||||
<div class="twin-callout" data-callout="legrest_extension"><span>腿托伸缩</span><b>0%</b><small>行程</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="backrest_included_angle" hidden><span>靠背-坐垫夹角</span><b>104°</b><small>本轮指定夹角</small></div>
|
||||
<div class="twin-callout included-angle-callout" data-callout="legrest_included_angle" hidden><span>坐垫-腿托夹角</span><b>95°</b><small>本轮指定夹角</small></div>
|
||||
</div>
|
||||
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
|
||||
<aside class="twin-readout" aria-live="polite">
|
||||
<div class="twin-readout-heading">
|
||||
<div><strong id="twinSeatName">副驾</strong><span id="twinSeatZone">front_right</span></div>
|
||||
<span class="twin-feedback"><i></i>FEEDBACK</span>
|
||||
</div>
|
||||
<div class="twin-axis-list">
|
||||
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
|
||||
</div>
|
||||
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
|
||||
<span data-twin-function="ventilation" data-level="0">
|
||||
<i data-lucide="wind"></i>
|
||||
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span data-twin-function="heating" data-level="0">
|
||||
<i data-lucide="flame"></i>
|
||||
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><span class="section-kicker">ACTUATOR STATE</span><h3>前排座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾</strong><span>front_left</span></div></div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾</strong><span>front_right</span></div></div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-section">
|
||||
<span class="section-kicker">MATRIX COVERAGE</span>
|
||||
<h3>当前控制边界</h3>
|
||||
<dl>
|
||||
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
|
||||
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
|
||||
<header class="result-workspace-heading">
|
||||
<div><span class="section-kicker">ORCHESTRATION WORKSPACE</span><h3 id="resultWorkspaceTitle">编排结果工作台</h3><p>四类结果在同一页面连续呈现,也可以通过下方入口快速定位。</p></div>
|
||||
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
|
||||
</header>
|
||||
<nav class="tabs result-nav" aria-label="结果快速导航">
|
||||
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
|
||||
</nav>
|
||||
|
||||
<div class="tab-panel active" id="panel-trace" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><span class="section-kicker">01 · DECISION TRACE</span><h3>决策轨迹与校验</h3><p>从自然语言识别到领域路由,集中查看每一步决策依据。</p></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
|
||||
<div class="trace-layout">
|
||||
<div>
|
||||
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
|
||||
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
|
||||
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-block">
|
||||
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
|
||||
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
|
||||
<div class="config-panel-heading">
|
||||
<div><span class="section-kicker">02 · INSTRUCTION → HARDWARE</span><h3>指令配置与硬件映射</h3><p>每张卡片对应一个控制对象,依次展示目标参数和最终硬件计划。</p></div>
|
||||
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
|
||||
</div>
|
||||
<div class="config-card-grid" id="configCards">
|
||||
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
|
||||
</div>
|
||||
<details class="config-source-details">
|
||||
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
|
||||
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
|
||||
<div class="workspace-panel-title plan-panel-heading"><div><span class="section-kicker">03 · CAN COMMAND PLAN</span><h3>CAN 编译计划</h3><p>快速核对帧数量、首帧载荷、反馈帧和执行模式。</p></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
|
||||
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
|
||||
<details class="config-source-details plan-source-details">
|
||||
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
|
||||
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><span class="section-kicker">04 · EXECUTION RESULT</span><h3>执行结果与硬件回读</h3><p>汇总每个领域的执行状态、CAN 回读和失败原因。</p></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
|
||||
<div class="execution-summary" id="executionSummary">
|
||||
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
|
||||
</div>
|
||||
<div class="result-list" id="resultList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="gesture-backdrop" id="gestureBackdrop" hidden></div>
|
||||
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
|
||||
<header class="gesture-drawer-header">
|
||||
<div>
|
||||
<span class="section-kicker">VISION CONTROL</span>
|
||||
<h2 id="gestureDrawerTitle">手势控制</h2>
|
||||
<p>本机识别,动作通过当前编排链路执行。</p>
|
||||
</div>
|
||||
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
|
||||
<div class="gesture-status-strip">
|
||||
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
|
||||
<span id="gestureSafetyText">摄像头尚未开启</span>
|
||||
</div>
|
||||
|
||||
<div class="gesture-camera-stage">
|
||||
<video id="gestureVideo" playsinline muted></video>
|
||||
<canvas id="gestureCanvas"></canvas>
|
||||
<div class="gesture-camera-empty" id="gestureCameraEmpty">
|
||||
<span><i data-lucide="scan-face"></i></span>
|
||||
<strong>用手势控制副驾驶座椅</strong>
|
||||
<p>画面只在本机处理,不上传视频。</p>
|
||||
</div>
|
||||
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-now">
|
||||
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
|
||||
<i data-lucide="arrow-right"></i>
|
||||
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
|
||||
|
||||
<div class="gesture-actions">
|
||||
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
|
||||
<button id="gestureArmButton" type="button" disabled><i data-lucide="hand"></i>激活控制</button>
|
||||
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
|
||||
</div>
|
||||
|
||||
<section class="gesture-guide-card">
|
||||
<div class="gesture-card-heading"><div><span>动作速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
|
||||
<div class="gesture-map-grid" id="gestureMapGrid"></div>
|
||||
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
|
||||
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
|
||||
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
|
||||
<button id="gestureBindButton" type="button">绑定当前手势</button>
|
||||
<button id="gestureResetMapping" type="button">恢复默认</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
|
||||
</aside>
|
||||
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
<body class="console-v2">
|
||||
<div id="app" aria-busy="true"></div>
|
||||
<noscript>需要启用 JavaScript 才能使用智能座舱控制台。</noscript>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,10 +1,52 @@
|
||||
:root {
|
||||
color: #17232b;
|
||||
background: #e8efed;
|
||||
color-scheme: light;
|
||||
--background: oklch(0.968 0.019 244);
|
||||
--foreground: oklch(0.29 0.042 252);
|
||||
--card: oklch(0.993 0.007 245);
|
||||
--card-foreground: oklch(0.34 0.032 250);
|
||||
--popover: oklch(0.998 0.004 245);
|
||||
--popover-foreground: oklch(0.29 0.042 252);
|
||||
--primary: oklch(0.57 0.19 258);
|
||||
--primary-foreground: oklch(0.99 0.004 245);
|
||||
--secondary: oklch(0.94 0.03 244);
|
||||
--secondary-foreground: oklch(0.38 0.045 250);
|
||||
--muted: oklch(0.955 0.017 245);
|
||||
--muted-foreground: oklch(0.53 0.035 248);
|
||||
--accent: oklch(0.918 0.045 249);
|
||||
--accent-foreground: oklch(0.38 0.1 258);
|
||||
--destructive: oklch(0.61 0.19 24);
|
||||
--border: oklch(0.85 0.04 245);
|
||||
--input: oklch(0.82 0.045 246);
|
||||
--ring: oklch(0.65 0.14 256);
|
||||
--radius: 0.875rem;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
font-family: Inter, "Noto Sans SC", "Microsoft YaHei", system-ui, sans-serif;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--background: oklch(0.2 0.035 252);
|
||||
--foreground: oklch(0.94 0.02 245);
|
||||
--card: oklch(0.25 0.035 251);
|
||||
--card-foreground: oklch(0.91 0.022 245);
|
||||
--popover: oklch(0.235 0.038 252);
|
||||
--popover-foreground: oklch(0.94 0.02 245);
|
||||
--primary: oklch(0.7 0.15 252);
|
||||
--primary-foreground: oklch(0.19 0.04 253);
|
||||
--secondary: oklch(0.285 0.045 250);
|
||||
--secondary-foreground: oklch(0.92 0.02 245);
|
||||
--muted: oklch(0.275 0.032 250);
|
||||
--muted-foreground: oklch(0.72 0.04 245);
|
||||
--accent: oklch(0.32 0.065 250);
|
||||
--accent-foreground: oklch(0.91 0.03 244);
|
||||
--destructive: oklch(0.7 0.18 24);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.68 0.13 252);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -14,9 +56,9 @@ body {
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
background:
|
||||
linear-gradient(rgba(217, 229, 225, 0.7) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(217, 229, 225, 0.7) 1px, transparent 1px),
|
||||
#eef3f1;
|
||||
linear-gradient(color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(90deg, color-mix(in oklch, var(--border) 70%, transparent) 1px, transparent 1px),
|
||||
linear-gradient(145deg, var(--background), var(--secondary));
|
||||
background-size: 34px 34px;
|
||||
}
|
||||
|
||||
@@ -30,10 +72,12 @@ body {
|
||||
.login-panel {
|
||||
width: min(420px, 100%);
|
||||
padding: 28px;
|
||||
background: #fff;
|
||||
border: 1px solid #cbd7d3;
|
||||
border-top: 4px solid #08766e;
|
||||
box-shadow: 0 18px 50px rgba(24, 42, 48, 0.12);
|
||||
color: var(--card-foreground);
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-top: 4px solid var(--primary);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: 0 18px 50px oklch(0.29 0.042 252 / 12%);
|
||||
}
|
||||
|
||||
.login-brand {
|
||||
@@ -41,7 +85,7 @@ body {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid #d9e1de;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.login-brand > span {
|
||||
@@ -49,8 +93,8 @@ body {
|
||||
height: 42px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
background: #08766e;
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary);
|
||||
border-radius: 5px;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
@@ -67,7 +111,7 @@ body {
|
||||
|
||||
.login-brand small {
|
||||
margin-top: 4px;
|
||||
color: #67747b;
|
||||
color: var(--muted-foreground);
|
||||
font: 8px "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
@@ -77,7 +121,7 @@ body {
|
||||
|
||||
.login-heading p {
|
||||
margin: 0 0 7px;
|
||||
color: #08766e;
|
||||
color: var(--primary);
|
||||
font: 750 9px "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
@@ -92,7 +136,7 @@ form {
|
||||
|
||||
label {
|
||||
margin: 0 0 7px;
|
||||
color: #4f5f66;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -102,32 +146,32 @@ input {
|
||||
height: 44px;
|
||||
margin-bottom: 18px;
|
||||
padding: 0 12px;
|
||||
color: #17232b;
|
||||
background: #f7f9f8;
|
||||
border: 1px solid #bbc8c4;
|
||||
border-radius: 4px;
|
||||
color: var(--foreground);
|
||||
background: var(--background);
|
||||
border: 1px solid var(--input);
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
font: inherit;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: 3px solid rgba(50, 105, 168, 0.16);
|
||||
border-color: #3269a8;
|
||||
outline: 3px solid color-mix(in oklch, var(--ring) 24%, transparent);
|
||||
border-color: var(--ring);
|
||||
}
|
||||
|
||||
.login-message {
|
||||
min-height: 17px;
|
||||
margin: -4px 0 10px;
|
||||
color: #bd3f49;
|
||||
color: var(--destructive);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
background: #08766e;
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary);
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: calc(var(--radius) - 2px);
|
||||
font: inherit;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
@@ -135,7 +179,7 @@ button {
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #075c57;
|
||||
background: color-mix(in oklch, var(--primary) 86%, var(--foreground));
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
@@ -148,7 +192,7 @@ footer {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 26px;
|
||||
color: #67747b;
|
||||
color: var(--muted-foreground);
|
||||
font: 8px "SFMono-Regular", Consolas, monospace;
|
||||
}
|
||||
|
||||
@@ -156,8 +200,8 @@ footer i {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: #08766e;
|
||||
box-shadow: 0 0 0 4px rgba(8, 118, 110, 0.12);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 12%, transparent);
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="oklch(0.57 0.19 258)" />
|
||||
<title>登录 · Cognitive Agent</title>
|
||||
<link rel="stylesheet" href="/login.css" />
|
||||
<link rel="stylesheet" href="/login.css?v=ai-blue-2" />
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/login.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -4,7 +4,7 @@ function nextPath() {
|
||||
}
|
||||
|
||||
async function readAuthStatus() {
|
||||
const response = await fetch("/api/auth/status", { cache: "no-store" });
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/auth/status", { cache: "no-store" });
|
||||
if (!response.ok) return;
|
||||
const status = await response.json();
|
||||
if (status.authenticated) window.location.replace(nextPath());
|
||||
@@ -18,7 +18,7 @@ async function submitLogin(event) {
|
||||
message.textContent = "";
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/auth/login", {
|
||||
const response = await window.CognitiveAgentNetwork.request("/api/auth/login", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "Cognitive Agent PAD Console",
|
||||
"short_name": "Cognitive Agent",
|
||||
"description": "车舱认知编排、模拟台架与手势控制 PAD 控制台",
|
||||
"lang": "zh-CN",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "any",
|
||||
"background_color": "oklch(0.968 0.019 244)",
|
||||
"theme_color": "oklch(0.57 0.19 258)",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/app-icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/app-icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
(function initializeCognitiveAgentNetwork() {
|
||||
const nativeFetch = window.fetch.bind(window);
|
||||
const listeners = new Set();
|
||||
const state = {
|
||||
status: navigator.onLine === false ? "offline" : "connecting",
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
};
|
||||
let probeTimer = null;
|
||||
|
||||
function snapshot() {
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
function publish(status, error = null) {
|
||||
const changed = state.status !== status || state.lastError !== error;
|
||||
state.status = status;
|
||||
state.lastError = error;
|
||||
if (status === "online") state.lastSuccessAt = new Date().toISOString();
|
||||
if (!changed) return;
|
||||
const detail = snapshot();
|
||||
listeners.forEach((listener) => listener(detail));
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:connection", { detail }));
|
||||
}
|
||||
|
||||
function sleep(milliseconds) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
async function request(input, options = {}) {
|
||||
const {
|
||||
timeoutMs = 10000,
|
||||
retries: configuredRetries,
|
||||
signal: callerSignal,
|
||||
...fetchOptions
|
||||
} = options;
|
||||
const method = String(fetchOptions.method || "GET").toUpperCase();
|
||||
const retries = configuredRetries ?? (method === "GET" ? 1 : 0);
|
||||
let lastError;
|
||||
|
||||
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
||||
const controller = new AbortController();
|
||||
const abortFromCaller = () => controller.abort(callerSignal?.reason);
|
||||
callerSignal?.addEventListener("abort", abortFromCaller, { once: true });
|
||||
const timeout = window.setTimeout(() => controller.abort("timeout"), timeoutMs);
|
||||
try {
|
||||
const response = await nativeFetch(input, {
|
||||
...fetchOptions,
|
||||
signal: controller.signal,
|
||||
});
|
||||
publish(response.status >= 500 ? "degraded" : "online",
|
||||
response.status >= 500 ? `服务返回 HTTP ${response.status}` : null);
|
||||
return response;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
const message = controller.signal.aborted && !callerSignal?.aborted
|
||||
? `请求超时(${Math.round(timeoutMs / 1000)} 秒)`
|
||||
: (error?.message || "网络请求失败");
|
||||
publish(attempt < retries ? "degraded" : "offline", message);
|
||||
if (callerSignal?.aborted || attempt >= retries || method !== "GET") {
|
||||
throw new Error(message, { cause: error });
|
||||
}
|
||||
await sleep(300 * (attempt + 1));
|
||||
} finally {
|
||||
window.clearTimeout(timeout);
|
||||
callerSignal?.removeEventListener("abort", abortFromCaller);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function probe() {
|
||||
if (document.hidden) return;
|
||||
if (navigator.onLine === false) {
|
||||
publish("offline", "设备当前处于离线状态");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await request("/api/health", {
|
||||
cache: "no-store",
|
||||
timeoutMs: 4000,
|
||||
retries: 0,
|
||||
});
|
||||
if (!response.ok && response.status !== 401) {
|
||||
publish("degraded", `健康检查返回 HTTP ${response.status}`);
|
||||
}
|
||||
} catch (_error) {
|
||||
// request() already published the detailed state.
|
||||
}
|
||||
}
|
||||
|
||||
function startMonitoring() {
|
||||
if (probeTimer !== null) return;
|
||||
probe();
|
||||
probeTimer = window.setInterval(probe, 10000);
|
||||
window.addEventListener("online", () => {
|
||||
publish("connecting");
|
||||
probe();
|
||||
});
|
||||
window.addEventListener("offline", () => publish("offline", "设备当前处于离线状态"));
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) probe();
|
||||
});
|
||||
}
|
||||
|
||||
window.CognitiveAgentNetwork = {
|
||||
request,
|
||||
probe,
|
||||
subscribe(listener) {
|
||||
listeners.add(listener);
|
||||
listener(snapshot());
|
||||
return () => listeners.delete(listener);
|
||||
},
|
||||
getState: snapshot,
|
||||
nativeFetch,
|
||||
};
|
||||
document.addEventListener("DOMContentLoaded", startMonitoring, { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@n7/cognitive-agent-web-console",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 5173",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4173"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "3.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "6.0.1",
|
||||
"vite": "7.3.6"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
(function initializePwa() {
|
||||
function notify(message) {
|
||||
window.CognitiveAgentConsole?.notify?.(message);
|
||||
}
|
||||
|
||||
function showUpdate(registration) {
|
||||
notify("检测到前端更新,正在刷新页面");
|
||||
registration.waiting?.postMessage({ type: "SKIP_WAITING" });
|
||||
}
|
||||
|
||||
async function register() {
|
||||
if (!("serviceWorker" in navigator) || !window.isSecureContext) return;
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register("/service-worker.js", {
|
||||
scope: "/",
|
||||
updateViaCache: "none",
|
||||
});
|
||||
await registration.update();
|
||||
if (registration.waiting) showUpdate(registration);
|
||||
registration.addEventListener("updatefound", () => {
|
||||
const worker = registration.installing;
|
||||
worker?.addEventListener("statechange", () => {
|
||||
if (worker.state === "installed" && navigator.serviceWorker.controller) {
|
||||
showUpdate(registration);
|
||||
}
|
||||
});
|
||||
});
|
||||
navigator.serviceWorker.addEventListener("controllerchange", () => window.location.reload());
|
||||
navigator.serviceWorker.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "OFFLINE_READY") notify("PAD 离线资源已准备完成");
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("PWA registration failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("load", register, { once: true });
|
||||
})();
|
||||
@@ -0,0 +1,113 @@
|
||||
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-24-dialog-size",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/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-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-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-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",
|
||||
"/icons/app-icon-192.png",
|
||||
"/icons/app-icon-512.png",
|
||||
"/vendor/lucide/0.468.0/lucide.min.js",
|
||||
"/vendor/vue/3.5.21/vue.runtime.global.prod.js",
|
||||
"/vendor/three/0.147.0/three.min.js",
|
||||
"/vendor/three/0.147.0/fflate.min.js",
|
||||
"/vendor/three/0.147.0/FBXLoader.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/vision_bundle.mjs",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_internal.wasm",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.js",
|
||||
"/vendor/mediapipe/tasks-vision/0.10.14/wasm/vision_wasm_nosimd_internal.wasm"
|
||||
];
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(SHELL_CACHE)
|
||||
.then((cache) => cache.addAll(APP_SHELL))
|
||||
.then(() => self.skipWaiting())
|
||||
.then(() => self.clients.matchAll({ includeUncontrolled: true }))
|
||||
.then((clients) => clients.forEach((client) => client.postMessage({ type: "OFFLINE_READY" })))
|
||||
);
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys()
|
||||
.then((keys) => Promise.all(
|
||||
keys.filter((key) => key.startsWith("cognitive-agent-pad-")
|
||||
&& ![SHELL_CACHE, RUNTIME_CACHE].includes(key))
|
||||
.map((key) => caches.delete(key))
|
||||
))
|
||||
.then(() => self.clients.claim())
|
||||
);
|
||||
});
|
||||
|
||||
async function cacheFirst(request) {
|
||||
const shellCache = await caches.open(SHELL_CACHE);
|
||||
const runtimeCache = await caches.open(RUNTIME_CACHE);
|
||||
const cached = (await shellCache.match(request)) || (await runtimeCache.match(request));
|
||||
if (cached) return cached;
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
await runtimeCache.put(request, response.clone());
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async function navigationResponse(request) {
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(RUNTIME_CACHE);
|
||||
await cache.put("/index.html", response.clone());
|
||||
}
|
||||
return response;
|
||||
} catch (_error) {
|
||||
const shellCache = await caches.open(SHELL_CACHE);
|
||||
const runtimeCache = await caches.open(RUNTIME_CACHE);
|
||||
return (await runtimeCache.match("/index.html"))
|
||||
|| (await shellCache.match("/index.html"))
|
||||
|| Response.error();
|
||||
}
|
||||
}
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
const url = new URL(event.request.url);
|
||||
if (url.origin !== self.location.origin || event.request.method !== "GET") return;
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(navigationResponse(event.request));
|
||||
return;
|
||||
}
|
||||
if (url.pathname.startsWith("/api/") && url.pathname !== "/api/gesture/model") return;
|
||||
event.respondWith(cacheFirst(event.request));
|
||||
});
|
||||
|
||||
self.addEventListener("message", (event) => {
|
||||
if (event.data?.type === "SKIP_WAITING") self.skipWaiting();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
class SpeechCaptureProcessor extends AudioWorkletProcessor {
|
||||
constructor() {
|
||||
super();
|
||||
this.frameSize = 2048;
|
||||
this.frame = new Float32Array(this.frameSize);
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
process(inputs) {
|
||||
const input = inputs[0]?.[0];
|
||||
if (!input) return true;
|
||||
let inputOffset = 0;
|
||||
while (inputOffset < input.length) {
|
||||
const available = Math.min(this.frameSize - this.offset, input.length - inputOffset);
|
||||
this.frame.set(input.subarray(inputOffset, inputOffset + available), this.offset);
|
||||
this.offset += available;
|
||||
inputOffset += available;
|
||||
if (this.offset === this.frameSize) {
|
||||
const ready = this.frame;
|
||||
this.port.postMessage(ready, [ready.buffer]);
|
||||
this.frame = new Float32Array(this.frameSize);
|
||||
this.offset = 0;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor("speech-capture-processor", SpeechCaptureProcessor);
|
||||
@@ -0,0 +1,298 @@
|
||||
.speech-visually-hidden {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Keep all composer actions on one calm, optically aligned rail. */
|
||||
.console-v2 .v2-request-form .speech-dictation {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 56px;
|
||||
bottom: auto;
|
||||
z-index: 13;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--foreground);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
transform: translateY(-50%);
|
||||
transition: color 160ms ease, background 160ms ease, box-shadow 160ms ease,
|
||||
transform 160ms ease;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation:hover:not(:disabled),
|
||||
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--background));
|
||||
box-shadow: 0 0 0 3px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation:disabled {
|
||||
color: var(--muted-foreground);
|
||||
cursor: not-allowed;
|
||||
opacity: 0.56;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation .speech-mic-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-waveform {
|
||||
height: 16px;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-waveform i {
|
||||
width: 2px;
|
||||
height: 5px;
|
||||
display: block;
|
||||
background: currentColor;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-waveform i:nth-child(2),
|
||||
.console-v2 .v2-request-form .speech-waveform i:nth-child(4) {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-waveform i:nth-child(3) {
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening,
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening:hover:not(:disabled),
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening:focus-visible {
|
||||
color: var(--primary-foreground);
|
||||
background: var(--primary);
|
||||
box-shadow: 0 0 0 5px color-mix(in oklch, var(--primary) 9%, transparent), 0 7px 18px color-mix(in oklch, var(--primary) 20%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-armed,
|
||||
.console-v2 .v2-request-form .speech-dictation.is-armed:hover:not(:disabled),
|
||||
.console-v2 .v2-request-form .speech-dictation.is-armed:focus-visible {
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--background));
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-armed::after {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 3px;
|
||||
content: "";
|
||||
background: var(--primary);
|
||||
border: 2px solid var(--background);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-mic-icon,
|
||||
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-mic-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform,
|
||||
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i {
|
||||
animation: speech-wave 760ms ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(2) {
|
||||
animation-delay: -420ms;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(3) {
|
||||
animation-delay: -180ms;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-listening .speech-waveform i:nth-child(4) {
|
||||
animation-delay: -560ms;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-processing {
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--background));
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation.is-processing .speech-waveform {
|
||||
animation: speech-processing-breathe 900ms ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-status {
|
||||
min-height: 16px;
|
||||
position: absolute;
|
||||
right: 140px;
|
||||
bottom: 10px;
|
||||
left: 194px;
|
||||
z-index: 12;
|
||||
overflow: hidden;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 10px;
|
||||
font-weight: 650;
|
||||
line-height: 16px;
|
||||
letter-spacing: 0.01em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] {
|
||||
border-color: color-mix(in oklch, var(--primary) 36%, transparent);
|
||||
box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="armed"],
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="capturing"] {
|
||||
border-color: color-mix(in oklch, var(--primary) 36%, transparent);
|
||||
box-shadow: 0 12px 35px color-mix(in oklch, var(--primary) 10%, transparent), inset 0 0 0 1px color-mix(in oklch, var(--primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="listening"] .speech-status {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="armed"] .speech-status,
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="capturing"] .speech-status {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="processing"] .speech-status {
|
||||
color: color-mix(in oklch, var(--primary) 72%, var(--foreground));
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="warming"] .speech-status,
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="arming"] .speech-status,
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="finalizing"] .speech-status {
|
||||
color: color-mix(in oklch, var(--primary) 72%, var(--foreground));
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="success"] .speech-status {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form #utterance[data-speech-uncertain="true"] {
|
||||
background: linear-gradient(90deg, color-mix(in oklch, var(--chart-1) 10%, transparent), transparent 42%);
|
||||
box-shadow: inset 3px 0 0 color-mix(in oklch, var(--chart-1) 58%, transparent);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form #utterance[data-speech-stage="partial"] {
|
||||
color: color-mix(in oklch, var(--foreground) 82%, var(--muted-foreground));
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer[data-speech-state="error"] .speech-status {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) .composer-hint {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form #utterance,
|
||||
.console-v2.v2-interactive-sync .v2-request-form #utterance,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form #utterance {
|
||||
padding-right: 142px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .context-reset,
|
||||
.console-v2.v2-interactive-sync .v2-request-form .context-reset,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .context-reset {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
top: 50%;
|
||||
right: 98px;
|
||||
bottom: auto;
|
||||
color: var(--muted-foreground);
|
||||
border-radius: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .context-reset:hover,
|
||||
.console-v2 .v2-request-form .context-reset:focus-visible {
|
||||
color: var(--primary);
|
||||
background: color-mix(in oklch, var(--primary) 8%, var(--background));
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .composer-hint,
|
||||
.console-v2.v2-interactive-sync .v2-request-form .composer-hint,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .composer-hint {
|
||||
right: 140px;
|
||||
}
|
||||
|
||||
@keyframes speech-wave {
|
||||
from { transform: scaleY(0.42); }
|
||||
to { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
@keyframes speech-processing-breathe {
|
||||
from { opacity: 0.42; transform: scaleX(0.78); }
|
||||
to { opacity: 1; transform: scaleX(1); }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.console-v2 .v2-request-form .speech-status {
|
||||
top: 8px;
|
||||
right: 140px;
|
||||
bottom: auto;
|
||||
left: 14px;
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .command-composer:not([data-speech-state="idle"]) #utterance {
|
||||
padding-top: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
.console-v2 .v2-request-form #utterance,
|
||||
.console-v2.v2-interactive-sync .v2-request-form #utterance,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form #utterance {
|
||||
padding-right: 132px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .speech-dictation {
|
||||
right: 52px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-request-form .context-reset,
|
||||
.console-v2.v2-interactive-sync .v2-request-form .context-reset,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-request-form .context-reset {
|
||||
right: 92px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.console-v2 .v2-request-form .speech-dictation,
|
||||
.console-v2 .v2-request-form .speech-waveform,
|
||||
.console-v2 .v2-request-form .speech-waveform i {
|
||||
animation: none !important;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,584 @@
|
||||
<script setup>
|
||||
defineOptions({ name: "N7ConsoleApp" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="v2-boot-screen" id="v2BootScreen" role="status" aria-live="polite">
|
||||
<span class="v2-boot-mark" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
<strong>511工程-智能座舱</strong>
|
||||
<small>正在加载车机控制与实时状态</small>
|
||||
</div>
|
||||
<div class="workspace">
|
||||
<aside class="command-panel">
|
||||
<div class="panel-heading">
|
||||
<h2>说一句,就调好了</h2>
|
||||
</div>
|
||||
|
||||
<div class="mode-rail" aria-label="功能模式">
|
||||
<div class="mode-rail-bar">
|
||||
<div class="mode-rail-identity">
|
||||
<span class="mode-rail-mark" aria-hidden="true"><i data-lucide="sparkles"></i></span>
|
||||
<span><strong class="mode-rail-title">功能模式</strong><small>预设与我的模式</small></span>
|
||||
</div>
|
||||
<div class="mode-rail-content" id="modeRailContent" role="region" aria-label="功能模式列表">
|
||||
<div class="mode-chips" tabindex="0" aria-label="快捷功能模式,可横向滚动" aria-live="polite">
|
||||
<button type="button" data-mode-profile-id="zero_gravity" data-mode-origin="preset" data-mode-command="进入零重力模式"><i data-lucide="orbit"></i><span>零重力</span></button>
|
||||
<button type="button" data-mode-profile-id="office" data-mode-origin="preset" data-mode-command="进入办公模式"><i data-lucide="briefcase"></i><span>办公</span></button>
|
||||
<button type="button" data-mode-profile-id="nap" data-mode-origin="preset" data-mode-command="进入小憩模式"><i data-lucide="moon-star"></i><span>小憩</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mode-rail-actions">
|
||||
<button class="mode-gallery-toggle" id="modeGalleryToggle" type="button" aria-expanded="false" aria-controls="modeGallery"><i data-lucide="settings-2" data-icon="inline-start"></i><span>管理</span><strong id="modeGalleryCount">0</strong></button>
|
||||
<button class="mode-create-toggle" id="saveModeToggle" type="button"><i data-lucide="sparkles" data-icon="inline-start"></i><span>AI 新建</span></button>
|
||||
<button class="mode-rail-toggle" id="modeRailToggle" type="button" aria-expanded="true" aria-controls="modeRailContent" aria-label="收起功能模式" title="收起功能模式">
|
||||
<span class="visually-hidden" id="modeRailSelection">静态模式</span><i data-lucide="chevron-up"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="requestForm" autocomplete="off">
|
||||
<section class="assistant-transcript" id="assistantTranscript" role="log" aria-live="polite" aria-label="AI助手对话">
|
||||
<div class="assistant-messages" id="assistantMessages">
|
||||
<article class="assistant-message assistant-message-assistant">
|
||||
<span class="assistant-avatar" aria-hidden="true"><i data-lucide="waypoints"></i></span>
|
||||
<div class="assistant-message-body">
|
||||
<div class="assistant-message-content">你好,我是 AI助手。你可以向我提问,也可以直接交代需要完成的工作。</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<div class="field-label-row">
|
||||
<label for="utterance">向 AI助手提问或交代工作</label>
|
||||
<button class="context-reset" id="newConversation" type="button" title="开始新会话" aria-label="开始新会话">
|
||||
<i data-lucide="message-square-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="command-composer">
|
||||
<textarea id="utterance" name="utterance" maxlength="4000" autocomplete="off" placeholder="向 AI助手提问或交代工作"></textarea>
|
||||
<button class="speech-dictation" id="speechDictationButton" type="button" aria-label="开始语音输入" aria-pressed="false" title="开始语音输入">
|
||||
<i class="speech-mic-icon" data-lucide="mic" aria-hidden="true"></i>
|
||||
<span class="speech-waveform" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span>
|
||||
<span class="speech-visually-hidden">开始语音输入</span>
|
||||
</button>
|
||||
<span class="speech-status" id="speechDictationStatus" role="status" aria-live="polite"></span>
|
||||
<span class="composer-hint">Enter 发送 · Shift + Enter 换行</span>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">意图解析器</span>
|
||||
<div class="segmented" role="group" aria-label="意图解析器">
|
||||
<button type="button" class="segment active" data-provider="openai">
|
||||
<i data-lucide="sparkles"></i>GPT
|
||||
</button>
|
||||
<button type="button" class="segment" data-provider="local">
|
||||
<i data-lucide="braces"></i>本地规则
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="providerNote">正在读取运行时配置</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="reasoningField">
|
||||
<span class="field-label">模型推理强度</span>
|
||||
<div class="segmented reasoning-segmented" role="group" aria-label="模型推理强度">
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="low">
|
||||
<i data-lucide="zap"></i><span><strong>极速</strong><small>简单指令,优先响应速度</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option active" data-reasoning-effort="medium">
|
||||
<i data-lucide="scale"></i><span><strong>均衡</strong><small>日常控制,兼顾速度与理解</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
<button type="button" class="segment reasoning-option" data-reasoning-effort="high">
|
||||
<i data-lucide="brain"></i><span><strong>深度</strong><small>复杂联动、相对动作与约束</small></span><i class="reasoning-check" data-lucide="check"></i>
|
||||
</button>
|
||||
</div>
|
||||
<p class="provider-note" id="reasoningNote">均衡模式兼顾自然语言解析精度与响应速度</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<span class="field-label">执行目标</span>
|
||||
<div class="segmented execution-mode-options" role="group" aria-label="执行目标">
|
||||
<button type="button" class="mode-option" data-execution-mode="dry_run">
|
||||
<i data-lucide="flask-conical"></i>模拟下发
|
||||
</button>
|
||||
<button type="button" class="mode-option active" id="hardwareMode" data-execution-mode="hardware">
|
||||
<i data-lucide="radio-tower"></i>真实下发
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group" id="vehicleTransportField">
|
||||
<span class="field-label">车辆连接方式</span>
|
||||
<div class="segmented vehicle-transport-options" role="group" aria-label="车辆连接方式">
|
||||
<button type="button" class="mode-option active" data-control-transport="wired_can">
|
||||
<i data-lucide="cable"></i>有线 CAN
|
||||
</button>
|
||||
<button type="button" class="mode-option" data-control-transport="wireless_mqtt">
|
||||
<i data-lucide="wifi"></i>无线 CAN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-group simulation-fault-field" id="simulationFaultField" hidden>
|
||||
<label class="field-label" for="simulationFault">故障注入场景</label>
|
||||
<select id="simulationFault" aria-label="选择模拟硬件故障">
|
||||
<option value="normal">正常反馈</option>
|
||||
<option value="feedback_timeout">反馈超时</option>
|
||||
<option value="adapter_disconnect">适配器断开</option>
|
||||
<option value="motor_stall">电机堵转</option>
|
||||
<option value="firmware_mismatch">固件不匹配</option>
|
||||
<option value="partial_feedback">部分反馈</option>
|
||||
</select>
|
||||
<p class="provider-note">仅生成模拟 HIL 结果,不会打开物理 CAN 适配器或调用 IAR。</p>
|
||||
</div>
|
||||
|
||||
<div class="field-group vehicle-field">
|
||||
<label class="field-label" for="vehicleSelect">车型配置</label>
|
||||
<div class="vehicle-select-control">
|
||||
<i data-lucide="car-front"></i>
|
||||
<select id="vehicleSelect" name="vehicle_id" aria-label="选择车型配置">
|
||||
<option value="">加载车型配置</option>
|
||||
</select>
|
||||
</div>
|
||||
<p class="provider-note" id="vehicleNote">正在读取座椅量程与角度标定</p>
|
||||
</div>
|
||||
|
||||
<button class="primary-action" id="runButton" type="submit" aria-label="发送消息">
|
||||
<i data-lucide="send-horizontal"></i>
|
||||
<span>发送</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</aside>
|
||||
|
||||
<main class="main-panel">
|
||||
<section class="request-header">
|
||||
<div>
|
||||
<div class="request-title-row">
|
||||
<h2 id="resultTitle">等待请求</h2>
|
||||
<span class="run-status idle" id="runStatus">IDLE</span>
|
||||
</div>
|
||||
<p id="requestMeta">Cabin / reference_vehicle / dry-run</p>
|
||||
</div>
|
||||
<div class="matrix-stat">
|
||||
<i data-lucide="table-properties"></i>
|
||||
<div><strong id="matrixCount">10 / 57</strong><span>报文 / 信号</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="cabin-band" aria-labelledby="cabinStateTitle">
|
||||
<div class="section-title-row cabin-heading">
|
||||
<div class="cabin-title-copy">
|
||||
<h3 id="cabinStateTitle">整车设备状态</h3>
|
||||
</div>
|
||||
<div class="cabin-channel-legend" aria-label="整车设备通道说明">
|
||||
<span data-channel="ch0"><i>CH0</i>副驾座椅</span>
|
||||
<span data-channel="ch1"><i>CH1</i>主驾 / 车身</span>
|
||||
<span class="simulation-label" id="feedbackModeLabel">MATRIX-BACKED · SIMULATED FEEDBACK</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="domain-grid">
|
||||
<article class="domain-panel climate-panel" data-domain-panel="climate" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="fan"></i></span>
|
||||
<div><strong>空调</strong><small>CMD 0x1DE · FB 0x404 · CH1</small></div>
|
||||
<span class="device-state" id="climatePower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>电源 · 温度 · 风量/风向</p>
|
||||
<div class="climate-display">
|
||||
<div class="zone-temperature">
|
||||
<span>主驾</span><strong id="driverTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="driverAirDirection">-</b></small>
|
||||
</div>
|
||||
<div class="airflow-visual" id="airflowVisual" data-level="0" aria-label="空调风量">
|
||||
<i></i><i></i><i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<div class="zone-temperature passenger-temperature">
|
||||
<span>副驾</span><strong id="passengerTemperature">--<sup>℃</sup></strong>
|
||||
<small>风向<b id="passengerAirDirection">-</b></small>
|
||||
</div>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>风量</dt><dd id="fanLevel">未知</dd></div>
|
||||
<div><dt>循环</dt><dd id="cycleMode">未知</dd></div>
|
||||
<div><dt>A/C</dt><dd id="acState">未知</dd></div>
|
||||
<div><dt>前除霜</dt><dd id="defrostState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel vehicle-status-panel" data-domain-panel="vehicle_status" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gauge"></i></span>
|
||||
<div><strong>车辆状态</strong><small>FB 0x342 · 0x4EB · 0x407 · CH1</small></div>
|
||||
<span class="device-state readonly-state" id="vehicleTelemetryState">只读</span>
|
||||
</header>
|
||||
<p class="domain-capability-line readonly-capability"><span>只读信号</span>档位 · 车速 · 四门锁状态</p>
|
||||
<div class="vehicle-primary-readout">
|
||||
<div><span>当前档位</span><strong id="vehicleGear">--</strong></div>
|
||||
<div><span>实时车速</span><strong id="vehicleSpeed">-- km/h</strong></div>
|
||||
</div>
|
||||
<div class="door-state-grid" aria-label="四门锁状态">
|
||||
<span data-door-state="front_left"><i></i>左前门<b id="frontLeftDoorState">未知</b></span>
|
||||
<span data-door-state="front_right"><i></i>右前门<b id="frontRightDoorState">未知</b></span>
|
||||
<span data-door-state="rear_left"><i></i>左后门<b id="rearLeftDoorState">未知</b></span>
|
||||
<span data-door-state="rear_right"><i></i>右后门<b id="rearRightDoorState">未知</b></span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel roof-panel" data-domain-panel="roof" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="panels-top-left"></i></span>
|
||||
<div><strong>遮阳帘</strong><small>CMD 0x431 · FB 0x41A · CH1</small></div>
|
||||
<span class="device-state" id="roofState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>开关 / 停止 · 位置</p>
|
||||
<div class="domain-summary-visual shade-summary-visual" aria-hidden="true">
|
||||
<span>闭合</span><i><b id="shadePositionMeter"></b></i><span>打开</span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>遮阳帘</dt><dd id="shadePositionValue">未知</dd></div>
|
||||
<div><dt>动作</dt><dd id="shadeMotionState">未知</dd></div>
|
||||
<div><dt>初始化</dt><dd id="shadeInitializedState">未知</dd></div>
|
||||
<div><dt>热保护</dt><dd id="shadeThermalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel ambient-panel" data-domain-panel="ambient_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="lamp-ceiling"></i></span>
|
||||
<div><strong>车内灯光</strong><small>INTERIOR / AMBIENT · CH1</small></div>
|
||||
<span class="device-state" id="ambientPower">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>阅读灯 · 氛围亮度 · 色号</p>
|
||||
<div class="domain-summary-visual ambient-summary-visual" aria-hidden="true">
|
||||
<span data-ambient-zone="front"><i id="ambientFrontMeter"></i><b>前排</b></span>
|
||||
<span data-ambient-zone="rear"><i id="ambientRearMeter"></i><b>后排</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>氛围灯</dt><dd id="ambientMode">未知</dd></div>
|
||||
<div><dt>室内/阅读灯</dt><dd id="interiorLightPower">未知</dd></div>
|
||||
<div><dt>氛围亮度</dt><dd id="ambientBrightness">- / -</dd></div>
|
||||
<div><dt>氛围色号</dt><dd id="ambientColorIndex">- / -</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel window-panel" data-domain-panel="window" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="gallery-vertical-end"></i></span>
|
||||
<div><strong>车窗</strong><small>CMD 0x431 · FB 0x388 · CH1</small></div>
|
||||
<span class="device-state" id="windowState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>四窗独立 · 开/关/停 · 位置</p>
|
||||
<div class="domain-summary-visual window-summary-visual" aria-hidden="true">
|
||||
<span data-window-meter="front_left"><i></i><b>左前</b></span>
|
||||
<span data-window-meter="front_right"><i></i><b>右前</b></span>
|
||||
<span data-window-meter="rear_left"><i></i><b>左后</b></span>
|
||||
<span data-window-meter="rear_right"><i></i><b>右后</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>左前窗</dt><dd id="frontLeftWindow">未知</dd></div>
|
||||
<div><dt>右前窗</dt><dd id="frontRightWindow">未知</dd></div>
|
||||
<div><dt>左后窗</dt><dd id="rearLeftWindow">未知</dd></div>
|
||||
<div><dt>右后窗</dt><dd id="rearRightWindow">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
<article class="domain-panel lighting-panel" data-domain-panel="front_light" data-device-channel="ch1">
|
||||
<header>
|
||||
<span class="domain-icon"><i data-lucide="car-front"></i></span>
|
||||
<div><strong>前大灯</strong><small>FB 0x38A · CH1</small></div>
|
||||
<span class="device-state" id="frontLightState">UNKNOWN</span>
|
||||
</header>
|
||||
<p class="domain-capability-line"><span>可控参数</span>近/远光 · 雾灯 · 转向灯</p>
|
||||
<div class="domain-summary-visual lighting-summary-visual" aria-hidden="true">
|
||||
<span data-light-meter="lowBeamState"><i></i><b>近光</b></span>
|
||||
<span data-light-meter="highBeamState"><i></i><b>远光</b></span>
|
||||
<span data-light-meter="fogLightState"><i></i><b>雾灯</b></span>
|
||||
<span data-light-meter="turnSignalState"><i></i><b>转向</b></span>
|
||||
</div>
|
||||
<dl class="domain-readouts">
|
||||
<div><dt>近光灯</dt><dd id="lowBeamState">未知</dd></div>
|
||||
<div><dt>远光灯</dt><dd id="highBeamState">未知</dd></div>
|
||||
<div><dt>雾灯</dt><dd id="fogLightState">未知</dd></div>
|
||||
<div><dt>转向灯</dt><dd id="turnSignalState">未知</dd></div>
|
||||
</dl>
|
||||
</article>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="twin-band" aria-labelledby="twinTitle">
|
||||
<div class="twin-heading">
|
||||
<div class="twin-title-copy">
|
||||
<h3 id="twinTitle">整车孪生界面</h3>
|
||||
<div class="preset-indicator" id="presetIndicator" hidden>
|
||||
<i data-lucide="bookmark-check"></i><span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-toolbar">
|
||||
<div class="twin-selector" role="tablist" aria-label="前排座椅孪生选择">
|
||||
<button class="twin-seat-button active" type="button" role="tab" aria-selected="true" data-twin-seat="passenger">副驾座椅 · CH0</button>
|
||||
<button class="twin-seat-button" type="button" role="tab" aria-selected="false" data-twin-seat="driver">主驾座椅 · CH1</button>
|
||||
</div>
|
||||
<button class="icon-button twin-reset" id="twinReset" type="button" title="复位 3D 视角" aria-label="复位 3D 视角">
|
||||
<i data-lucide="rotate-3d"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="twin-stage" id="seatTwin" data-selected-seat="passenger">
|
||||
<canvas id="seatTwinCanvas" data-vehicle-model="byd-tang-2024-reference" data-seat-layout="five-seat" data-active-seat="front-right" aria-label="通过模型动画展示 CH0 副驾座椅与 CH1 主驾座椅、空调气流、智能天幕、阅读灯、四门车窗与前大灯状态的整车数字孪生"></canvas>
|
||||
<div class="twin-seat-label passenger"><i></i><span>副驾座椅</span><small>CH0 · 右前排</small></div>
|
||||
<div class="twin-seat-label driver"><i></i><span>主驾座椅</span><small>CH1 · 左前排</small></div>
|
||||
<div class="twin-orientation"><i data-lucide="move-down"></i><span>车头方向</span></div>
|
||||
<aside class="twin-readout" aria-live="polite">
|
||||
<div class="twin-readout-heading">
|
||||
<div><strong id="twinSeatName">副驾座椅</strong><span id="twinSeatZone">CH0 · 右前排</span></div>
|
||||
<span class="twin-feedback"><i></i>FEEDBACK</span>
|
||||
</div>
|
||||
<div class="twin-axis-list">
|
||||
<div data-twin-axis="longitudinal"><span>水平位置</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="cushion_tilt"><span>坐垫俯仰</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="height"><span>座椅高度</span><b>50%</b><i></i></div>
|
||||
<div data-twin-axis="backrest_recline"><span>靠背夹角</span><b>20% · 104°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_height"><span>腿托夹角</span><b>0% · 95°</b><i></i></div>
|
||||
<div data-twin-axis="legrest_extension"><span>腿托伸缩</span><b>0%</b><i></i></div>
|
||||
</div>
|
||||
<div class="twin-thermal-row" aria-label="座椅热舒适状态">
|
||||
<span data-twin-function="ventilation" data-level="0">
|
||||
<i data-lucide="wind"></i>
|
||||
<span class="twin-thermal-copy"><small>通风</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
<span data-twin-function="heating" data-level="0">
|
||||
<i data-lucide="flame"></i>
|
||||
<span class="twin-thermal-copy"><small>加热</small><b>关闭</b></span>
|
||||
<span class="twin-level-meter" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="twin-fallback" id="twinFallback"><i data-lucide="loader-circle"></i><span>正在加载整车参考模型</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="state-band">
|
||||
<div class="seat-state-section">
|
||||
<div class="section-title-row">
|
||||
<div><h3>前排座椅状态</h3></div>
|
||||
<span class="simulation-label" id="seatFeedbackModeLabel">CH0 / CH1 FEEDBACK</span>
|
||||
</div>
|
||||
<div class="seat-grid">
|
||||
<article class="seat-card" data-seat="passenger">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>副驾座椅</strong><span>右前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="passenger">CH0 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="waves"></i><span>动态模式</span><b>未运行</b>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
<article class="seat-card" data-seat="driver">
|
||||
<div class="seat-identity"><i data-lucide="armchair"></i><div><strong>主驾座椅</strong><span>左前排</span></div></div>
|
||||
<span class="seat-channel-badge" data-seat-feedback="driver">CH1 · 等待反馈</span>
|
||||
<div class="dynamic-mode-state" data-dynamic-mode>
|
||||
<i data-lucide="radio-tower"></i><span>车端 CAN FD</span><b>只读同步</b>
|
||||
</div>
|
||||
<div class="thermal-states">
|
||||
<span data-function="ventilation"><i data-lucide="wind"></i>通风 <b>--</b></span>
|
||||
<span data-function="heating"><i data-lucide="flame"></i>加热 <b>--</b></span>
|
||||
<span data-function="massage"><i data-lucide="waves"></i>按摩 <b>--</b></span>
|
||||
</div>
|
||||
<div class="motion-state-grid" aria-live="polite"></div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coverage-section">
|
||||
<h3>当前控制边界</h3>
|
||||
<dl>
|
||||
<div><dt>空调 18–32℃ / 0–7 档</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>天幕 / 遮阳帘 0–100%</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>氛围灯 RGB / 亮度</dt><dd class="available">可编译</dd></div>
|
||||
<div><dt>副驾座椅统一执行器</dt><dd class="simulated">t2g_primary_seat</dd></div>
|
||||
<div><dt>主驾座椅车端执行器</dt><dd class="simulated">CH1 · 左前排</dd></div>
|
||||
<div><dt>真实台架 CAN 下发</dt><dd class="blocked" id="hardwareCoverage">未配置</dd></div>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="detail-section result-workspace" aria-labelledby="resultWorkspaceTitle">
|
||||
<header class="result-workspace-heading">
|
||||
<div><h3 id="resultWorkspaceTitle">编排结果工作台</h3></div>
|
||||
<span class="result-workspace-badge"><i data-lucide="route"></i>REQUEST → CONFIG → CAN → RESULT</span>
|
||||
</header>
|
||||
<nav class="tabs result-nav" aria-label="结果快速导航">
|
||||
<button class="tab active" type="button" data-tab="trace"><span class="tab-index">01</span><i data-lucide="list-tree"></i><span class="tab-copy"><strong>决策轨迹</strong><small id="traceNavMeta">等待请求</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="config"><span class="tab-index">02</span><i data-lucide="layout-grid"></i><span class="tab-copy"><strong>配置卡片</strong><small id="configNavMeta">等待生成</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="plan"><span class="tab-index">03</span><i data-lucide="binary"></i><span class="tab-copy"><strong>CAN 计划</strong><small id="planNavMeta">等待编译</small></span><i data-lucide="arrow-down"></i></button>
|
||||
<button class="tab" type="button" data-tab="result"><span class="tab-index">04</span><i data-lucide="circle-check-big"></i><span class="tab-copy"><strong>执行结果</strong><small id="resultNavMeta">尚未执行</small></span><i data-lucide="arrow-down"></i></button>
|
||||
</nav>
|
||||
|
||||
<div class="tab-panel active" id="panel-trace" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>决策轨迹与校验</h3></div><span class="workspace-panel-state" id="tracePanelState">IDLE</span></div>
|
||||
<div class="trace-layout">
|
||||
<div>
|
||||
<div class="detail-heading"><h3>可审计决策摘要</h3><span id="providerUsed">--</span></div>
|
||||
<ol class="decision-list" id="decisionList"><li>提交请求后显示识别目标、能力选择和约束来源。</li></ol>
|
||||
</div>
|
||||
<div>
|
||||
<div class="detail-heading"><h3>阶段事件</h3><span id="traceDuration">0 ms</span></div>
|
||||
<div class="trace-list" id="traceList"><div class="empty-line">暂无执行事件</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="validation-block">
|
||||
<div class="detail-heading"><h3>校验结果</h3><span id="validationCount">0 条</span></div>
|
||||
<div id="validationList" class="validation-list"><div class="empty-line">等待配置生成</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel config-panel" id="panel-config" role="tabpanel">
|
||||
<div class="config-panel-heading">
|
||||
<div><h3>指令配置与硬件映射</h3></div>
|
||||
<div class="config-actions"><button class="config-action" type="button" data-copy="config"><i data-lucide="copy"></i>复制 YAML</button><button class="config-action" type="button" data-download="config"><i data-lucide="download"></i>下载配置</button></div>
|
||||
</div>
|
||||
<div class="config-card-grid" id="configCards">
|
||||
<div class="config-empty"><i data-lucide="panel-top-dashed"></i><strong>等待生成配置</strong><span>提交车舱控制指令后,这里会显示控制对象、目标参数和硬件信号。</span></div>
|
||||
</div>
|
||||
<details class="config-source-details">
|
||||
<summary><span><i data-lucide="braces"></i>查看原始配置</span><small id="configFileName">functions/seat.yaml</small></summary>
|
||||
<pre id="configCode"><code># 等待生成 FunctionConfig</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel plan-panel" id="panel-plan" role="tabpanel">
|
||||
<div class="workspace-panel-title plan-panel-heading"><div><h3>CAN 编译计划</h3></div><button class="config-action" type="button" data-copy="plan"><i data-lucide="copy"></i>复制计划</button></div>
|
||||
<div class="frame-summary" id="frameSummary"><span>Frame ID</span><b>--</b><span>Payload</span><code>--</code><span>Mode</span><b>DRY-RUN</b></div>
|
||||
<details class="config-source-details plan-source-details">
|
||||
<summary><span><i data-lucide="binary"></i>查看原始 CAN 计划</span><small id="planFileName">command-plans/seat.yaml</small></summary>
|
||||
<pre id="planCode"><code># 等待 CAN 编译</code></pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel result-panel" id="panel-result" role="tabpanel">
|
||||
<div class="workspace-panel-title"><div><h3>执行结果与硬件回读</h3></div><span class="workspace-panel-state" id="resultPanelState">IDLE</span></div>
|
||||
<div class="execution-summary" id="executionSummary">
|
||||
<i data-lucide="circle-dashed"></i><div><strong>尚未执行</strong><span>运行结果将在此处显示</span></div>
|
||||
</div>
|
||||
<div class="result-list" id="resultList"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<aside class="gesture-drawer" id="gestureDrawer" aria-hidden="true" aria-labelledby="gestureDrawerTitle">
|
||||
<header class="gesture-drawer-header">
|
||||
<div>
|
||||
<h2 id="gestureDrawerTitle">手势控制</h2>
|
||||
</div>
|
||||
<button class="gesture-close" id="gestureCloseButton" type="button" aria-label="关闭手势控制"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
|
||||
<div class="gesture-status-strip">
|
||||
<span class="gesture-state-pill idle" id="gestureStatePill"><i></i><b>未启动</b></span>
|
||||
<span id="gestureSafetyText">摄像头尚未开启</span>
|
||||
</div>
|
||||
|
||||
<div class="gesture-camera-stage">
|
||||
<video id="gestureVideo" playsinline muted></video>
|
||||
<canvas id="gestureCanvas"></canvas>
|
||||
<div class="gesture-camera-empty" id="gestureCameraEmpty">
|
||||
<span><i data-lucide="scan-face"></i></span>
|
||||
<strong>用手势控制副驾驶座椅</strong>
|
||||
<p>画面只在本机处理,不上传视频。</p>
|
||||
</div>
|
||||
<div class="gesture-live-badge" id="gestureLiveBadge" hidden><i></i>LIVE</div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-now">
|
||||
<div><span>当前手势</span><strong id="gestureDetected">—</strong></div>
|
||||
<i data-lucide="arrow-right"></i>
|
||||
<div><span>期望动作</span><strong id="gestureExpected">等待启动</strong></div>
|
||||
</div>
|
||||
|
||||
<div class="gesture-progress"><i id="gestureProgressBar"></i></div>
|
||||
|
||||
<div class="gesture-actions">
|
||||
<button class="gesture-primary" id="gestureStartButton" type="button"><i data-lucide="camera"></i><span>开启摄像头</span></button>
|
||||
<button id="gestureStopButton" type="button" disabled><i data-lucide="octagon"></i>停止</button>
|
||||
</div>
|
||||
|
||||
<section class="gesture-guide-card">
|
||||
<div class="gesture-card-heading"><div><span>手势速查</span><small>张掌激活 · 握拳只停止</small></div><button id="gestureCustomizeButton" type="button"><i data-lucide="sliders-horizontal"></i>自定义</button></div>
|
||||
<div class="gesture-map-grid" id="gestureMapGrid"></div>
|
||||
<div class="gesture-customize" id="gestureCustomizePanel" hidden>
|
||||
<div><span>识别到</span><strong id="gestureCustomizeToken">请先摆出手势</strong></div>
|
||||
<select id="gestureActionSelect" aria-label="选择座椅动作"></select>
|
||||
<button id="gestureBindButton" type="button">绑定当前手势</button>
|
||||
<button id="gestureResetMapping" type="button">恢复默认</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="gesture-footnote"><i data-lucide="shield-check"></i><span>换动作无需重新张掌;连续 3 秒无有效动作后自动锁止。</span></footer>
|
||||
</aside>
|
||||
|
||||
<div class="mode-gallery-backdrop" id="modeGalleryBackdrop" hidden>
|
||||
<section class="mode-gallery" id="modeGallery" role="dialog" aria-modal="true" aria-labelledby="modeGalleryTitle">
|
||||
<header class="mode-gallery-header">
|
||||
<div>
|
||||
<h2 id="modeGalleryTitle">管理功能模式</h2>
|
||||
<p>按来源与运行方式整理,快速定位并管理模式</p>
|
||||
</div>
|
||||
<button class="mode-gallery-close" id="modeGalleryClose" type="button" aria-label="关闭功能模式管理"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-gallery-summary"><span><i data-lucide="layers-3"></i>自定义模式 · 静态模式 · 动态模式</span><strong id="modeGallerySummaryCount">0</strong></div>
|
||||
<div class="mode-gallery-groups" id="modeGalleryGroups" aria-live="polite"></div>
|
||||
<footer class="mode-gallery-footer"><i data-lucide="info"></i><span>自定义模式支持编辑与删除;静态模式执行后保持目标状态;动态模式持续变化,可随时停止。</span></footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="mode-editor-backdrop" id="modeEditorBackdrop" hidden>
|
||||
<section class="mode-editor" role="dialog" aria-modal="true" aria-labelledby="modeEditorTitle">
|
||||
<div class="mode-editor-setup">
|
||||
<header class="mode-editor-header">
|
||||
<div>
|
||||
<h2 id="modeEditorTitle">编辑功能模式</h2>
|
||||
</div>
|
||||
<button id="modeEditorClose" type="button" aria-label="关闭模式编辑器"><i data-lucide="x"></i></button>
|
||||
</header>
|
||||
<div class="mode-editor-inputs">
|
||||
<label class="mode-editor-field mode-editor-name-field" for="modeEditorName">
|
||||
<span>模式名称</span>
|
||||
<input id="modeEditorName" maxlength="64" autocomplete="off" placeholder="输入模式名称" />
|
||||
</label>
|
||||
<label class="mode-editor-field mode-editor-instruction-field" for="modeEditorInstruction">
|
||||
<span>自然语言指令</span>
|
||||
<textarea id="modeEditorInstruction" maxlength="1000" rows="3" placeholder="例如:空调24度、风量2档,打开遮阳帘"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<section class="mode-editor-flow" aria-live="polite">
|
||||
<header>
|
||||
<span><i data-lucide="workflow"></i>控制指令流</span>
|
||||
<div class="mode-editor-flow-tools">
|
||||
<b id="modeEditorFlowCount">尚未生成</b>
|
||||
<button id="modeEditorFlowExpand" type="button" aria-expanded="false" aria-label="展开控制指令区域" title="展开控制指令区域">
|
||||
<i data-lucide="maximize-2"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<div class="mode-editor-flow-preview empty" id="modeEditorFlowPreview">
|
||||
<p>点击“生成指令”后,这里会显示由后端解析、校验并编译的控制指令流。</p>
|
||||
</div>
|
||||
</section>
|
||||
<p class="mode-editor-status" id="modeEditorStatus">修改自然语言后需要重新生成指令,才能点击完成永久保存。</p>
|
||||
<footer class="mode-editor-actions">
|
||||
<button id="modeEditorCancel" type="button">取消</button>
|
||||
<button class="mode-editor-execute" id="modeEditorExecute" type="button"><i data-lucide="wand-sparkles" data-icon="inline-start"></i><span>生成指令</span></button>
|
||||
<button class="mode-editor-complete" id="modeEditorComplete" type="button" disabled><i data-lucide="save" data-icon="inline-start"></i><span>完成</span></button>
|
||||
</footer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<button class="glass-theme-toggle" id="glassThemeToggle" type="button" aria-label="切换到白日模式" aria-pressed="true" title="切换到白日模式">
|
||||
<i data-lucide="sun" data-theme-icon="sun" aria-hidden="true"></i>
|
||||
<i data-lucide="moon" data-theme-icon="moon" aria-hidden="true"></i>
|
||||
<span class="visually-hidden">切换白日/夜间模式</span>
|
||||
</button>
|
||||
<div class="toast" id="toast" role="status" aria-live="polite"></div>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createApp, nextTick } from "vue";
|
||||
|
||||
import App from "./App.vue";
|
||||
import "../styles.css";
|
||||
import "../console-v2.css";
|
||||
import "../self-extension.css";
|
||||
import "../speech-input.css";
|
||||
import "../assistant-chat.css";
|
||||
import "../energy-monitor.css";
|
||||
import "../ui-consistency.css";
|
||||
import "../codepen-glass-theme.css";
|
||||
import "./styles/design-system.css";
|
||||
import "../vue-ui.js";
|
||||
|
||||
const application = createApp(App);
|
||||
application.config.errorHandler = (error, instance, info) => {
|
||||
console.error("N7 Vue application error", { error, instance, info });
|
||||
};
|
||||
application.mount("#app");
|
||||
|
||||
document.querySelector("#app")?.removeAttribute("aria-busy");
|
||||
document.documentElement.classList.add("vue-vite-application");
|
||||
document.documentElement.dataset.vueVersion = application.version || "3";
|
||||
|
||||
nextTick(() => {
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
window.dispatchEvent(new CustomEvent("cognitive-agent:vue-mounted"));
|
||||
});
|
||||
|
||||
window.CognitiveAgentVueApp = Object.freeze({
|
||||
application,
|
||||
version: application.version || "3"
|
||||
});
|
||||
@@ -0,0 +1,389 @@
|
||||
/* N7 Web Console design-system contract.
|
||||
* Loaded last by Vite so every legacy surface consumes one geometry,
|
||||
* typography, state and elevation scale while the runtime is migrated to
|
||||
* Vue components incrementally. */
|
||||
|
||||
:root {
|
||||
--n7-control-height-sm: 32px;
|
||||
--n7-control-height: 38px;
|
||||
--n7-icon-button-size: 38px;
|
||||
--n7-radius-control: 10px;
|
||||
--n7-radius-card: 14px;
|
||||
--n7-radius-section: 16px;
|
||||
--n7-radius-panel: 18px;
|
||||
--n7-radius-dialog: 22px;
|
||||
--n7-font-caption: 12px;
|
||||
--n7-font-body: 13px;
|
||||
--n7-font-card-title: 14px;
|
||||
--n7-font-panel-title: 16px;
|
||||
--n7-font-dialog-title: 22px;
|
||||
--n7-shadow-card: inset 0 1px 0 var(--glass-highlight), 0 7px 20px color-mix(in oklch, var(--foreground) 8%, transparent);
|
||||
--n7-shadow-panel: inset 0 1px 0 var(--glass-highlight), 0 14px 34px color-mix(in oklch, var(--foreground) 13%, transparent);
|
||||
--n7-shadow-dialog: inset 0 1px 0 var(--glass-highlight), 0 28px 84px color-mix(in oklch, var(--foreground) 28%, transparent);
|
||||
}
|
||||
|
||||
#app {
|
||||
min-width: inherit;
|
||||
min-height: 100dvh;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Workspace panels: one top-level geometry, distinct from their children. */
|
||||
body.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.main-panel > .v2-mode-rail,
|
||||
#assistantDrawer,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border-radius: var(--n7-radius-panel) !important;
|
||||
box-shadow: var(--n7-shadow-panel) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel,
|
||||
.v2-control-link-card,
|
||||
.v2-nfc-inline,
|
||||
.v2-extension-gateway,
|
||||
.v2-extension-synced-device,
|
||||
.v2-extension-empty,
|
||||
.v2-pet-card,
|
||||
.v2-pet-presence-card,
|
||||
.v2-device-card,
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi,
|
||||
.energy-device-row,
|
||||
.assistant-dispatched-controls,
|
||||
.assistant-mode-draft,
|
||||
.assistant-mode-created,
|
||||
.assistant-control-suggestion
|
||||
) {
|
||||
border-radius: var(--n7-radius-card) !important;
|
||||
box-shadow: var(--n7-shadow-card) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.v2-sync-log,
|
||||
.v2-pet-card-region,
|
||||
.mode-gallery-group,
|
||||
.mode-editor-flow,
|
||||
.v2-nfc-stage,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
) {
|
||||
border-radius: var(--n7-radius-section) !important;
|
||||
}
|
||||
|
||||
/* Only actionable cards float. Read-only panels remain spatially stable. */
|
||||
body.console-v2 :is(
|
||||
.v2-sync-log,
|
||||
.v2-pet-card-region,
|
||||
.mode-editor-flow,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card
|
||||
):is(:hover, :focus-within) {
|
||||
transform: none !important;
|
||||
box-shadow: var(--n7-shadow-card) !important;
|
||||
}
|
||||
|
||||
/* The current DOM uses .twin-selector; the previous glass selector targeted
|
||||
* the removed .twin-seat-switch class and produced white-on-white tabs. */
|
||||
body.console-v2 .joint-twin-shell :is(.twin-selector, .twin-selector button, .twin-reset, .twin-focus-badge) {
|
||||
color: var(--muted-foreground) !important;
|
||||
background: var(--glass-card) !important;
|
||||
border-color: var(--glass-line) !important;
|
||||
box-shadow: inset 0 1px 0 var(--glass-highlight) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .joint-twin-shell .twin-selector button.active {
|
||||
color: var(--primary-foreground) !important;
|
||||
background: var(--primary) !important;
|
||||
border-color: color-mix(in oklch, var(--primary) 72%, white 18%) !important;
|
||||
}
|
||||
|
||||
/* Shared heading hierarchy. */
|
||||
body.console-v2 :is(
|
||||
#assistantDrawer h2,
|
||||
.joint-twin-shell .twin-heading h3,
|
||||
.v2-device-sync-board > header h3,
|
||||
.v2-extension-sync-heading h3,
|
||||
.v2-mode-rail .mode-rail-title
|
||||
) {
|
||||
font-size: var(--n7-font-panel-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.25 !important;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
/* The log title shares a 217 px header with its filter, so it needs the
|
||||
* compact card-title scale instead of the full panel-title scale. */
|
||||
body.console-v2 .v2-sync-log h3 {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
font-size: var(--n7-font-card-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.25 !important;
|
||||
letter-spacing: -0.02em;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Sidebar disclosure, connection status and control-command readability use
|
||||
* the same semantic tokens in both day and night themes. */
|
||||
body.console-v2 .v2-sync-log[data-collapsed="true"] {
|
||||
min-height: 0;
|
||||
align-self: start;
|
||||
grid-template-rows: auto;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log[data-collapsed="true"] > header {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log[data-collapsed="true"] #v2LogFilter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log #v2SyncLogList[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log > header > div:first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log .v2-sync-log-actions {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log .v2-sync-log-toggle {
|
||||
min-width: 46px;
|
||||
min-height: var(--n7-control-height-sm);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 0 7px;
|
||||
color: var(--muted-foreground);
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--input);
|
||||
border-radius: var(--n7-radius-control);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log .v2-sync-log-toggle:is(:hover, :focus-visible) {
|
||||
color: var(--accent-foreground);
|
||||
background: var(--accent);
|
||||
border-color: var(--ring);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log .v2-sync-log-toggle svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-sync-log .v2-sync-log-toggle[aria-expanded="true"] svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
body.console-v2.v2-passive-display.v2-interactive-sync .v2-control-link-card {
|
||||
min-height: 56px;
|
||||
grid-template-columns: 10px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
margin: 0 !important;
|
||||
padding: 10px 11px !important;
|
||||
color: var(--card-foreground);
|
||||
background: var(--glass-card) !important;
|
||||
border: 1px solid var(--glass-line) !important;
|
||||
border-radius: var(--n7-radius-card) !important;
|
||||
box-shadow: var(--n7-shadow-card) !important;
|
||||
-webkit-backdrop-filter: blur(16px) saturate(1.2);
|
||||
backdrop-filter: blur(16px) saturate(1.2);
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card strong {
|
||||
color: var(--card-foreground) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card small {
|
||||
color: var(--muted-foreground) !important;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card[data-state="online"] .v2-live-dot {
|
||||
background: var(--status-online);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-online) 18%, transparent);
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card[data-state="connecting"] .v2-live-dot {
|
||||
background: var(--status-info);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-info) 18%, transparent);
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card[data-state="degraded"] .v2-live-dot {
|
||||
background: var(--status-warning);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-warning) 18%, transparent);
|
||||
}
|
||||
|
||||
body.console-v2 .v2-control-link-card[data-state="offline"] .v2-live-dot {
|
||||
background: var(--status-offline);
|
||||
box-shadow: 0 0 0 4px color-mix(in oklch, var(--status-offline) 18%, transparent);
|
||||
}
|
||||
|
||||
body.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list li[data-can-channel]
|
||||
.v2-can-command-actual code {
|
||||
color: var(--card-foreground) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-header h2,
|
||||
.mode-editor-header h2,
|
||||
.v2-extension-dialog > header h2,
|
||||
.v2-extension-dialog form > header h2,
|
||||
.energy-monitor-heading h2
|
||||
) {
|
||||
font-size: var(--n7-font-dialog-title) !important;
|
||||
font-weight: 750 !important;
|
||||
line-height: 1.2 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-card-heading strong,
|
||||
.v2-device-card-focus strong,
|
||||
.v2-nfc-inline strong,
|
||||
.v2-extension-gateway strong,
|
||||
.v2-pet-card > header > strong,
|
||||
.energy-trend-card h3,
|
||||
.energy-devices-card h3
|
||||
) {
|
||||
font-size: var(--n7-font-card-title) !important;
|
||||
line-height: 1.35 !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-editor-status,
|
||||
.mode-gallery-footer,
|
||||
.v2-pet-settings-footnote,
|
||||
.v2-pet-card-fields > label > span,
|
||||
.v2-pet-nfc-field output,
|
||||
.energy-kpi > small,
|
||||
.energy-kpi > em,
|
||||
.energy-devices-card > footer
|
||||
) {
|
||||
font-size: var(--n7-font-caption) !important;
|
||||
line-height: 1.45 !important;
|
||||
}
|
||||
|
||||
/* One dialog radius, elevation and close-button size. */
|
||||
body.console-v2 :is(
|
||||
.mode-gallery,
|
||||
.mode-editor,
|
||||
.v2-nfc-dialog,
|
||||
.v2-gscu-tcp-dialog,
|
||||
.v2-pet-settings-dialog,
|
||||
.energy-monitor-page
|
||||
) {
|
||||
border-radius: var(--n7-radius-dialog) !important;
|
||||
box-shadow: var(--n7-shadow-dialog) !important;
|
||||
}
|
||||
|
||||
body.console-v2 :is(
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
.v2-extension-dialog [data-dialog-close],
|
||||
.v2-extension-dialog [data-close-extension-dialog],
|
||||
.v2-extension-dialog [data-close-nfc-dialog],
|
||||
#energyMonitorClose
|
||||
) {
|
||||
width: var(--n7-icon-button-size) !important;
|
||||
height: var(--n7-icon-button-size) !important;
|
||||
min-width: var(--n7-icon-button-size);
|
||||
min-height: var(--n7-icon-button-size);
|
||||
border-radius: var(--n7-radius-control) !important;
|
||||
}
|
||||
|
||||
/* Compact icon actions still meet a usable hit target without becoming a
|
||||
* second visual system. */
|
||||
body.console-v2 :is(
|
||||
.twin-reset,
|
||||
.mode-gallery-edit,
|
||||
.mode-gallery-delete,
|
||||
.v2-pet-card > header > button
|
||||
) {
|
||||
width: var(--n7-control-height-sm) !important;
|
||||
height: var(--n7-control-height-sm) !important;
|
||||
min-width: var(--n7-control-height-sm);
|
||||
min-height: var(--n7-control-height-sm);
|
||||
border-radius: var(--n7-radius-control) !important;
|
||||
}
|
||||
|
||||
/* Offline hardware is a destructive state, not a primary/active state. */
|
||||
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header {
|
||||
color: var(--status-error-foreground) !important;
|
||||
background: color-mix(in oklch, var(--destructive) 16%, var(--card)) !important;
|
||||
border-bottom: 1px solid color-mix(in oklch, var(--destructive) 32%, var(--border));
|
||||
}
|
||||
|
||||
body.console-v2 .v2-nfc-stage:has(.v2-nfc-waiting.offline) > header :is(span, small, h3, p, b) {
|
||||
color: var(--status-error-foreground) !important;
|
||||
}
|
||||
|
||||
/* Keep the global appearance control out of content and below every modal. */
|
||||
.glass-theme-toggle {
|
||||
left: max(18px, env(safe-area-inset-left)) !important;
|
||||
bottom: max(18px, env(safe-area-inset-bottom)) !important;
|
||||
z-index: 1100 !important;
|
||||
}
|
||||
|
||||
body.mode-gallery-open .glass-theme-toggle,
|
||||
body.mode-editor-open .glass-theme-toggle,
|
||||
body:has(.v2-extension-dialog[open]) .glass-theme-toggle,
|
||||
body:has(.energy-monitor-layer:not([hidden])) .glass-theme-toggle {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* The sidebar reserves the floating control's footprint instead of allowing
|
||||
* it to cover the log timeline. */
|
||||
body.console-v2 .v2-sync-log {
|
||||
margin-bottom: 58px;
|
||||
}
|
||||
|
||||
/* Top mode rail communicates that more content is available. */
|
||||
body.console-v2 .v2-mode-rail .mode-rail-content {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
body.console-v2 .v2-mode-rail .mode-rail-content::after {
|
||||
width: 28px;
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0 0 0 auto;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(90deg, transparent, color-mix(in oklch, var(--glass-panel) 94%, transparent));
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.glass-theme-toggle {
|
||||
width: 42px !important;
|
||||
height: 42px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
body.console-v2 * {
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
/* Shared spatial UI contract --------------------------------------------
|
||||
Keep geometry consistent across legacy modules without changing their
|
||||
domain-specific colors, content density, or interaction behavior.
|
||||
|
||||
Hierarchy: control 10px -> card 13px -> section 15px -> panel 16px ->
|
||||
modal 20px. All modules consume the same semantic color tokens. */
|
||||
:root {
|
||||
--ui-control-height-compact: 30px;
|
||||
--ui-control-height: 38px;
|
||||
--ui-control-size-icon: 36px;
|
||||
--ui-font-card-title: 13px;
|
||||
--ui-font-section-title: 15px;
|
||||
--ui-font-panel-title: 16px;
|
||||
--ui-inline-action-width: 88px;
|
||||
--ui-radius-control-inner: calc(var(--radius) - 2px);
|
||||
--ui-radius-control: var(--radius);
|
||||
--ui-radius-item: calc(var(--radius) + 2px);
|
||||
--ui-radius-section: calc(var(--radius) + 6px);
|
||||
--ui-radius-panel: calc(var(--radius) + 8px);
|
||||
--ui-radius-modal: calc(var(--radius) + 12px);
|
||||
--ui-space-card: 10px;
|
||||
--ui-border-subtle: 1px solid var(--border);
|
||||
--ui-shadow-item: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent);
|
||||
--ui-shadow-panel:
|
||||
0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent);
|
||||
--ui-shadow-modal:
|
||||
0 28px 72px color-mix(in oklch, var(--foreground) 22%, transparent),
|
||||
0 4px 18px color-mix(in oklch, var(--primary) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent);
|
||||
--ui-focus-ring: 0 0 0 3px color-mix(in oklch, var(--ring) 22%, transparent);
|
||||
}
|
||||
|
||||
/* One readable type scale prevents large legacy headings and tiny newer card
|
||||
titles from competing inside the same workspace. */
|
||||
.console-v2 :is(
|
||||
.v2-operation-heading h2,
|
||||
.joint-twin-shell .twin-heading h3,
|
||||
.v2-device-sync-board > header h3,
|
||||
.v2-extension-sync-heading h3,
|
||||
.v2-sync-sidebar-heading h2,
|
||||
.workspace-panel-title h3,
|
||||
.config-panel-heading h3
|
||||
) {
|
||||
font-size: var(--ui-font-panel-title) !important;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
.v2-pet-card-region > header h4,
|
||||
.v2-gscu-actions > header strong,
|
||||
.v2-gscu-command-catalog > header strong
|
||||
) {
|
||||
font-size: var(--ui-font-section-title) !important;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
.v2-device-card-focus strong,
|
||||
.v2-nfc-inline strong,
|
||||
.v2-extension-gateway strong,
|
||||
.v2-extension-synced-device header strong,
|
||||
.v2-pet-presence-card > header strong,
|
||||
.v2-pet-card > header > strong
|
||||
) {
|
||||
font-size: var(--ui-font-card-title) !important;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
/* Top-level workspace surfaces share one frame. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-operation-shell,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-panel);
|
||||
box-shadow: var(--ui-shadow-panel);
|
||||
}
|
||||
|
||||
/* The operation shell owns the frame; its active view must not draw a
|
||||
second border directly inside the same spatial boundary. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-operation-shell > .v2-natural-panel {
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Modal surfaces and their backdrops use one elevation level. */
|
||||
:is(
|
||||
.mode-gallery-backdrop .mode-gallery,
|
||||
.mode-editor-backdrop .mode-editor,
|
||||
.energy-monitor-layer .energy-monitor-page
|
||||
) {
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: var(--ui-radius-modal);
|
||||
box-shadow: var(--ui-shadow-modal);
|
||||
}
|
||||
|
||||
.mode-gallery-backdrop,
|
||||
.mode-editor-backdrop,
|
||||
.energy-monitor-layer {
|
||||
background: color-mix(in oklch, var(--foreground) 42%, transparent);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
/* Section containers sit between panels and individual cards. */
|
||||
.v2-sync-log,
|
||||
.mode-gallery-group,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card {
|
||||
border-radius: var(--ui-radius-section);
|
||||
}
|
||||
|
||||
/* Repeated content cards share radius and elevation while retaining their
|
||||
own semantic state colors and specialized internal layouts. */
|
||||
.v2-synced-device,
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi {
|
||||
border-radius: var(--ui-radius-item);
|
||||
box-shadow: var(--ui-shadow-item);
|
||||
}
|
||||
|
||||
.energy-kpi-primary {
|
||||
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline {
|
||||
padding: var(--ui-space-card);
|
||||
}
|
||||
|
||||
/* Sidebar cards and their nested segmented controls preserve a clear
|
||||
parent/child radius relationship. */
|
||||
.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item);
|
||||
}
|
||||
|
||||
/* The compact-sidebar rules in console-v2.css intentionally use highly
|
||||
specific selectors. Keep all peer cards on the shared item radius even
|
||||
when that layout is active. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log {
|
||||
border-radius: var(--ui-radius-section) !important;
|
||||
}
|
||||
|
||||
.console-v2 :is(.segmented, .twin-selector) {
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
background: var(--muted);
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > :is(.segment, .mode-option),
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > .mode-option {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Primary header controls use one medium size. */
|
||||
.console-v2 #v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail .mode-rail-actions > :is(
|
||||
#modeGalleryToggle,
|
||||
#saveModeToggle,
|
||||
#modeRailToggle
|
||||
) {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > #v2SidebarToggle.v2-panel-control-button {
|
||||
border-radius: var(--ui-radius-control) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail #modeRailToggle {
|
||||
width: var(--ui-control-height);
|
||||
min-width: var(--ui-control-height);
|
||||
}
|
||||
|
||||
/* Repeated inline actions use one compact size. */
|
||||
.console-v2 :is(
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-pet-settings-launch,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
),
|
||||
.energy-trend-tabs button {
|
||||
min-height: var(--ui-control-height-compact);
|
||||
height: var(--ui-control-height-compact);
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
/* Related extension actions use equal columns and a shared trailing action
|
||||
width, so every card keeps the same horizontal rhythm. */
|
||||
.console-v2 .v2-extension-sync-actions {
|
||||
width: calc(var(--ui-inline-action-width) * 2 + 6px);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-extension-sync-actions > :is(
|
||||
.v2-pet-settings-launch,
|
||||
.v2-extension-nfc-launch
|
||||
) {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: var(--ui-control-height-compact);
|
||||
justify-content: center;
|
||||
padding-inline: 7px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
.v2-nfc-inline > button,
|
||||
.v2-extension-gateway > button
|
||||
) {
|
||||
width: var(--ui-inline-action-width);
|
||||
min-width: var(--ui-inline-action-width);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Header icon actions are distinct from circular record/send actions. */
|
||||
.assistant-drawer-actions .context-reset,
|
||||
.assistant-drawer-close,
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
#energyMonitorClose {
|
||||
width: var(--ui-control-size-icon);
|
||||
min-width: var(--ui-control-size-icon);
|
||||
height: var(--ui-control-size-icon);
|
||||
min-height: var(--ui-control-size-icon);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Text fields align with medium controls and use the same focus treatment. */
|
||||
.mode-editor-field input,
|
||||
.mode-editor-field textarea {
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Reserve the right side for the microphone/send actions. Anchoring the hint
|
||||
from the content edge prevents the legacy right offsets from pushing it
|
||||
outside narrow assistant composers. */
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync
|
||||
.v2-request-form .composer-hint {
|
||||
left: 16px !important;
|
||||
right: auto !important;
|
||||
bottom: 8px;
|
||||
max-width: calc(100% - 132px);
|
||||
}
|
||||
|
||||
/* Delivery states use the same semantic feedback language in the sidebar and
|
||||
in the compact assistant command card. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="failed"] {
|
||||
color: var(--destructive);
|
||||
background: color-mix(in oklch, var(--destructive) 10%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="blocked"] {
|
||||
color: var(--status-warning-foreground);
|
||||
background: var(--status-warning-soft);
|
||||
border-color: color-mix(in oklch, var(--status-warning) 38%, var(--border));
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery[data-delivery-state="aborted"] {
|
||||
color: var(--muted-foreground);
|
||||
background: var(--muted);
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-can-channel]
|
||||
.v2-can-command-delivery-detail {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
margin: 1px 0 2px;
|
||||
padding: 5px 7px;
|
||||
color: var(--muted-foreground);
|
||||
background: color-mix(in oklch, var(--muted) 62%, transparent);
|
||||
border-left: 2px solid var(--border);
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
font-size: 9px;
|
||||
line-height: 1.45;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-delivery-state="failed"]
|
||||
.v2-can-command-delivery-detail {
|
||||
color: var(--status-error-foreground);
|
||||
border-left-color: var(--destructive);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-list
|
||||
li[data-delivery-state="blocked"]
|
||||
.v2-can-command-delivery-detail {
|
||||
color: var(--status-warning-foreground);
|
||||
border-left-color: var(--status-warning);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="failed"] {
|
||||
background: color-mix(in oklch, var(--destructive) 4%, var(--card));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="failed"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="blocked"] {
|
||||
background: var(--status-warning-soft);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="blocked"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--status-warning-foreground);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li[data-delivery-state="aborted"] {
|
||||
background: color-mix(in oklch, var(--muted) 68%, transparent);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list
|
||||
> li[data-delivery-state="aborted"]
|
||||
.assistant-dispatched-control-icon {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.v2-mode-rail .mode-rail-actions > button,
|
||||
.segmented > button,
|
||||
.twin-selector > button,
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-pet-settings-launch,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
):focus-visible,
|
||||
.mode-gallery :is(button, input, textarea):focus-visible,
|
||||
.mode-editor :is(button, input, textarea):focus-visible,
|
||||
.energy-monitor-page button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ui-focus-ring);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
:root {
|
||||
--ui-control-height: 40px;
|
||||
--ui-control-height-compact: 32px;
|
||||
--ui-control-size-icon: 38px;
|
||||
}
|
||||
|
||||
.console-v2 .v2-extension-sync-actions {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.energy-kpi,
|
||||
.console-v2 button {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License
|
||||
|
||||
Copyright © 2010-2022 three.js authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
@@ -0,0 +1,177 @@
|
||||
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";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const webRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const outputRoot = resolve(webRoot, "dist");
|
||||
const backendOrigin = "http://127.0.0.1:8000";
|
||||
|
||||
const runtimeDirectories = ["assets", "icons", "vendor"];
|
||||
const runtimeFiles = [
|
||||
"manifest.webmanifest",
|
||||
"service-worker.js",
|
||||
"speech-audio-worklet.js",
|
||||
"login.html",
|
||||
"login.css",
|
||||
"login.js",
|
||||
"hardware-control.html",
|
||||
"hardware-control.css",
|
||||
"hardware-control.js",
|
||||
"network.js",
|
||||
"app.js",
|
||||
"gesture-control.js",
|
||||
"pwa.js",
|
||||
"console-v2.js",
|
||||
"self-extension.js",
|
||||
"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 {
|
||||
name: "copy-n7-runtime-assets",
|
||||
apply: "build",
|
||||
closeBundle() {
|
||||
for (const directory of runtimeDirectories) {
|
||||
const source = resolve(webRoot, directory);
|
||||
if (existsSync(source)) {
|
||||
cpSync(source, resolve(outputRoot, directory), { recursive: true });
|
||||
}
|
||||
}
|
||||
for (const file of runtimeFiles) {
|
||||
const source = resolve(webRoot, file);
|
||||
if (!existsSync(source)) continue;
|
||||
const target = resolve(outputRoot, file);
|
||||
mkdirSync(dirname(target), { recursive: true });
|
||||
copyFileSync(source, target);
|
||||
}
|
||||
fingerprintRuntimeAssets();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function backendProxy() {
|
||||
return {
|
||||
target: backendOrigin,
|
||||
changeOrigin: true,
|
||||
configure(proxy) {
|
||||
proxy.on("proxyReq", (request) => request.setHeader("origin", backendOrigin));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
root: webRoot,
|
||||
publicDir: false,
|
||||
plugins: [vue(), copyRuntimeAssets()],
|
||||
server: {
|
||||
strictPort: true,
|
||||
fs: { allow: [webRoot] },
|
||||
proxy: {
|
||||
"/api": backendProxy(),
|
||||
"/artifacts": backendProxy()
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: outputRoot,
|
||||
emptyOutDir: true,
|
||||
assetsDir: "vite-assets",
|
||||
target: "es2022",
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,171 @@
|
||||
import { Fragment, createApp, h, nextTick, reactive } from "vue";
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const VUE_VERSION = "3.5.21";
|
||||
const modeMounts = new WeakMap();
|
||||
const commandMounts = new WeakMap();
|
||||
const text = (value, fallback = "") => String(value ?? fallback);
|
||||
|
||||
function refreshIcons() {
|
||||
nextTick(() => window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }));
|
||||
}
|
||||
|
||||
function replaceReactiveArray(target, values) {
|
||||
target.splice(0, target.length, ...values.map((value) => ({ ...value })));
|
||||
}
|
||||
|
||||
function renderModeEntry(entry) {
|
||||
const key = text(entry.key || `${entry.type}-${entry.id || entry.label}`);
|
||||
if (entry.type === "label") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-label",
|
||||
"data-mode-group-label": text(entry.id),
|
||||
}, text(entry.label));
|
||||
}
|
||||
if (entry.type === "divider") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-divider",
|
||||
"data-mode-divider": text(entry.id),
|
||||
"aria-hidden": "true",
|
||||
});
|
||||
}
|
||||
const children = [
|
||||
h("i", {
|
||||
class: "mode-chip-icon",
|
||||
"data-lucide": text(entry.icon, "bookmark"),
|
||||
}),
|
||||
h("span", { class: "mode-chip-label" }, text(entry.label, "模式")),
|
||||
h("small", { class: "mode-chip-summary" }, text(entry.summary, "整车联动")),
|
||||
];
|
||||
if (entry.cycleLabel) {
|
||||
children.push(h("small", { class: "mode-cycle-badge" }, text(entry.cycleLabel)));
|
||||
}
|
||||
children.push(h("i", {
|
||||
class: "mode-chip-run",
|
||||
"data-lucide": "play",
|
||||
"data-mode-run": "true",
|
||||
title: `执行${text(entry.label, "模式")}`,
|
||||
}));
|
||||
return h("button", {
|
||||
key,
|
||||
type: "button",
|
||||
title: text(entry.title),
|
||||
"data-mode-profile-id": text(entry.id),
|
||||
"data-mode-command": text(entry.command),
|
||||
"data-mode-origin": text(entry.origin, "preset"),
|
||||
...(entry.behavior ? { "data-mode-behavior": text(entry.behavior) } : {}),
|
||||
"aria-label": text(entry.ariaLabel, `${text(entry.label, "模式")},点击执行`),
|
||||
"aria-pressed": "false",
|
||||
}, children);
|
||||
}
|
||||
|
||||
function createModeMount(container) {
|
||||
const state = reactive({ entries: [] });
|
||||
const app = createApp({
|
||||
name: "N7ModeChipRail",
|
||||
setup() {
|
||||
return () => h(Fragment, null, state.entries.map(renderModeEntry));
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
modeMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
function renderModeChips(container, entries) {
|
||||
if (!(container instanceof Element) || !Array.isArray(entries)) return false;
|
||||
const mount = modeMounts.get(container) || createModeMount(container);
|
||||
replaceReactiveArray(mount.state.entries, entries);
|
||||
container.dataset.vueSurface = "mode-chips";
|
||||
refreshIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderCommandRow(command, index) {
|
||||
const semantic = text(command.semantic);
|
||||
const actual = text(command.command);
|
||||
const accessibleText = [
|
||||
semantic ? `语义控制指令:${semantic}` : "",
|
||||
actual ? `实际控制指令:${actual}` : "",
|
||||
command.deliveryLabel
|
||||
? `下发状态:${text(command.deliveryLabel)}${command.deliveryDetail ? `(${text(command.deliveryDetail)})` : ""}`
|
||||
: "",
|
||||
].filter(Boolean).join(";");
|
||||
return h("li", {
|
||||
key: text(command.key, `command-${index}`),
|
||||
title: accessibleText,
|
||||
"data-delivery-state": text(command.deliveryState, "not_submitted"),
|
||||
}, [
|
||||
h("span", { class: "assistant-dispatched-control-icon" }, [
|
||||
h("i", { "data-lucide": text(command.icon, "binary") }),
|
||||
]),
|
||||
h("span", { class: "assistant-dispatched-control-label" }, text(command.category, "控制指令")),
|
||||
h("strong", { class: "assistant-dispatched-control-value" }, text(command.value, "控制")),
|
||||
h("small", { class: "visually-hidden" }, accessibleText),
|
||||
]);
|
||||
}
|
||||
|
||||
function createCommandMount(container) {
|
||||
const state = reactive({ commands: [], open: true, heading: "下发控制指令" });
|
||||
const app = createApp({
|
||||
name: "N7DispatchedControlsCard",
|
||||
setup() {
|
||||
return () => h("details", {
|
||||
class: "assistant-dispatched-controls",
|
||||
open: state.open,
|
||||
onToggle(event) {
|
||||
state.open = event.currentTarget.open;
|
||||
if (!state.open) return;
|
||||
const transcript = document.querySelector("#assistantMessages");
|
||||
transcript?.querySelectorAll(".assistant-dispatched-controls[open]").forEach((card) => {
|
||||
if (card !== event.currentTarget) card.open = false;
|
||||
});
|
||||
},
|
||||
}, [
|
||||
h("summary", null, [
|
||||
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
|
||||
h("i", { "data-lucide": "list-tree" }),
|
||||
]),
|
||||
h("b", null, state.heading),
|
||||
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
|
||||
h("i", {
|
||||
class: "assistant-dispatched-controls-chevron",
|
||||
"data-lucide": "chevron-down",
|
||||
}),
|
||||
]),
|
||||
h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
|
||||
]);
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
commandMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
document.documentElement.classList.add("vue-presentation-enabled");
|
||||
document.documentElement.dataset.vueVersion = VUE_VERSION;
|
||||
window.CognitiveAgentVueUI = Object.freeze({
|
||||
available: true,
|
||||
version: VUE_VERSION,
|
||||
renderDispatchedControls,
|
||||
renderModeChips,
|
||||
});
|
||||
}());
|
||||
|
Before Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"schema_version": "1.0.0",
|
||||
"validated_at": "2026-08-04T09:55:52+08:00",
|
||||
"platform": "t2g_cabin_bench",
|
||||
"adapter": {
|
||||
"model": "USBCANFD-200U",
|
||||
"device_type": 41,
|
||||
"device_index": 0,
|
||||
"driver_self_test": {
|
||||
"passed": true,
|
||||
"pointer_bits": 32,
|
||||
"missing_exports": [],
|
||||
"classic_frame_size": 16,
|
||||
"can_fd_frame_size": 72,
|
||||
"can_fd_tx_size": 76,
|
||||
"can_fd_rx_size": 80
|
||||
}
|
||||
},
|
||||
"channels": {
|
||||
"seat_can": {
|
||||
"channel": 0,
|
||||
"format": "classic_can_11_bit",
|
||||
"bitrate": 500000,
|
||||
"diagnostic_self_reception": {
|
||||
"frame_id": "0x7AA",
|
||||
"payload_hex": "43 48 30 53 45 41 54 30",
|
||||
"passed": true
|
||||
},
|
||||
"external_passive_observation_seconds": 6,
|
||||
"external_frames_observed": 0,
|
||||
"physical_feedback_verified": false
|
||||
},
|
||||
"body_can": {
|
||||
"channel": 1,
|
||||
"format": "iso_can_fd_11_bit",
|
||||
"arbitration_bitrate": 500000,
|
||||
"data_bitrate": 2000000,
|
||||
"bitrate_switch": true,
|
||||
"diagnostic_self_reception": {
|
||||
"frame_id": "0x7AA",
|
||||
"payload_hex": "43 48 31 2D 42 4F 44 59 2D 43 41 4E 46 44 21 21",
|
||||
"payload_size": 16,
|
||||
"passed": true
|
||||
},
|
||||
"external_passive_observation_seconds": 5,
|
||||
"external_frames_observed": 0,
|
||||
"physical_feedback_verified": false
|
||||
}
|
||||
},
|
||||
"linked_request": {
|
||||
"utterance": "关闭副驾座椅通风,同时关闭空调",
|
||||
"dry_run": {
|
||||
"status": "succeeded",
|
||||
"function_configs": ["seat", "native_climate"],
|
||||
"command_plans": ["seat", "native_climate"]
|
||||
},
|
||||
"hardware_attempts": [
|
||||
{
|
||||
"request_id": "req-20260804015505-713de5",
|
||||
"status": "execution_failed",
|
||||
"body_can": "0x1DE transmit rejected; channel_error=0x00000004",
|
||||
"seat_can": "passive firmware signature not observed within 1200 ms; no seat control frame sent"
|
||||
},
|
||||
{
|
||||
"request_id": "req-20260804015552-674fb4",
|
||||
"status": "execution_failed",
|
||||
"body_can": "0x1DE transmit rejected; channel_error=0x00000000",
|
||||
"seat_can": "passive firmware signature not observed within 3500 ms; no seat control frame sent"
|
||||
}
|
||||
]
|
||||
},
|
||||
"verdict": {
|
||||
"adapter_dual_channel_transport": "passed",
|
||||
"classic_can_transport": "passed",
|
||||
"iso_can_fd_brs_transport": "passed",
|
||||
"one_sentence_compilation_and_routing": "passed",
|
||||
"seat_ecu_closed_loop": "not_verified_no_external_frames",
|
||||
"body_ecu_closed_loop": "not_verified_no_bus_ack_or_0x404_feedback",
|
||||
"overall_hardware_requirement": "blocked_by_external_bus_connectivity_or_power"
|
||||
},
|
||||
"safety_cleanup": {
|
||||
"seat_control_acknowledged": false,
|
||||
"body_control_acknowledged": false,
|
||||
"actuator_state_changed_by_verified_command": false,
|
||||
"zcanpro_restarted": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
{
|
||||
"suite": "ch1-vehicle-door-smoke-hil",
|
||||
"started_with": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:38.774392Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2751,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"finished_at": "2026-08-10T18:27:06.208449Z",
|
||||
"results": [
|
||||
{
|
||||
"at": "2026-08-10T18:26:43.864787Z",
|
||||
"module": "self_orchestration",
|
||||
"case_id": "DOOR-VEHICLE-LOCK",
|
||||
"status": "failed",
|
||||
"duration_ms": 4032,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:39.775825Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2256,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"error": "Ch1HilContractError: DOOR-VEHICLE-LOCK: request status is execution_failed"
|
||||
},
|
||||
{
|
||||
"at": "2026-08-10T18:27:06.208449Z",
|
||||
"module": "cleanup",
|
||||
"case_id": "suite_finally",
|
||||
"status": "passed",
|
||||
"results": [
|
||||
{
|
||||
"case_id": "WINDOW-FRONT-LEFT-0",
|
||||
"command": "window front-left position 0",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2843,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:44.790088Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2279,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182644-0f4f23"
|
||||
},
|
||||
{
|
||||
"case_id": "SHADE-POSITION-0",
|
||||
"command": "shade position 0",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2235,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:47.795715Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2694,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182647-436fd7"
|
||||
},
|
||||
{
|
||||
"case_id": "DOOR-VEHICLE-UNLOCK",
|
||||
"command": "door vehicle unlock",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2906,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:49.802764Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2017,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182649-4a52a7"
|
||||
},
|
||||
{
|
||||
"case_id": "SEAT-DRIVER-MASSAGE-OFF",
|
||||
"command": "seat driver massage off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2500,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:52.820116Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 2374,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182652-028f24"
|
||||
},
|
||||
{
|
||||
"case_id": "SEAT-DRIVER-HEAT-OFF",
|
||||
"command": "seat driver heat off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2000,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:54.817949Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 697,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182654-200daa"
|
||||
},
|
||||
{
|
||||
"case_id": "SEAT-DRIVER-VENT-OFF",
|
||||
"command": "seat driver vent off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2000,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:56.821969Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 709,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182656-77bc93"
|
||||
},
|
||||
{
|
||||
"case_id": "CLIMATE-POWER-OFF",
|
||||
"command": "ac off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 1859,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:26:58.826087Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 735,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182658-1509d2"
|
||||
},
|
||||
{
|
||||
"case_id": "LIGHT-INDOOR-OFF",
|
||||
"command": "light indoor off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 1953,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:27:00.833034Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 1204,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182700-64ded1"
|
||||
},
|
||||
{
|
||||
"case_id": "AMBIENT-POWER-OFF",
|
||||
"command": "light ambient off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 2172,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:27:02.837333Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 1445,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182702-78f422"
|
||||
},
|
||||
{
|
||||
"case_id": "LIGHT-READING-ALL-OFF",
|
||||
"command": "light reading all off",
|
||||
"http_status": 201,
|
||||
"status": "passed",
|
||||
"duration_ms": 1875,
|
||||
"safety_snapshot": {
|
||||
"source": "physical_can_ch1",
|
||||
"channel": 1,
|
||||
"read_only": true,
|
||||
"captured_at": "2026-08-10T18:27:04.842348Z",
|
||||
"gear": "P",
|
||||
"speed_kmh": 0.0,
|
||||
"frame_count": 829,
|
||||
"evidence": {
|
||||
"gear_frame_id": "0x342",
|
||||
"speed_frame_id": "0x4EB"
|
||||
}
|
||||
},
|
||||
"request_id": "req-20260810182704-244a2c"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"aborted_on_first_failure": true,
|
||||
"cleanup_runs": 1,
|
||||
"cleanup_failed": false,
|
||||
"offline_rejection_cases": 24
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"validated_at": "2026-08-04",
|
||||
"firmware_revision": 10,
|
||||
"firmware_image_sha256": "85f9d67ff1efb36c37f38bb7584dffdbb6ecbee33bfc70ab491bad0d57da63b2",
|
||||
"change": "remove delayed M1 clearance/catch-up and keep all four axes moving from the phase start",
|
||||
"max_hall": [499, 374, 917, 647],
|
||||
"known_range_home": {
|
||||
"hall_before": [379, 255, 417, 416],
|
||||
"hall_after": [0, 0, 0, 0],
|
||||
"all_complete_ok": true,
|
||||
"all_position_valid": true
|
||||
},
|
||||
"step_1_to_step_2": {
|
||||
"start_hall": [391, 140, 406, 405],
|
||||
"target_hall": [246, 281, 701, 494],
|
||||
"observed_hall": [246, 281, 701, 494],
|
||||
"four_axis_start_spread_s": 0.070,
|
||||
"four_axis_arrival_spread_s": 0.342,
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 3.090,
|
||||
"hall": [354, 202, 510, 468],
|
||||
"normalized_progress": {
|
||||
"longitudinal": 0.255,
|
||||
"cushion_tilt": 0.440,
|
||||
"backrest_recline": 0.353,
|
||||
"legrest_height": 0.708
|
||||
}
|
||||
},
|
||||
"result": "PASS"
|
||||
},
|
||||
"step_2_to_step_3": {
|
||||
"start_hall": [246, 281, 701, 494],
|
||||
"target_hall": [391, 140, 405, 404],
|
||||
"observed_hall": [391, 140, 405, 404],
|
||||
"four_axis_start_spread_s": 0.050,
|
||||
"four_axis_arrival_spread_s": 0.339,
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 2.591,
|
||||
"hall": [283, 226, 616, 435],
|
||||
"normalized_progress": {
|
||||
"longitudinal": 0.255,
|
||||
"cushion_tilt": 0.390,
|
||||
"backrest_recline": 0.287,
|
||||
"legrest_height": 0.656
|
||||
}
|
||||
},
|
||||
"result": "PASS"
|
||||
},
|
||||
"continuous_rollover": {
|
||||
"next_cycle_all_four_axes_active": true,
|
||||
"next_cycle_start_spread_s": 0.051,
|
||||
"result": "PASS"
|
||||
},
|
||||
"stop_verification": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"final_0x306": "00 00 00 00 00 00 00 00",
|
||||
"active_mask": 0,
|
||||
"fault_mask": 0,
|
||||
"result": "PASS"
|
||||
},
|
||||
"result": "PASS"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"validated_at": "2026-08-04",
|
||||
"firmware_revision": 9,
|
||||
"firmware_image_sha256": "216f3f6b2d7c441f2050e99c2b9d54153a69b46466235232dec1b7ab7644b7c4",
|
||||
"mode": "dynamic_cradle",
|
||||
"coordinate_precondition": {
|
||||
"motor": 1,
|
||||
"operation": "HOME_KNOWN_RANGE",
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"hall_before": 169,
|
||||
"hall_after": 0,
|
||||
"max_hall_preserved": 374,
|
||||
"near": true,
|
||||
"position_valid": true,
|
||||
"result": "PASS"
|
||||
},
|
||||
"full_cycle": {
|
||||
"entry_step_1": {
|
||||
"target_hall": [391, 140, 405, 404],
|
||||
"observed_hall": [392, 141, 405, 403]
|
||||
},
|
||||
"step_1_to_step_2": {
|
||||
"target_hall": [246, 281, 701, 494],
|
||||
"observed_hall": [246, 281, 701, 494],
|
||||
"duration_s": 17.329,
|
||||
"four_axis_start_spread_s": 0.077,
|
||||
"four_axis_arrival_spread_s": 0.338,
|
||||
"result": "PASS"
|
||||
},
|
||||
"step_2_to_step_3": {
|
||||
"target_hall": [391, 140, 405, 404],
|
||||
"observed_hall": [391, 140, 405, 405],
|
||||
"duration_s": 15.155,
|
||||
"four_axis_start_spread_s": 0.079,
|
||||
"four_axis_arrival_spread_s": 0.495,
|
||||
"result": "PASS"
|
||||
},
|
||||
"cycle_index_after_return": 1,
|
||||
"fault_mask": 0,
|
||||
"result": "PASS"
|
||||
},
|
||||
"continuous_rollover": {
|
||||
"cycle_0_forward_start_spread_s": 0.052,
|
||||
"cycle_0_return_start_spread_s": 0.053,
|
||||
"cycle_1_forward_start_spread_s": 0.082,
|
||||
"phase_transitions_s": [0.370, 17.888, 32.781],
|
||||
"result": "PASS"
|
||||
},
|
||||
"step_3_to_step_1_semantics": "Step 3 and Step 1 have identical Hall targets, so their boundary has zero physical displacement. The ECU rolls the cycle index atomically and synchronously launches the next Step 1-to-Step 2 leg.",
|
||||
"stop_verification": {
|
||||
"stop_dynamic_sent": true,
|
||||
"final_0x306": "00 00 00 00 00 00 00 00",
|
||||
"result": "PASS"
|
||||
},
|
||||
"frontend_natural_language_hil": {
|
||||
"start": {
|
||||
"utterance": "开启动态摇篮模式",
|
||||
"request_id": "req-20260804071706-45a1df",
|
||||
"result": "SUCCEEDED",
|
||||
"firmware_signature": "seat-controller-smv8-dynamic-modes-v9",
|
||||
"command": "0x300 0E 10 06 00 02 00 00 00",
|
||||
"ui_state": "动态摇篮模式 · 循环中 · 最长10分钟"
|
||||
},
|
||||
"active_physical_can_observation": {
|
||||
"source": "physical_can",
|
||||
"complete": true,
|
||||
"moving_axes": {
|
||||
"longitudinal_pct": 59,
|
||||
"backrest_recline_pct": 75
|
||||
},
|
||||
"all_axes_position_valid": true,
|
||||
"faults": false
|
||||
},
|
||||
"stop": {
|
||||
"utterance": "关闭动态摇篮模式",
|
||||
"request_id": "req-20260804072028-55179f",
|
||||
"result": "SUCCEEDED",
|
||||
"command": "0x300 0F 10 07 00 00 00 00 00",
|
||||
"ui_state": "未运行",
|
||||
"all_axes_stopped": true,
|
||||
"all_axes_position_valid": true,
|
||||
"faults": false
|
||||
},
|
||||
"hardware_access_after_test": "automatically released by the 180-second local idle policy",
|
||||
"result": "PASS"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"mode": "rocking_horse",
|
||||
"firmware_revision": 11,
|
||||
"max_hall": [
|
||||
499,
|
||||
374,
|
||||
917,
|
||||
647
|
||||
],
|
||||
"homes": [
|
||||
{
|
||||
"motor": 0,
|
||||
"hall_before": 349,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 21,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 0,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 21,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 1,
|
||||
"hall_before": 163,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 22,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 1,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 22,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 2,
|
||||
"hall_before": 534,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 23,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 2,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 23,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 3,
|
||||
"hall_before": 456,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 24,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 3,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 24,
|
||||
"position_x100": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"start_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 37,
|
||||
"opcode": 14,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 2,
|
||||
"phase_start": 262550.8121217,
|
||||
"start_hall": [
|
||||
320,
|
||||
191,
|
||||
500,
|
||||
431
|
||||
],
|
||||
"first_active": {
|
||||
"2": 262550.8883221,
|
||||
"3": 262550.9146271,
|
||||
"0": 262550.9401055,
|
||||
"1": 262550.9661217
|
||||
},
|
||||
"arrival": {
|
||||
"3": 262552.7830827,
|
||||
"1": 262552.8335206,
|
||||
"2": 262553.0696745,
|
||||
"0": 262553.1213695
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 0.6343701000441797,
|
||||
"hall": [
|
||||
312,
|
||||
201,
|
||||
481,
|
||||
417
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.26666666666666666,
|
||||
0.3448275862068966,
|
||||
0.475,
|
||||
0.45161290322580644
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
290,
|
||||
219,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"start_spread_s": 0.07779960002517328,
|
||||
"arrival_spread_s": 0.3382868000189774,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
290,
|
||||
220,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 3,
|
||||
"phase_start": 262553.4043395,
|
||||
"start_hall": [
|
||||
290,
|
||||
219,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"first_active": {
|
||||
"2": 262553.4973208,
|
||||
"3": 262553.5228996,
|
||||
"0": 262553.5485228,
|
||||
"1": 262553.5743734
|
||||
},
|
||||
"arrival": {
|
||||
"3": 262557.0745088,
|
||||
"1": 262557.1252219,
|
||||
"2": 262557.2577929,
|
||||
"0": 262557.3081852
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.079516199999489,
|
||||
"hall": [
|
||||
306,
|
||||
199,
|
||||
489,
|
||||
422
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.26666666666666666,
|
||||
0.3389830508474576,
|
||||
0.3625,
|
||||
0.36666666666666664
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
350,
|
||||
160,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"start_spread_s": 0.0770526000414975,
|
||||
"arrival_spread_s": 0.23367639997741207,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
350,
|
||||
160,
|
||||
540,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 2,
|
||||
"phase_start": 262557.4375185,
|
||||
"start_hall": [
|
||||
350,
|
||||
160,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"first_active": {
|
||||
"3": 262557.4893286,
|
||||
"0": 262557.5161744,
|
||||
"1": 262557.5424914,
|
||||
"2": 262557.5696487
|
||||
},
|
||||
"arrival": {
|
||||
"3": 262561.8903825,
|
||||
"1": 262561.9425291,
|
||||
"2": 262562.0729115,
|
||||
"0": 262562.1245799
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.337460899958387,
|
||||
"hall": [
|
||||
335,
|
||||
185,
|
||||
498,
|
||||
430
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.4166666666666667,
|
||||
0.525,
|
||||
0.5
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
290,
|
||||
220,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"start_spread_s": 0.08032010000897571,
|
||||
"arrival_spread_s": 0.23419739998644218,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
290,
|
||||
220,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 3,
|
||||
"phase_start": 262562.35135919997,
|
||||
"start_hall": [
|
||||
290,
|
||||
220,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"first_active": {
|
||||
"3": 262562.3911222,
|
||||
"0": 262562.4169718,
|
||||
"1": 262562.4437546,
|
||||
"2": 262562.4700679
|
||||
},
|
||||
"arrival": {
|
||||
"3": 262566.0670384,
|
||||
"1": 262566.1195433,
|
||||
"2": 262566.1444,
|
||||
"0": 262566.2991242
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.1310953000211157,
|
||||
"hall": [
|
||||
305,
|
||||
198,
|
||||
497,
|
||||
423
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.36666666666666664,
|
||||
0.4625,
|
||||
0.38333333333333336
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
350,
|
||||
160,
|
||||
540,
|
||||
459
|
||||
],
|
||||
"start_spread_s": 0.07894570002099499,
|
||||
"arrival_spread_s": 0.23208580003120005,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
350,
|
||||
160,
|
||||
540,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat_first_active": {
|
||||
"0": 262566.4901705,
|
||||
"1": 262566.5153454,
|
||||
"2": 262566.542698,
|
||||
"3": 262566.5681911
|
||||
},
|
||||
"stop_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 38,
|
||||
"opcode": 15,
|
||||
"firmware_revision": 11
|
||||
},
|
||||
"idle": {
|
||||
"mode_id": 0,
|
||||
"state": 0,
|
||||
"phase": 0,
|
||||
"active_mask": 0,
|
||||
"fault_mask": 0,
|
||||
"cycle_index": 0,
|
||||
"phase_elapsed_ms": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"mode": "rocking_horse",
|
||||
"firmware_revision": 12,
|
||||
"max_hall": [
|
||||
499,
|
||||
374,
|
||||
917,
|
||||
647
|
||||
],
|
||||
"homes": [
|
||||
{
|
||||
"motor": 0,
|
||||
"hall_before": 342,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 1,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 0,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 1,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 1,
|
||||
"hall_before": 166,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 2,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 1,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 2,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 2,
|
||||
"hall_before": 534,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 3,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 2,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 3,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 3,
|
||||
"hall_before": 454,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 4,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 3,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 4,
|
||||
"position_x100": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"start_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 17,
|
||||
"opcode": 14,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 2,
|
||||
"phase_start": 264077.9579478,
|
||||
"start_hall": [
|
||||
320,
|
||||
191,
|
||||
500,
|
||||
430
|
||||
],
|
||||
"first_active": {
|
||||
"0": 264078.038142,
|
||||
"1": 264078.0663315,
|
||||
"2": 264078.0774349,
|
||||
"3": 264078.1169224
|
||||
},
|
||||
"arrival": {
|
||||
"3": 264079.9439302,
|
||||
"1": 264079.9959276,
|
||||
"2": 264080.2325822,
|
||||
"0": 264080.2840808
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 0.6441632999922149,
|
||||
"hall": [
|
||||
312,
|
||||
214,
|
||||
481,
|
||||
416
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.26666666666666666,
|
||||
0.3898305084745763,
|
||||
0.475,
|
||||
0.4666666666666667
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"start_spread_s": 0.07878040004288778,
|
||||
"arrival_spread_s": 0.34015060000820085,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 3,
|
||||
"phase_start": 264080.62360189995,
|
||||
"start_hall": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"first_active": {
|
||||
"0": 264080.6481176,
|
||||
"1": 264080.6733371,
|
||||
"2": 264080.7001602,
|
||||
"3": 264080.7257557
|
||||
},
|
||||
"arrival": {
|
||||
"1": 264084.2944588,
|
||||
"3": 264084.344339,
|
||||
"0": 264084.475855,
|
||||
"2": 264084.5271018
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.0275715000461787,
|
||||
"hall": [
|
||||
305,
|
||||
206,
|
||||
492,
|
||||
421
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.36666666666666664,
|
||||
0.4,
|
||||
0.35
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
350,
|
||||
132,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"start_spread_s": 0.07763810001779348,
|
||||
"arrival_spread_s": 0.2326430000248365,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
350,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 2,
|
||||
"phase_start": 264084.65867120004,
|
||||
"start_hall": [
|
||||
350,
|
||||
132,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"first_active": {
|
||||
"2": 264084.7388101,
|
||||
"1": 264084.7410872,
|
||||
"0": 264084.7599586,
|
||||
"3": 264084.7926978
|
||||
},
|
||||
"arrival": {
|
||||
"3": 264089.0549796,
|
||||
"1": 264089.108405,
|
||||
"0": 264089.3941196,
|
||||
"2": 264089.4443601
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.391282099939417,
|
||||
"hall": [
|
||||
335,
|
||||
187,
|
||||
497,
|
||||
429
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.4661016949152542,
|
||||
0.5375,
|
||||
0.5166666666666667
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"start_spread_s": 0.05388769996352494,
|
||||
"arrival_spread_s": 0.38938050001161173,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 3,
|
||||
"phase_start": 264089.7385309,
|
||||
"start_hall": [
|
||||
290,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"first_active": {
|
||||
"1": 264089.857611,
|
||||
"2": 264089.8838599,
|
||||
"3": 264089.910015,
|
||||
"0": 264089.938548
|
||||
},
|
||||
"arrival": {
|
||||
"1": 264093.3849972,
|
||||
"3": 264093.4384044,
|
||||
"2": 264093.5165479,
|
||||
"0": 264093.5667763
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.011584700027015,
|
||||
"hall": [
|
||||
305,
|
||||
207,
|
||||
492,
|
||||
420
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.35833333333333334,
|
||||
0.4,
|
||||
0.3333333333333333
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
350,
|
||||
132,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"start_spread_s": 0.08093699999153614,
|
||||
"arrival_spread_s": 0.1817791000357829,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
350,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat_first_active": {
|
||||
"0": 264093.8496089,
|
||||
"2": 264093.8578157,
|
||||
"3": 264093.8843526,
|
||||
"1": 264093.9379308
|
||||
},
|
||||
"stop_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 18,
|
||||
"opcode": 15,
|
||||
"firmware_revision": 12
|
||||
},
|
||||
"idle": {
|
||||
"mode_id": 0,
|
||||
"state": 0,
|
||||
"phase": 0,
|
||||
"active_mask": 0,
|
||||
"fault_mask": 0,
|
||||
"cycle_index": 0,
|
||||
"phase_elapsed_ms": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
{
|
||||
"mode": "rocking_horse",
|
||||
"firmware_revision": 13,
|
||||
"max_hall": [
|
||||
499,
|
||||
374,
|
||||
917,
|
||||
647
|
||||
],
|
||||
"homes": [
|
||||
{
|
||||
"motor": 0,
|
||||
"hall_before": 326,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 1,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 0,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 1,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 1,
|
||||
"hall_before": 138,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 2,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 1,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 2,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 2,
|
||||
"hall_before": 532,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 3,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 2,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 3,
|
||||
"position_x100": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"motor": 3,
|
||||
"hall_before": 451,
|
||||
"hall_after": 0,
|
||||
"ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 4,
|
||||
"opcode": 11,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"terminal": {
|
||||
"motor": 3,
|
||||
"task": 6,
|
||||
"result": 2,
|
||||
"active": false,
|
||||
"position_valid": true,
|
||||
"sequence": 4,
|
||||
"position_x100": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"start_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 17,
|
||||
"opcode": 14,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"segments": [
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 2,
|
||||
"phase_start": 265437.2473564,
|
||||
"start_hall": [
|
||||
320,
|
||||
190,
|
||||
500,
|
||||
430
|
||||
],
|
||||
"first_active": {
|
||||
"2": 265437.2955111,
|
||||
"3": 265437.3198522,
|
||||
"0": 265437.346692,
|
||||
"1": 265437.3733125
|
||||
},
|
||||
"arrival": {
|
||||
"1": 265438.9329059,
|
||||
"3": 265438.9846889,
|
||||
"0": 265439.2201775,
|
||||
"2": 265439.2731793
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 0.5042391999741085,
|
||||
"hall": [
|
||||
306,
|
||||
209,
|
||||
486,
|
||||
419
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.28,
|
||||
0.31666666666666665,
|
||||
0.35,
|
||||
0.36666666666666664
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"start_spread_s": 0.07780140003887936,
|
||||
"arrival_spread_s": 0.34027340001193807,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 0,
|
||||
"phase": 3,
|
||||
"phase_start": 265439.64164610003,
|
||||
"start_hall": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
],
|
||||
"first_active": {
|
||||
"2": 265439.6948618,
|
||||
"3": 265439.7208077,
|
||||
"0": 265439.7458537,
|
||||
"1": 265439.7726268
|
||||
},
|
||||
"arrival": {
|
||||
"1": 265442.9056772,
|
||||
"3": 265442.9558483,
|
||||
"2": 265443.0339587,
|
||||
"0": 265443.0861803
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 0.8318679999792948,
|
||||
"hall": [
|
||||
295,
|
||||
216,
|
||||
484,
|
||||
417
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.25,
|
||||
0.2833333333333333,
|
||||
0.3,
|
||||
0.2833333333333333
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
370,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"start_spread_s": 0.07776499999454245,
|
||||
"arrival_spread_s": 0.18050309998216107,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
370,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 2,
|
||||
"phase_start": 265443.25305889995,
|
||||
"start_hall": [
|
||||
370,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"first_active": {
|
||||
"0": 265443.2954135,
|
||||
"1": 265443.3206536,
|
||||
"2": 265443.3348354,
|
||||
"3": 265443.3728105
|
||||
},
|
||||
"arrival": {
|
||||
"3": 265446.8221541,
|
||||
"1": 265446.8749014,
|
||||
"2": 265447.0045205,
|
||||
"0": 265447.159461
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 1.084834800043609,
|
||||
"hall": [
|
||||
343,
|
||||
175,
|
||||
507,
|
||||
436
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.27,
|
||||
0.375,
|
||||
0.4125,
|
||||
0.4
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
401
|
||||
],
|
||||
"start_spread_s": 0.07739699998637661,
|
||||
"arrival_spread_s": 0.3373069000081159,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
400
|
||||
]
|
||||
},
|
||||
{
|
||||
"cycle_index": 1,
|
||||
"phase": 3,
|
||||
"phase_start": 265447.3484032,
|
||||
"start_hall": [
|
||||
270,
|
||||
250,
|
||||
460,
|
||||
401
|
||||
],
|
||||
"first_active": {
|
||||
"1": 265447.3961179,
|
||||
"0": 265447.400082,
|
||||
"2": 265447.4222059,
|
||||
"3": 265447.4483388
|
||||
},
|
||||
"arrival": {
|
||||
"1": 265450.6434356,
|
||||
"3": 265450.693563,
|
||||
"2": 265450.7765666,
|
||||
"0": 265450.8258244
|
||||
},
|
||||
"cruise_checkpoint": {
|
||||
"elapsed_s": 0.9676259999978356,
|
||||
"hall": [
|
||||
297,
|
||||
209,
|
||||
490,
|
||||
421
|
||||
],
|
||||
"normalized_progress": [
|
||||
0.27,
|
||||
0.3416666666666667,
|
||||
0.375,
|
||||
0.3389830508474576
|
||||
]
|
||||
},
|
||||
"end_hall": [
|
||||
370,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
],
|
||||
"start_spread_s": 0.05222090001916513,
|
||||
"arrival_spread_s": 0.18238880002172664,
|
||||
"all_axes_started": true,
|
||||
"all_axes_arrived": true,
|
||||
"target": [
|
||||
370,
|
||||
130,
|
||||
540,
|
||||
460
|
||||
]
|
||||
}
|
||||
],
|
||||
"repeat_first_active": {
|
||||
"3": 265450.9977942,
|
||||
"0": 265451.025777,
|
||||
"2": 265451.0380102,
|
||||
"1": 265451.0497914
|
||||
},
|
||||
"stop_ack": {
|
||||
"ack": 1,
|
||||
"reject": 0,
|
||||
"sequence": 18,
|
||||
"opcode": 15,
|
||||
"firmware_revision": 13
|
||||
},
|
||||
"idle": {
|
||||
"mode_id": 0,
|
||||
"state": 0,
|
||||
"phase": 0,
|
||||
"active_mask": 0,
|
||||
"fault_mask": 0,
|
||||
"cycle_index": 0,
|
||||
"phase_elapsed_ms": 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,764 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T14:53:29.226676Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801145329",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MODE-QUEEN",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行女王副驾行驶模式",
|
||||
"started_at": "2026-08-01T14:53:29.226724Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145329-c441d5",
|
||||
"failure_telemetry": {
|
||||
"motor_selector": 0,
|
||||
"motion_axis": "longitudinal",
|
||||
"task_state": 7,
|
||||
"motor_result": 6,
|
||||
"motor_result_name": "TIMEOUT",
|
||||
"position_valid": true,
|
||||
"hall_before": 499,
|
||||
"hall_after": 499,
|
||||
"hall_delta": 0,
|
||||
"endpoint_recovery_max_near_hall": 72,
|
||||
"command_frame": {
|
||||
"frame_id": "0x300",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 10 30 00 20 1C 30 75",
|
||||
"timestamp": null
|
||||
},
|
||||
"ack_frame": {
|
||||
"frame_id": "0x301",
|
||||
"payload_size": 8,
|
||||
"data_hex": "21 00 30 00 01 01 F0 4B",
|
||||
"timestamp": 26444.37267
|
||||
},
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 67 28 30 00 10 27 00",
|
||||
"timestamp": 26450.902006
|
||||
},
|
||||
"diagnostic_before_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 F3 01 F3 01 00 90",
|
||||
"timestamp": 26444.360963
|
||||
},
|
||||
"diagnostic_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 05 F3 01 F3 01 00 A0",
|
||||
"timestamp": 26450.903153
|
||||
},
|
||||
"diagnostic_read_error": null,
|
||||
"current_diagnostic": {
|
||||
"sample_count": 65,
|
||||
"valid_sample_count": 65,
|
||||
"max_current_raw": 214,
|
||||
"max_current_delta_raw": 213,
|
||||
"high_current_seen": false,
|
||||
"low_hall_seen": true,
|
||||
"stall_overlap_seen": false,
|
||||
"peak": {
|
||||
"current_raw": 214,
|
||||
"current_baseline_raw": 1,
|
||||
"current_delta_raw": 213,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 0B D6 00 01 00 F4 01",
|
||||
"timestamp": 26449.53215
|
||||
}
|
||||
},
|
||||
"latest": {
|
||||
"current_raw": 212,
|
||||
"current_baseline_raw": 1,
|
||||
"current_delta_raw": 211,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 0B D4 00 01 00 F4 01",
|
||||
"timestamp": 26450.886864
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_type": "HardwareExecutionError",
|
||||
"stop_all_attempted": true,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T14:53:36.269029Z",
|
||||
"duration_ms": 7042
|
||||
},
|
||||
{
|
||||
"id": "MODE-BABY-NAP",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行宝宝午睡模式",
|
||||
"status": "safety_blocked",
|
||||
"started_at": "2026-08-01T14:53:36.269096Z",
|
||||
"finished_at": "2026-08-01T14:53:36.269098Z",
|
||||
"duration_ms": 0,
|
||||
"errors": [],
|
||||
"safety_block": {
|
||||
"axis": "cushion_tilt",
|
||||
"target_pct": 30,
|
||||
"minimum_safe_target_pct": 35,
|
||||
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
|
||||
"evidence": {
|
||||
"hall_count": 122,
|
||||
"pulse_count": 0,
|
||||
"peak_current_delta_raw": 1205,
|
||||
"stall_threshold_raw": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MODE-CRUISE-SOFA",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行巡航沙发模式",
|
||||
"status": "safety_blocked",
|
||||
"started_at": "2026-08-01T14:53:36.269107Z",
|
||||
"finished_at": "2026-08-01T14:53:36.269108Z",
|
||||
"duration_ms": 0,
|
||||
"errors": [],
|
||||
"safety_block": {
|
||||
"axis": "cushion_tilt",
|
||||
"target_pct": 30,
|
||||
"minimum_safe_target_pct": 35,
|
||||
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
|
||||
"evidence": {
|
||||
"hall_count": 122,
|
||||
"pulse_count": 0,
|
||||
"peak_current_delta_raw": 1205,
|
||||
"stall_threshold_raw": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MODE-MEDITATION",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行冥想空间模式",
|
||||
"started_at": "2026-08-01T14:53:36.269121Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"T2G motor 1 STALL: task_state=7, Hall=374->374"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145336-cea6e3",
|
||||
"failure_telemetry": {
|
||||
"motor_selector": 1,
|
||||
"motion_axis": "cushion_tilt",
|
||||
"task_state": 7,
|
||||
"motor_result": 5,
|
||||
"motor_result_name": "STALL",
|
||||
"position_valid": true,
|
||||
"hall_before": 374,
|
||||
"hall_after": 374,
|
||||
"hall_delta": 0,
|
||||
"endpoint_recovery_max_near_hall": 56,
|
||||
"command_frame": {
|
||||
"frame_id": "0x300",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 11 34 00 30 11 30 75",
|
||||
"timestamp": null
|
||||
},
|
||||
"ack_frame": {
|
||||
"frame_id": "0x301",
|
||||
"payload_size": 8,
|
||||
"data_hex": "21 00 34 00 01 02 F0 49",
|
||||
"timestamp": 26453.41257
|
||||
},
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 57 68 34 00 10 27 00",
|
||||
"timestamp": 26462.801669
|
||||
},
|
||||
"diagnostic_before_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 76 01 76 01 00 B0",
|
||||
"timestamp": 26451.441787
|
||||
},
|
||||
"diagnostic_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 14 76 01 76 01 00 C1",
|
||||
"timestamp": 26462.802815
|
||||
},
|
||||
"diagnostic_read_error": null,
|
||||
"current_diagnostic": {
|
||||
"sample_count": 113,
|
||||
"valid_sample_count": 113,
|
||||
"max_current_raw": 1353,
|
||||
"max_current_delta_raw": 1351,
|
||||
"high_current_seen": true,
|
||||
"low_hall_seen": true,
|
||||
"stall_overlap_seen": true,
|
||||
"peak": {
|
||||
"current_raw": 1353,
|
||||
"current_baseline_raw": 2,
|
||||
"current_delta_raw": 1351,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": true,
|
||||
"hall_activity_low": false,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 07 49 05 02 00 F4 01",
|
||||
"timestamp": 26461.7585
|
||||
}
|
||||
},
|
||||
"latest": {
|
||||
"current_raw": 1199,
|
||||
"current_baseline_raw": 2,
|
||||
"current_delta_raw": 1197,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": true,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 0F AF 04 02 00 F4 01",
|
||||
"timestamp": 26462.798972
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_type": "HardwareExecutionError",
|
||||
"stop_all_attempted": true,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T14:53:48.119388Z",
|
||||
"duration_ms": 11850
|
||||
},
|
||||
{
|
||||
"id": "MODE-LEISURE",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行休闲躺椅模式",
|
||||
"status": "safety_blocked",
|
||||
"started_at": "2026-08-01T14:53:48.119450Z",
|
||||
"finished_at": "2026-08-01T14:53:48.119452Z",
|
||||
"duration_ms": 0,
|
||||
"errors": [],
|
||||
"safety_block": {
|
||||
"axis": "cushion_tilt",
|
||||
"target_pct": 30,
|
||||
"minimum_safe_target_pct": 35,
|
||||
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
|
||||
"evidence": {
|
||||
"hall_count": 122,
|
||||
"pulse_count": 0,
|
||||
"peak_current_delta_raw": 1205,
|
||||
"stall_threshold_raw": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MODE-CALF",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行小腿解压悬浮模式",
|
||||
"started_at": "2026-08-01T14:53:48.119461Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"T2G motor 0 TIMEOUT: task_state=7, Hall=499->499"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145348-bae676",
|
||||
"failure_telemetry": {
|
||||
"motor_selector": 0,
|
||||
"motion_axis": "longitudinal",
|
||||
"task_state": 7,
|
||||
"motor_result": 6,
|
||||
"motor_result_name": "TIMEOUT",
|
||||
"position_valid": true,
|
||||
"hall_before": 499,
|
||||
"hall_after": 499,
|
||||
"hall_delta": 0,
|
||||
"endpoint_recovery_max_near_hall": 72,
|
||||
"command_frame": {
|
||||
"frame_id": "0x300",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 10 37 00 EC 13 30 75",
|
||||
"timestamp": null
|
||||
},
|
||||
"ack_frame": {
|
||||
"frame_id": "0x301",
|
||||
"payload_size": 8,
|
||||
"data_hex": "21 00 37 00 01 01 F0 41",
|
||||
"timestamp": 26463.239675
|
||||
},
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 67 28 37 00 10 27 00",
|
||||
"timestamp": 26469.752618
|
||||
},
|
||||
"diagnostic_before_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 F3 01 F3 01 00 10",
|
||||
"timestamp": 26463.237716
|
||||
},
|
||||
"diagnostic_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 05 F3 01 F3 01 00 20",
|
||||
"timestamp": 26469.754985
|
||||
},
|
||||
"diagnostic_read_error": null,
|
||||
"current_diagnostic": {
|
||||
"sample_count": 65,
|
||||
"valid_sample_count": 65,
|
||||
"max_current_raw": 253,
|
||||
"max_current_delta_raw": 250,
|
||||
"high_current_seen": false,
|
||||
"low_hall_seen": true,
|
||||
"stall_overlap_seen": false,
|
||||
"peak": {
|
||||
"current_raw": 253,
|
||||
"current_baseline_raw": 3,
|
||||
"current_delta_raw": 250,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": false,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 03 FD 00 03 00 F4 01",
|
||||
"timestamp": 26463.272395
|
||||
}
|
||||
},
|
||||
"latest": {
|
||||
"current_raw": 207,
|
||||
"current_baseline_raw": 3,
|
||||
"current_delta_raw": 204,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 0B CF 00 03 00 F4 01",
|
||||
"timestamp": 26469.728393
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_type": "HardwareExecutionError",
|
||||
"stop_all_attempted": true,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T14:53:55.135955Z",
|
||||
"duration_ms": 7017
|
||||
},
|
||||
{
|
||||
"id": "MODE-BEACH",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行电感沙滩椅模式",
|
||||
"status": "safety_blocked",
|
||||
"started_at": "2026-08-01T14:53:55.136004Z",
|
||||
"finished_at": "2026-08-01T14:53:55.136006Z",
|
||||
"duration_ms": 0,
|
||||
"errors": [],
|
||||
"safety_block": {
|
||||
"axis": "cushion_tilt",
|
||||
"target_pct": 0,
|
||||
"minimum_safe_target_pct": 35,
|
||||
"reason": "M1 near-direction mechanical obstruction: repeated Hall stop at 122/123 with zero pulses and high current; do not drive toward the near endpoint until the mechanism is inspected.",
|
||||
"evidence": {
|
||||
"hall_count": 122,
|
||||
"pulse_count": 0,
|
||||
"peak_current_delta_raw": 1205,
|
||||
"stall_threshold_raw": 500
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "MODE-NAP",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行小憩模式",
|
||||
"started_at": "2026-08-01T14:53:55.136014Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145355-391520",
|
||||
"matched_modes": [
|
||||
"nap"
|
||||
],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 28 3A 00 10 27 00",
|
||||
"timestamp": 26476.732654
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 28 3B 00 10 27 00",
|
||||
"timestamp": 26476.759174
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 3C 00 A7 1F 00",
|
||||
"timestamp": 26476.785245
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 08 3D 00 B5 24 00",
|
||||
"timestamp": 26476.810211
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26476.820464
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26476.820464
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T14:54:01.990826Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T14:54:02.155719Z",
|
||||
"duration_ms": 7020
|
||||
},
|
||||
{
|
||||
"id": "MODE-ZERO-GRAVITY",
|
||||
"group": "mode",
|
||||
"utterance": "副驾执行零重力模式",
|
||||
"started_at": "2026-08-01T14:54:02.155796Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145402-f6ff8b",
|
||||
"matched_modes": [
|
||||
"zero_gravity"
|
||||
],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 28 3F 00 10 27 00",
|
||||
"timestamp": 26483.63127
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 28 40 00 10 27 00",
|
||||
"timestamp": 26483.657826
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 41 00 A7 1F 00",
|
||||
"timestamp": 26483.682735
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 08 42 00 B5 24 00",
|
||||
"timestamp": 26483.708846
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26483.717983
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26483.717983
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T14:54:08.888287Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T14:54:08.963843Z",
|
||||
"duration_ms": 6808
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T14:54:08.963900Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145408-c833b5",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 08 42 00 B5 24 00",
|
||||
"timestamp": 26484.485071
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 28 3F 00 10 27 00",
|
||||
"timestamp": 26484.511148
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 28 40 00 10 27 00",
|
||||
"timestamp": 26484.537248
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 41 00 A7 1F 00",
|
||||
"timestamp": 26484.562609
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26484.572862
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26484.572862
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T14:54:09.742991Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T14:54:09.821001Z",
|
||||
"duration_ms": 857
|
||||
},
|
||||
"finished_at": "2026-08-01T14:54:09.821038Z",
|
||||
"summary": {
|
||||
"total": 9,
|
||||
"passed": 2,
|
||||
"failed": 3,
|
||||
"safety_blocked": 4,
|
||||
"executed": 5,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T14:55:16.839705Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801145516",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-M0-HOME",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾座椅水平位置归零",
|
||||
"started_at": "2026-08-01T14:55:16.839746Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145516-496358",
|
||||
"failure_telemetry": {
|
||||
"motor_selector": 0,
|
||||
"motion_axis": "longitudinal",
|
||||
"task_state": 7,
|
||||
"motor_result": 11,
|
||||
"motor_result_name": "CALIBRATION_FAILED",
|
||||
"position_valid": false,
|
||||
"hall_before": 499,
|
||||
"hall_after": 499,
|
||||
"hall_delta": 0,
|
||||
"endpoint_recovery_max_near_hall": 72,
|
||||
"command_frame": {
|
||||
"frame_id": "0x300",
|
||||
"payload_size": 8,
|
||||
"data_hex": "0B 10 44 00 52 4B 78 00",
|
||||
"timestamp": null
|
||||
},
|
||||
"ack_frame": {
|
||||
"frame_id": "0x301",
|
||||
"payload_size": 8,
|
||||
"data_hex": "51 00 44 00 0B 41 F0 4F",
|
||||
"timestamp": 26552.027376
|
||||
},
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 B7 00 44 00 FF FF 00",
|
||||
"timestamp": 26555.266451
|
||||
},
|
||||
"diagnostic_before_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 F3 01 F3 01 00 C0",
|
||||
"timestamp": 26552.016761
|
||||
},
|
||||
"diagnostic_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 88 F3 01 F3 01 00 00",
|
||||
"timestamp": 26555.267587
|
||||
},
|
||||
"diagnostic_read_error": null,
|
||||
"current_diagnostic": {
|
||||
"sample_count": 36,
|
||||
"valid_sample_count": 36,
|
||||
"max_current_raw": 223,
|
||||
"max_current_delta_raw": 220,
|
||||
"high_current_seen": false,
|
||||
"low_hall_seen": true,
|
||||
"stall_overlap_seen": false,
|
||||
"peak": {
|
||||
"current_raw": 223,
|
||||
"current_baseline_raw": 3,
|
||||
"current_delta_raw": 220,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": false,
|
||||
"current_calibrating": true,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 13 DF 00 03 00 F4 01",
|
||||
"timestamp": 26552.116307
|
||||
}
|
||||
},
|
||||
"latest": {
|
||||
"current_raw": 10,
|
||||
"current_baseline_raw": 23,
|
||||
"current_delta_raw": 0,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 0B 0A 00 17 00 F4 01",
|
||||
"timestamp": 26555.246382
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_type": "HardwareExecutionError",
|
||||
"stop_all_attempted": true,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T14:55:20.627987Z",
|
||||
"duration_ms": 3788
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T14:55:20.628029Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801145520-65fe81",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 30 08 45 00 A7 1F 00",
|
||||
"timestamp": 26556.1968
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 30 08 45 00 B5 24 00",
|
||||
"timestamp": 26556.222629
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": false,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 65535,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 30 00 45 00 FF FF 00",
|
||||
"timestamp": 26556.248442
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 30 28 45 00 10 27 00",
|
||||
"timestamp": 26556.274646
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26556.283681
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 26556.283681
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T14:55:21.452124Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T14:55:21.531805Z",
|
||||
"duration_ms": 904
|
||||
},
|
||||
"finished_at": "2026-08-01T14:55:21.531835Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T15:32:33.035150Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801153233",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-M0-HOME",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾座椅水平位置归零",
|
||||
"started_at": "2026-08-01T15:32:33.035187Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801153233-4d8760",
|
||||
"failure_telemetry": {
|
||||
"error_type": "CanGatewayError",
|
||||
"stop_all_attempted": false,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T15:32:33.233128Z",
|
||||
"duration_ms": 198
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T15:32:33.233166Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"ZCAN_SetValue(canfd_abit_baud_rate) failed"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801153233-dc02d8",
|
||||
"failure_telemetry": {
|
||||
"error_type": "CanGatewayError",
|
||||
"stop_all_attempted": false,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T15:32:33.390312Z",
|
||||
"duration_ms": 157
|
||||
},
|
||||
"finished_at": "2026-08-01T15:32:33.390330Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T15:38:36.967401Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801153836",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-M0-HOME",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾座椅水平位置归零",
|
||||
"started_at": "2026-08-01T15:38:36.967442Z",
|
||||
"status": "failed",
|
||||
"errors": [
|
||||
"T2G motor 0 CALIBRATION_FAILED: task_state=7, Hall=499->499"
|
||||
],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801153836-3cd030",
|
||||
"failure_telemetry": {
|
||||
"motor_selector": 0,
|
||||
"motion_axis": "longitudinal",
|
||||
"task_state": 7,
|
||||
"motor_result": 11,
|
||||
"motor_result_name": "CALIBRATION_FAILED",
|
||||
"position_valid": false,
|
||||
"hall_before": 499,
|
||||
"hall_after": 499,
|
||||
"hall_delta": 0,
|
||||
"endpoint_recovery_max_near_hall": 72,
|
||||
"command_frame": {
|
||||
"frame_id": "0x300",
|
||||
"payload_size": 8,
|
||||
"data_hex": "0B 10 02 00 52 4B 78 00",
|
||||
"timestamp": null
|
||||
},
|
||||
"ack_frame": {
|
||||
"frame_id": "0x301",
|
||||
"payload_size": 8,
|
||||
"data_hex": "51 00 02 00 0B 41 E0 55",
|
||||
"timestamp": 29165.165292
|
||||
},
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 B7 00 02 00 FF FF 00",
|
||||
"timestamp": 29168.401573
|
||||
},
|
||||
"diagnostic_before_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 F3 01 F3 01 00 20",
|
||||
"timestamp": 29165.154003
|
||||
},
|
||||
"diagnostic_frame": {
|
||||
"frame_id": "0x303",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 88 F3 01 F3 01 00 60",
|
||||
"timestamp": 29168.402701
|
||||
},
|
||||
"diagnostic_read_error": null,
|
||||
"current_diagnostic": {
|
||||
"sample_count": 36,
|
||||
"valid_sample_count": 36,
|
||||
"max_current_raw": 238,
|
||||
"max_current_delta_raw": 233,
|
||||
"high_current_seen": false,
|
||||
"low_hall_seen": true,
|
||||
"stall_overlap_seen": false,
|
||||
"peak": {
|
||||
"current_raw": 236,
|
||||
"current_baseline_raw": 3,
|
||||
"current_delta_raw": 233,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": false,
|
||||
"current_calibrating": true,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 13 EC 00 03 00 F4 01",
|
||||
"timestamp": 29165.251397
|
||||
}
|
||||
},
|
||||
"latest": {
|
||||
"current_raw": 12,
|
||||
"current_baseline_raw": 36,
|
||||
"current_delta_raw": 0,
|
||||
"stall_current_delta_threshold_raw": 500,
|
||||
"current_sample_valid": true,
|
||||
"current_baseline_valid": true,
|
||||
"high_current": false,
|
||||
"hall_activity_low": true,
|
||||
"current_calibrating": false,
|
||||
"frame": {
|
||||
"frame_id": "0x304",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 0B 0C 00 24 00 F4 01",
|
||||
"timestamp": 29168.379476
|
||||
}
|
||||
}
|
||||
},
|
||||
"error_type": "HardwareExecutionError",
|
||||
"stop_all_attempted": true,
|
||||
"simulated": false
|
||||
},
|
||||
"finished_at": "2026-08-01T15:38:53.697878Z",
|
||||
"duration_ms": 16730
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T15:38:53.697934Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801153853-1d639f",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 30 28 03 00 10 27 00",
|
||||
"timestamp": 29169.295418
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 30 08 03 00 A7 1F 00",
|
||||
"timestamp": 29169.322276
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 30 08 03 00 B5 24 00",
|
||||
"timestamp": 29169.347323
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": false,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 65535,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 30 00 03 00 FF FF 00",
|
||||
"timestamp": 29169.374233
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29169.380975
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29169.380975
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T15:38:54.491848Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T15:38:54.576238Z",
|
||||
"duration_ms": 878
|
||||
},
|
||||
"finished_at": "2026-08-01T15:38:54.576286Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 0,
|
||||
"failed": 1,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T15:43:36.160681Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801154336",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "THERM-H-OFF",
|
||||
"group": "thermal",
|
||||
"utterance": "关闭副驾座椅加热",
|
||||
"started_at": "2026-08-01T15:43:36.160725Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801154336-0eefe8",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 08 00 00 A7 1F 00",
|
||||
"timestamp": 29463.233038
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 08 00 00 B5 24 00",
|
||||
"timestamp": 29463.259912
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": false,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 65535,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 00 00 00 FF FF 00",
|
||||
"timestamp": 29463.284973
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 28 00 00 10 27 00",
|
||||
"timestamp": 29463.311881
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29463.319822
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29463.319822
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T15:43:48.424601Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T15:43:48.512170Z",
|
||||
"duration_ms": 12351
|
||||
},
|
||||
{
|
||||
"id": "THERM-V-OFF",
|
||||
"group": "thermal",
|
||||
"utterance": "关闭副驾座椅通风",
|
||||
"started_at": "2026-08-01T15:43:48.512241Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801154348-d8018c",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 08 00 00 A7 1F 00",
|
||||
"timestamp": 29464.079538
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 08 00 00 B5 24 00",
|
||||
"timestamp": 29464.105253
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": false,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 65535,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 00 00 00 FF FF 00",
|
||||
"timestamp": 29464.131102
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 28 00 00 10 27 00",
|
||||
"timestamp": 29464.157968
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29464.165819
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29464.165819
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T15:43:49.270734Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T15:43:49.336080Z",
|
||||
"duration_ms": 824
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T15:43:49.336131Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801154349-f42e7f",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 100,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 10000,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 28 00 00 10 27 00",
|
||||
"timestamp": 29464.904964
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 81,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 8103,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 08 00 00 A7 1F 00",
|
||||
"timestamp": 29464.931901
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 94,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 9397,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 08 00 00 B5 24 00",
|
||||
"timestamp": 29464.95768
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 100,
|
||||
"position_valid": false,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 65535,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 00 00 00 FF FF 00",
|
||||
"timestamp": 29464.98386
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29464.990659
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 29464.990659
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T15:43:50.095435Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T15:43:50.176751Z",
|
||||
"duration_ms": 841
|
||||
},
|
||||
"finished_at": "2026-08-01T15:43:50.176781Z",
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"passed": 2,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 2,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T17:39:16.430915Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801173916",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-ALL-ZERO",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T17:39:16.430950Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801173916-90206d",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 6D 01 00 00 00",
|
||||
"timestamp": 36459.051975
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 6E 01 00 00 00",
|
||||
"timestamp": 36459.077734
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 6F 01 00 00 00",
|
||||
"timestamp": 36459.104324
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 6C 01 00 00 00",
|
||||
"timestamp": 36459.129781
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36459.136472
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36459.136472
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:40:24.101369Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:40:24.299645Z",
|
||||
"duration_ms": 67869
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T17:40:24.299777Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801174024-2bef93",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 6C 01 00 00 00",
|
||||
"timestamp": 36460.001759
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 6D 01 00 00 00",
|
||||
"timestamp": 36460.028327
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 6E 01 00 00 00",
|
||||
"timestamp": 36460.055218
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 6F 01 00 00 00",
|
||||
"timestamp": 36460.08066
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36460.087354
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36460.087354
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:40:25.052281Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:40:25.130245Z",
|
||||
"duration_ms": 830
|
||||
},
|
||||
"finished_at": "2026-08-01T17:40:25.130276Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,823 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-comprehensive-hil",
|
||||
"started_at": "2026-08-01T22:06:10.662623Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801220610",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-M0-ABS-DECIMAL-ROUND",
|
||||
"group": "motion",
|
||||
"utterance": "副驾座椅水平位置调到50.5%",
|
||||
"started_at": "2026-08-01T22:06:33.819391Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220633-35a9ee",
|
||||
"provider_used": "local_rules",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 30 18 0B 00 00 00 00",
|
||||
"timestamp": 52437.943305
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 30 18 0B 00 00 00 00",
|
||||
"timestamp": 52437.968795
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 51,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 5110,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 08 0C 00 F6 13 00",
|
||||
"timestamp": 52437.995098
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 30 18 0B 00 00 00 00",
|
||||
"timestamp": 52438.021395
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52438.031724
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52438.031724
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:06:42.683267Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:06:42.768028Z",
|
||||
"duration_ms": 8949
|
||||
},
|
||||
{
|
||||
"id": "MOTION-M0-ABS-NEGATIVE-CLAMP",
|
||||
"group": "motion",
|
||||
"utterance": "副驾座椅水平位置调到-10%",
|
||||
"started_at": "2026-08-01T22:06:42.768083Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220642-ffc18b",
|
||||
"provider_used": "local_rules",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 30 18 0D 00 00 00 00",
|
||||
"timestamp": 52448.169066
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 30 18 0D 00 00 00 00",
|
||||
"timestamp": 52448.196
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 30 18 0D 00 00 00 00",
|
||||
"timestamp": 52448.221821
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 0E 00 00 00 00",
|
||||
"timestamp": 52448.247656
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52448.255615
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52448.255615
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:06:52.906777Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:06:52.966157Z",
|
||||
"duration_ms": 10198
|
||||
},
|
||||
{
|
||||
"id": "MOTION-M2-TIMED-ZERO-MIN-CLAMP",
|
||||
"group": "motion",
|
||||
"utterance": "副驾靠背向后移动0秒",
|
||||
"started_at": "2026-08-01T22:06:52.966239Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220652-ebe458",
|
||||
"provider_used": "local_rules",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 30 18 0F 00 00 00 00",
|
||||
"timestamp": 52449.295516
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 30 18 0F 00 00 00 00",
|
||||
"timestamp": 52449.321809
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 1,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 120,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 10 00 78 00 00",
|
||||
"timestamp": 52449.347017
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 3,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 30 18 0F 00 00 00 00",
|
||||
"timestamp": 52449.37364
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52449.382777
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52449.382777
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:06:54.033862Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:06:54.110745Z",
|
||||
"duration_ms": 1145
|
||||
},
|
||||
{
|
||||
"id": "MODEL-FUZZY-COMFORT-ONLY",
|
||||
"group": "model",
|
||||
"utterance": "把座椅调舒服一点",
|
||||
"started_at": "2026-08-01T22:06:54.110804Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220654-5f0af4",
|
||||
"provider_used": "openai:gpt-5.6-sol",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"legrest_height": {
|
||||
"position_pct": 10,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 1005,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 08 15 00 ED 03 00",
|
||||
"timestamp": 52491.95849
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 80,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 8016,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 08 12 00 50 1F 00",
|
||||
"timestamp": 52491.984306
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 30,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 2995,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 08 13 00 B3 0B 00",
|
||||
"timestamp": 52492.010881
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 35,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 3501,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 14 00 AD 0D 00",
|
||||
"timestamp": 52492.036223
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52492.046648
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52492.046648
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:07:36.697054Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:07:36.773665Z",
|
||||
"duration_ms": 42663
|
||||
},
|
||||
{
|
||||
"id": "MODEL-UNKNOWN-BOSS-MODE",
|
||||
"group": "model",
|
||||
"utterance": "打开老板模式",
|
||||
"started_at": "2026-08-01T22:07:36.773730Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220736-be8392",
|
||||
"provider_used": "openai:gpt-5.6-sol",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 1A 00 00 00 00",
|
||||
"timestamp": 52526.819003
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 20,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 2004,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 08 17 00 D4 07 00",
|
||||
"timestamp": 52526.845094
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 30,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 2995,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 08 18 00 B3 0B 00",
|
||||
"timestamp": 52526.87121
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 10,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 1003,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 19 00 EB 03 00",
|
||||
"timestamp": 52526.897575
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52526.90671
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52526.90671
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:08:11.556208Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:08:11.634298Z",
|
||||
"duration_ms": 34861
|
||||
}
|
||||
],
|
||||
"position_memory_sync": {
|
||||
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
|
||||
"group": "preflight",
|
||||
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T22:06:10.662651Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220610-fe1cde",
|
||||
"provider_used": "local_rules",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 0A 00 00 00 00",
|
||||
"timestamp": 52428.988603
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 07 00 00 00 00",
|
||||
"timestamp": 52429.015565
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 08 00 00 00 00",
|
||||
"timestamp": 52429.041376
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 09 00 00 00 00",
|
||||
"timestamp": 52429.06754
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52429.075411
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52429.075411
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:06:33.726921Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:06:33.819288Z",
|
||||
"duration_ms": 23157
|
||||
},
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T22:08:11.634351Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801220811-9d1330",
|
||||
"provider_used": "local_rules",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 1D 00 00 00 00",
|
||||
"timestamp": 52552.285415
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 1E 00 00 00 00",
|
||||
"timestamp": 52552.311577
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 1F 00 00 00 00",
|
||||
"timestamp": 52552.337346
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 1C 00 00 00 00",
|
||||
"timestamp": 52552.364354
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52552.372264
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 52552.372264
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T22:08:37.021239Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T22:08:37.100192Z",
|
||||
"duration_ms": 25466
|
||||
},
|
||||
"finished_at": "2026-08-01T22:08:37.100232Z",
|
||||
"summary": {
|
||||
"total": 5,
|
||||
"passed": 5,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 5,
|
||||
"position_memory_sync_passed": true,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-comprehensive-hil",
|
||||
"started_at": "2026-08-01T20:30:03.347380Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801203003",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MODEL-FUZZY-CRAMPED-COMFORT",
|
||||
"group": "model",
|
||||
"utterance": "前舱空间太挤了,把座椅调整到舒服的位置",
|
||||
"started_at": "2026-08-01T20:30:43.772650Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801203043-6dba03",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 35,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 3501,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 08 09 00 AD 0D 00",
|
||||
"timestamp": 46720.941274
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 10,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 1005,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 08 0A 00 ED 03 00",
|
||||
"timestamp": 46720.967852
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 80,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 8016,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 08 07 00 50 1F 00",
|
||||
"timestamp": 46720.993263
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 30,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 2995,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 08 08 00 B3 0B 00",
|
||||
"timestamp": 46721.019784
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46721.030217
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46721.030217
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T20:31:25.795596Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T20:31:25.863040Z",
|
||||
"duration_ms": 42090
|
||||
}
|
||||
],
|
||||
"position_memory_sync": {
|
||||
"id": "PREFLIGHT-POSITION-MEMORY-SYNC",
|
||||
"group": "preflight",
|
||||
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T20:30:03.347411Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801203003-f00e7a",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 03 00 00 00 00",
|
||||
"timestamp": 46678.830874
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 04 00 00 00 00",
|
||||
"timestamp": 46678.856952
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 05 00 00 00 00",
|
||||
"timestamp": 46678.882795
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 02 00 00 00 00",
|
||||
"timestamp": 46678.908649
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46678.916532
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46678.916532
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T20:30:43.683001Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T20:30:43.772568Z",
|
||||
"duration_ms": 40425
|
||||
},
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风,并把水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T20:31:25.863092Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801203125-a4af68",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 0C 00 00 00 00",
|
||||
"timestamp": 46759.956435
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 0D 00 00 00 00",
|
||||
"timestamp": 46759.982688
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 0E 00 00 00 00",
|
||||
"timestamp": 46760.008938
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 0F 00 00 00 00",
|
||||
"timestamp": 46760.041009
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46760.053878
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 46760.053878
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T20:32:04.818553Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T20:32:04.895597Z",
|
||||
"duration_ms": 39033
|
||||
},
|
||||
"finished_at": "2026-08-01T20:32:04.895635Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"position_memory_sync_passed": true,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T17:16:40.264559Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801171640",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-ALL-ZERO",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T17:16:40.264614Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801171640-4a5f1e",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 19 01 00 00 00",
|
||||
"timestamp": 35105.608647
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 1A 01 00 00 00",
|
||||
"timestamp": 35105.634423
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 17 01 00 00 00",
|
||||
"timestamp": 35105.661016
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 18 01 00 00 00",
|
||||
"timestamp": 35105.68652
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35105.692065
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35105.692065
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:17:50.683023Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:17:50.762718Z",
|
||||
"duration_ms": 70498
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T17:17:50.762772Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801171750-2f0f2f",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 19 01 00 00 00",
|
||||
"timestamp": 35106.475011
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 1A 01 00 00 00",
|
||||
"timestamp": 35106.500476
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 17 01 00 00 00",
|
||||
"timestamp": 35106.526322
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 18 01 00 00 00",
|
||||
"timestamp": 35106.552849
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35106.558466
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35106.558466
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:17:51.549417Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:17:51.636183Z",
|
||||
"duration_ms": 873
|
||||
},
|
||||
"finished_at": "2026-08-01T17:17:51.636217Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T16:40:47.172484Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801164047",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "THERM-H-OFF",
|
||||
"group": "thermal",
|
||||
"utterance": "关闭副驾座椅加热",
|
||||
"started_at": "2026-08-01T16:40:47.172525Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801164047-8cddb4",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 18 00 00 00 00 00",
|
||||
"timestamp": 32897.543083
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 18 00 00 00 00 00",
|
||||
"timestamp": 32897.568506
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 18 00 00 00 00 00",
|
||||
"timestamp": 32897.595022
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 18 00 00 00 00 00",
|
||||
"timestamp": 32897.620753
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32897.62524
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32897.62524
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T16:41:02.659736Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T16:41:02.738362Z",
|
||||
"duration_ms": 15566
|
||||
},
|
||||
{
|
||||
"id": "THERM-V-OFF",
|
||||
"group": "thermal",
|
||||
"utterance": "关闭副驾座椅通风",
|
||||
"started_at": "2026-08-01T16:41:02.738421Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801164102-401927",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 18 00 00 00 00 00",
|
||||
"timestamp": 32898.36913
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 18 00 00 00 00 00",
|
||||
"timestamp": 32898.394558
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 18 00 00 00 00 00",
|
||||
"timestamp": 32898.421367
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 18 00 00 00 00 00",
|
||||
"timestamp": 32898.446823
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32898.453497
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32898.453497
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T16:41:03.487981Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T16:41:03.570272Z",
|
||||
"duration_ms": 832
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T16:41:03.570317Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801164103-4efef7",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 00 18 00 00 00 00 00",
|
||||
"timestamp": 32899.183035
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 00 18 00 00 00 00 00",
|
||||
"timestamp": 32899.209498
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 00 18 00 00 00 00 00",
|
||||
"timestamp": 32899.23527
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 0,
|
||||
"motor_result": 0,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 00 18 00 00 00 00 00",
|
||||
"timestamp": 32899.260679
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32899.267388
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 32899.267388
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T16:41:04.301854Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T16:41:04.424088Z",
|
||||
"duration_ms": 854
|
||||
},
|
||||
"finished_at": "2026-08-01T16:41:04.424118Z",
|
||||
"summary": {
|
||||
"total": 2,
|
||||
"passed": 2,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 2,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T17:32:24.627065Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801173224",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-ALL-ZERO",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T17:32:24.627106Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801173224-cedf76",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 30 01 00 00 00",
|
||||
"timestamp": 36047.100786
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 31 01 00 00 00",
|
||||
"timestamp": 36047.126273
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 32 01 00 00 00",
|
||||
"timestamp": 36047.152829
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 33 01 00 00 00",
|
||||
"timestamp": 36047.178304
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36047.185019
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36047.185019
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:33:32.157834Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:33:32.231664Z",
|
||||
"duration_ms": 67605
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T17:33:32.231720Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801173332-3d22e2",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 30 01 00 00 00",
|
||||
"timestamp": 36047.928676
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 31 01 00 00 00",
|
||||
"timestamp": 36047.954191
|
||||
}
|
||||
},
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 32 01 00 00 00",
|
||||
"timestamp": 36047.980699
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 33 01 00 00 00",
|
||||
"timestamp": 36048.006522
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36048.013163
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 36048.013163
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:33:32.986103Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:33:33.067889Z",
|
||||
"duration_ms": 836
|
||||
},
|
||||
"finished_at": "2026-08-01T17:33:33.067920Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
{
|
||||
"suite": "t2g-seat-frontend-full-hil",
|
||||
"started_at": "2026-08-01T17:26:35.994926Z",
|
||||
"base_url": "http://127.0.0.1:8000",
|
||||
"vehicle_id": "denza_n7",
|
||||
"conversation_id": "conv-hil-full-20260801172635",
|
||||
"runtime": {
|
||||
"hardware_execution_allowed": true,
|
||||
"firmware_profiles": [
|
||||
"combined"
|
||||
]
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"id": "MOTION-ALL-ZERO",
|
||||
"group": "motion",
|
||||
"utterance": "把副驾水平、坐盆、靠背、腿托四轴归零",
|
||||
"started_at": "2026-08-01T17:26:35.994963Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801172635-cf368d",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 23 01 00 00 00",
|
||||
"timestamp": 35698.280073
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 24 01 00 00 00",
|
||||
"timestamp": 35698.306668
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 21 01 00 00 00",
|
||||
"timestamp": 35698.332116
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 22 01 00 00 00",
|
||||
"timestamp": 35698.357889
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35698.36569
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35698.36569
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:27:43.345246Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:27:43.418173Z",
|
||||
"duration_ms": 67423
|
||||
}
|
||||
],
|
||||
"final_state": {
|
||||
"id": "FINAL-THERMAL-OFF",
|
||||
"group": "cleanup",
|
||||
"utterance": "关闭副驾座椅加热和通风",
|
||||
"started_at": "2026-08-01T17:27:43.418231Z",
|
||||
"status": "passed",
|
||||
"errors": [],
|
||||
"http_status": 201,
|
||||
"request_id": "req-20260801172743-1f95ae",
|
||||
"matched_modes": [],
|
||||
"state_http_status": 200,
|
||||
"physical_state": {
|
||||
"position": {
|
||||
"backrest_recline": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 2,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "02 26 18 23 01 00 00 00",
|
||||
"timestamp": 35699.098847
|
||||
}
|
||||
},
|
||||
"legrest_height": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 3,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "03 26 18 24 01 00 00 00",
|
||||
"timestamp": 35699.124304
|
||||
}
|
||||
},
|
||||
"longitudinal": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 0,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "00 26 18 21 01 00 00 00",
|
||||
"timestamp": 35699.150901
|
||||
}
|
||||
},
|
||||
"cushion_tilt": {
|
||||
"position_pct": 0,
|
||||
"position_valid": true,
|
||||
"motion_state": "stopped",
|
||||
"fault": false,
|
||||
"task_state": 6,
|
||||
"motor_result": 2,
|
||||
"motor_selector": 1,
|
||||
"value_x100": 0,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x302",
|
||||
"payload_size": 8,
|
||||
"data_hex": "01 26 18 22 01 00 00 00",
|
||||
"timestamp": 35699.176715
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilation": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35699.184476
|
||||
}
|
||||
},
|
||||
"heating": {
|
||||
"state": "off",
|
||||
"level": 0,
|
||||
"feedback_value": 1,
|
||||
"feedback_frame": {
|
||||
"frame_id": "0x4FA",
|
||||
"payload_size": 8,
|
||||
"data_hex": "09 00 00 00 00 00 00 00",
|
||||
"timestamp": 35699.184476
|
||||
}
|
||||
},
|
||||
"captured_at": "2026-08-01T17:27:44.164137Z",
|
||||
"source": "physical_can",
|
||||
"target": {
|
||||
"device": "seat",
|
||||
"zone": "front_right",
|
||||
"instance": "passenger"
|
||||
},
|
||||
"complete": true,
|
||||
"missing": []
|
||||
},
|
||||
"finished_at": "2026-08-01T17:27:44.276096Z",
|
||||
"duration_ms": 858
|
||||
},
|
||||
"finished_at": "2026-08-01T17:27:44.276129Z",
|
||||
"summary": {
|
||||
"total": 1,
|
||||
"passed": 1,
|
||||
"failed": 0,
|
||||
"safety_blocked": 0,
|
||||
"executed": 1,
|
||||
"final_state_passed": true
|
||||
}
|
||||
}
|
||||