init
This commit is contained in:
Executable
+27
@@ -0,0 +1,27 @@
|
||||
# Domain Plugins
|
||||
|
||||
每个子目录是一个完整的领域交付单元。领域插件只能依赖 `cognitive_agent.sdk` 和 `contracts/`,不得依赖其他领域或中央编排实现。
|
||||
|
||||
新领域从 `_template/` 复制,目录名和 `spec.domain` 必须使用相同的 ASCII snake_case 标识。插件注册由 `manifest.yaml` 驱动,不在中央代码中硬编码领域列表。
|
||||
|
||||
领域目录固定结构:
|
||||
|
||||
```text
|
||||
<domain>/
|
||||
├── README.md
|
||||
├── OWNERS.yaml
|
||||
├── manifest.yaml
|
||||
├── config/
|
||||
│ ├── capabilities.yaml
|
||||
│ └── presets/
|
||||
├── schemas/
|
||||
│ └── function-parameters.schema.json
|
||||
├── can/
|
||||
│ └── signal-mapping.yaml
|
||||
├── src/<domain>_domain/
|
||||
└── tests/
|
||||
├── unit/
|
||||
├── contract/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
domain: example_domain
|
||||
primary_owner: TBD
|
||||
reviewers:
|
||||
- integration_owner
|
||||
paths:
|
||||
- domains/example_domain/**
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
# Domain Template
|
||||
|
||||
复制本目录后完成以下替换:
|
||||
|
||||
1. 把 `example_domain` 替换为稳定领域标识。
|
||||
2. 把 `src/domain_plugin` 重命名为 `<domain>_domain`,同步修改 manifest entrypoint。
|
||||
3. 填写真实负责人、能力、参数 Schema、preset 和信号映射。
|
||||
4. `signal-mapping.yaml` 在通过 HIL 前保持 `status: draft`。
|
||||
5. 补齐 Generator、Validator、Compiler、Telemetry Decoder 和测试。
|
||||
|
||||
领域插件不得保存 DBC 副本或直接发送 CAN。Compiler 返回符合公共 Schema 的 `CommandPlan`,公共 Runtime 负责安全门禁和发送。
|
||||
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: example-domain-reference-mapping
|
||||
name: Example domain reference vehicle mapping
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: example_domain
|
||||
platform: reference_vehicle
|
||||
dbc_ref: ../../../platforms/reference_vehicle/can/dbc/vehicle.dbc
|
||||
status: draft
|
||||
mappings:
|
||||
- capability: example_domain.set_level
|
||||
parameter: level_pct
|
||||
compiler_handler: compile_set_level
|
||||
calibration_ref: ../../../platforms/reference_vehicle/calibration/example_domain.yaml
|
||||
unit: percent
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: example-domain-capabilities
|
||||
name: Example domain capabilities
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: example_domain
|
||||
capabilities:
|
||||
- name: example_domain.set_level
|
||||
description: Replace with a user-facing semantic capability description.
|
||||
target_devices:
|
||||
- example_device
|
||||
target_scopes:
|
||||
- vehicle
|
||||
parameters:
|
||||
- name: level_pct
|
||||
type: number
|
||||
unit: percent
|
||||
required: true
|
||||
description: Replace with the physical meaning and range source.
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: example-domain-default
|
||||
name: Example preset
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: example_domain
|
||||
operations:
|
||||
- id: set_example_level
|
||||
capability: example_domain.set_level
|
||||
target:
|
||||
device: example_device
|
||||
parameters:
|
||||
level_pct: 50
|
||||
transition:
|
||||
duration_ms: 1000
|
||||
profile: device_default
|
||||
timeout_ms: 5000
|
||||
failure_policy: abort
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: DomainManifest
|
||||
metadata:
|
||||
id: example-domain-plugin
|
||||
name: Example domain plugin
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: example_domain
|
||||
entrypoint: domain_plugin.plugin:create_plugin
|
||||
function_schema: schemas/function-parameters.schema.json
|
||||
capabilities: config/capabilities.yaml
|
||||
signal_mapping: can/signal-mapping.yaml
|
||||
supported_platforms:
|
||||
- reference_vehicle
|
||||
telemetry_required: true
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cognitive-agent.local/domains/example_domain/function-parameters.schema.json",
|
||||
"title": "ExampleDomainFunctionParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"const": "example_domain"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {"const": "example_domain.set_level"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["level_pct"],
|
||||
"properties": {
|
||||
"level_pct": {"type": "number", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Plugin Source
|
||||
|
||||
建议实现以下模块,具体接口以 `cognitive_agent.sdk` 为准:
|
||||
|
||||
- `plugin.py`:创建并注册插件能力。
|
||||
- `generator.py`:`IntentIR -> FunctionConfig`。
|
||||
- `validator.py`:领域组合约束和上下文校验。
|
||||
- `compiler.py`:`FunctionConfig + PlatformContext -> CommandPlan`。
|
||||
- `telemetry.py`:CAN 回读到领域状态。
|
||||
|
||||
模块只返回数据或领域错误,不直接访问 LLM、数据库、HTTP 或 CAN socket。
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
# Domain Tests
|
||||
|
||||
- `unit/`:Generator、Validator、Compiler、Telemetry Decoder 的纯单元测试。
|
||||
- `contract/`:manifest、能力目录、preset、Schema、映射和 SDK 插件契约。
|
||||
- `integration/`:通过公共模拟 CAN Gateway 验证发送计划、回读、超时和失败行为。
|
||||
|
||||
硬件用例必须显式标记 `pytest.mark.hardware`,默认 CI 不运行。
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
domain: ambient_light
|
||||
primary_owner: ambient_light_owner
|
||||
reviewers:
|
||||
- integration_owner
|
||||
- can_owner
|
||||
paths:
|
||||
- domains/ambient_light/**
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
# Ambient Light Domain
|
||||
|
||||
氛围灯领域已根据根目录 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的“氛围灯”工作表完成 dry-run 闭环。
|
||||
|
||||
当前能力包括总开关、精确 `#RRGGBB` 颜色、0–100% 亮度,以及露营、小憩、观影、冥想和浪漫等矩阵原生模式。颜色和亮度使用 `0x32B/0x2B` 中的 IVI 1680 万色 RGB 信号,无需虚构颜色索引;总开关使用 `0x064/0x02`,场景模式使用 `0x496/0x04`。
|
||||
|
||||
动态效果不会由 Agent 逐帧生成。源表缺少完整时序、计数器、校验和和车型适用性,当前严格禁止实车发送。
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
version: 1.0.0
|
||||
source:
|
||||
file: 空调-天幕-氛围灯CAN信号矩阵表.xlsx
|
||||
sheet: 氛围灯
|
||||
scope: normalized_ambient_light_subset
|
||||
hardware_execution_allowed: false
|
||||
limitations:
|
||||
- Source workbook is partial and omits bus timing, counter, checksum, and variant applicability.
|
||||
- Exact RGB control uses the matrix-defined IVI 16.8-million-color request.
|
||||
messages:
|
||||
- name: Ambient_Power_Control_064_02
|
||||
frame_id: 100
|
||||
minimum_payload_bytes: 8
|
||||
fixed_signals: {Sub_ID: 2}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Atomic_atmosphere_light_switch_S, start_bit: 56, bit_length: 2}
|
||||
- name: Ambient_Mode_Control_496_04
|
||||
frame_id: 1174
|
||||
minimum_payload_bytes: 4
|
||||
fixed_signals: {Sub_ID: 4}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: atmosphere_Light_Request_Command_496_S, start_bit: 27, bit_length: 5}
|
||||
- name: Ambient_RGB_Control_32B_2B
|
||||
frame_id: 811
|
||||
minimum_payload_bytes: 5
|
||||
fixed_signals: {Sub_ID: 43}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: IVI_1680Atmosphere_Color_R_Setting_S, start_bit: 8, bit_length: 8}
|
||||
- {name: IVI_1680Atmosphere_Color_G_Setting_S, start_bit: 16, bit_length: 8}
|
||||
- {name: IVI_1680Atmosphere_Color_B_Setting_S, start_bit: 24, bit_length: 8}
|
||||
- {name: IVI_1680Atmosphere_Brightness_Setting_S, start_bit: 32, bit_length: 7}
|
||||
- name: Ambient_Power_Status_3F3
|
||||
frame_id: 1011
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Atmosphere_Light_Switch_Fdback_S, start_bit: 62, bit_length: 2}
|
||||
- name: Ambient_RGB_Status_1F1
|
||||
frame_id: 497
|
||||
minimum_payload_bytes: 48
|
||||
signals:
|
||||
- {name: LFDoor_Flowing_Ambient_Light_Brightness_S, start_bit: 40, bit_length: 7}
|
||||
- {name: Front_Atmspr_Light_Color_R_S, start_bit: 336, bit_length: 8}
|
||||
- {name: Front_Atmspr_Light_Color_G_S, start_bit: 344, bit_length: 8}
|
||||
- {name: Front_Atmspr_Light_Color_B_S, start_bit: 352, bit_length: 8}
|
||||
- {name: Rear_Atmspr_Light_Color_R_S, start_bit: 360, bit_length: 8}
|
||||
- {name: Rear_Atmspr_Light_Color_G_S, start_bit: 368, bit_length: 8}
|
||||
- {name: Rear_Atmspr_Light_Color_B_S, start_bit: 376, bit_length: 8}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: ambient-light-reference-vehicle-mapping
|
||||
name: Ambient-light mapping from supplied CAN matrix
|
||||
version: 1.0.0
|
||||
description: Virtual-only mapping using matrix-defined power, mode, RGB and brightness signals.
|
||||
spec:
|
||||
domain: ambient_light
|
||||
platform: reference_vehicle
|
||||
dbc_ref: ../../../platforms/reference_vehicle/can/dbc/vehicle.dbc
|
||||
matrix_ref: can-matrix.yaml
|
||||
status: draft
|
||||
mappings:
|
||||
- capability: ambient_light.set_power
|
||||
parameter: state
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_enum
|
||||
value_map: {"on": 1, "off": 2}
|
||||
bus: cabin_can
|
||||
message: Ambient_Power_Control_064_02
|
||||
signal: Atomic_atmosphere_light_switch_S
|
||||
feedback_message: Ambient_Power_Status_3F3
|
||||
feedback_signal: Atmosphere_Light_Switch_Fdback_S
|
||||
feedback_handler: compile_enum
|
||||
feedback_value_map: {"on": 2, "off": 1}
|
||||
- capability: ambient_light.set_illumination
|
||||
parameter: color_hex
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_hex_rgb
|
||||
bus: cabin_can
|
||||
message: Ambient_RGB_Control_32B_2B
|
||||
signals:
|
||||
- IVI_1680Atmosphere_Color_R_Setting_S
|
||||
- IVI_1680Atmosphere_Color_G_Setting_S
|
||||
- IVI_1680Atmosphere_Color_B_Setting_S
|
||||
- capability: ambient_light.set_illumination
|
||||
parameter: brightness_pct
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_integer
|
||||
bus: cabin_can
|
||||
message: Ambient_RGB_Control_32B_2B
|
||||
signal: IVI_1680Atmosphere_Brightness_Setting_S
|
||||
feedback_message: Ambient_RGB_Status_1F1
|
||||
feedback_signal: LFDoor_Flowing_Ambient_Light_Brightness_S
|
||||
feedback_handler: compile_integer
|
||||
unit: percent
|
||||
- capability: ambient_light.set_mode
|
||||
parameter: mode
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_enum
|
||||
value_map: {camping: 1, rest: 2, cinema: 3, meditation: 4, romantic: 5, "off": 6, "on": 7}
|
||||
bus: cabin_can
|
||||
message: Ambient_Mode_Control_496_04
|
||||
signal: atmosphere_Light_Request_Command_496_S
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: ambient-light-capabilities
|
||||
name: Ambient light capabilities
|
||||
version: 1.0.0
|
||||
spec:
|
||||
domain: ambient_light
|
||||
capabilities:
|
||||
- name: ambient_light.set_power
|
||||
description: Turn the vehicle ambient-light system on or off.
|
||||
target_devices: [ambient_light]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: ["on", "off"]
|
||||
description: Absolute ambient-light power state.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: ambient_light.set_illumination
|
||||
description: Set exact 24-bit RGB color and brightness for the cabin ambient light.
|
||||
target_devices: [ambient_light]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: color_hex
|
||||
type: string
|
||||
required: true
|
||||
description: RGB color encoded as #RRGGBB; infer a suitable color for semantic requests.
|
||||
- name: brightness_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
required: true
|
||||
description: Absolute brightness percentage.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: ambient_light.set_mode
|
||||
description: Select a matrix-defined ambient-light scene mode.
|
||||
target_devices: [ambient_light]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: mode
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: [camping, rest, cinema, meditation, romantic, "off", "on"]
|
||||
description: Absolute scene mode.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: ambient-light-relax
|
||||
name: Ambient light relaxation preset
|
||||
version: 1.0.0
|
||||
description: Virtual visual preset using the matrix-defined 24-bit RGB request.
|
||||
spec:
|
||||
domain: ambient_light
|
||||
operations:
|
||||
- id: set_relax_lighting
|
||||
capability: ambient_light.set_illumination
|
||||
target:
|
||||
device: ambient_light
|
||||
zone: vehicle
|
||||
parameters:
|
||||
color_hex: "#2E6F9E"
|
||||
brightness_pct: 30
|
||||
transition:
|
||||
duration_ms: 1200
|
||||
profile: ease_in_out
|
||||
timeout_ms: 5000
|
||||
failure_policy: continue
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: DomainManifest
|
||||
metadata:
|
||||
id: ambient-light-domain-plugin
|
||||
name: Ambient light domain plugin
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: ambient_light
|
||||
entrypoint: ambient_light_domain.plugin:create_plugin
|
||||
function_schema: schemas/function-parameters.schema.json
|
||||
capabilities: config/capabilities.yaml
|
||||
signal_mapping: can/signal-mapping.yaml
|
||||
supported_platforms:
|
||||
- reference_vehicle
|
||||
telemetry_required: true
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cognitive-agent.local/domains/ambient_light/function-parameters.schema.json",
|
||||
"title": "AmbientLightFunctionParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"const": "ambient_light"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "ambient_light.set_power"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["state"],
|
||||
"properties": {"state": {"enum": ["on", "off"]}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "ambient_light.set_illumination"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["color_hex", "brightness_pct"],
|
||||
"properties": {
|
||||
"color_hex": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"},
|
||||
"brightness_pct": {"type": "integer", "minimum": 0, "maximum": 100}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "ambient_light.set_mode"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode"],
|
||||
"properties": {
|
||||
"mode": {"enum": ["camping", "rest", "cinema", "meditation", "romantic", "off", "on"]}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# Ambient Light Plugin Source
|
||||
|
||||
实现 `plugin.py`、`generator.py`、`validator.py`、`compiler.py` 和 `telemetry.py`。颜色、亮度和效果必须转换为 ECU 支持的稳定命令;不在插件内实现无限制逐帧 CAN 动画。
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Ambient Light Tests
|
||||
|
||||
覆盖颜色格式、亮度边界、灯区支持、效果降级、亮度曲线、回读和超时。动态效果测试需验证命令速率上限。
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
domain: climate
|
||||
primary_owner: climate_owner
|
||||
reviewers:
|
||||
- integration_owner
|
||||
- can_owner
|
||||
paths:
|
||||
- domains/climate/**
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
# Climate Domain
|
||||
|
||||
空调领域已根据根目录 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的“空调”工作表完成 dry-run 闭环。
|
||||
|
||||
当前能力包括分区电源、18–32℃ 温度、整车 0–7 档风量、A/C、内外循环、分区吹面/吹脚和前除霜。`can/can-matrix.yaml` 保留来源信号名、帧 ID、Sub ID、Start Bit 和 Bit Length;`can/signal-mapping.yaml` 负责把稳定语义参数确定性转换为矩阵原始值。
|
||||
|
||||
命令主要使用 `0x064` 的 `0x03`–`0x06` 子帧,虚拟反馈使用 `0x0DB` 和 `0x404`。源表未提供完整周期、计数器、校验和、总线通道和车型适用性,`hardware_execution_allowed` 固定为 `false`。
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
version: 1.0.0
|
||||
source:
|
||||
file: 空调-天幕-氛围灯CAN信号矩阵表.xlsx
|
||||
sheet: 空调
|
||||
scope: normalized_climate_subset
|
||||
hardware_execution_allowed: false
|
||||
limitations:
|
||||
- Source workbook is a partial matrix without bus timing, counters, checksums, or vehicle variant applicability.
|
||||
- Sub IDs are encoded as the first payload byte for virtual execution.
|
||||
messages:
|
||||
- name: Climate_Control_064_03
|
||||
frame_id: 100
|
||||
minimum_payload_bytes: 8
|
||||
fixed_signals: {Sub_ID: 3}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Atomic_AC_S, start_bit: 36, bit_length: 2}
|
||||
- {name: Atomic_cycle_mode_S, start_bit: 38, bit_length: 2}
|
||||
- {name: Atomic_defrosting_S, start_bit: 40, bit_length: 2}
|
||||
- {name: Atomic_vehicle_air_conditioning_switch_S, start_bit: 50, bit_length: 2}
|
||||
- {name: Atomic_front_air_conditioning_switch_S, start_bit: 52, bit_length: 2}
|
||||
- {name: Atomic_rear_air_conditioning_switch_S, start_bit: 54, bit_length: 2}
|
||||
- {name: Atomic_driver_air_conditioning_switch_S, start_bit: 56, bit_length: 2}
|
||||
- {name: Atomic_passenger_air_conditioning_switch_S, start_bit: 58, bit_length: 2}
|
||||
- {name: Atomic_left_rear_air_conditioning_switch_S, start_bit: 60, bit_length: 2}
|
||||
- {name: Atomic_right_rear_air_conditioning_switch_S, start_bit: 62, bit_length: 2}
|
||||
- name: Climate_Control_064_04
|
||||
frame_id: 100
|
||||
minimum_payload_bytes: 8
|
||||
fixed_signals: {Sub_ID: 4}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Atomic_vehicle_air_volume_S, start_bit: 8, bit_length: 4}
|
||||
- {name: Atomic_vehicle_air_conditioning_temperature_S, start_bit: 40, bit_length: 8}
|
||||
- {name: Atomic_front_air_conditioning_temperature_S, start_bit: 48, bit_length: 8}
|
||||
- {name: Atomic_rear_air_conditioning_temperature_S, start_bit: 56, bit_length: 8}
|
||||
- name: Climate_Control_064_05
|
||||
frame_id: 100
|
||||
minimum_payload_bytes: 8
|
||||
fixed_signals: {Sub_ID: 5}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Atomic_driver_air_conditioning_temperature_S, start_bit: 8, bit_length: 8}
|
||||
- {name: Atomic_passenger_air_conditioning_temperature_S, start_bit: 16, bit_length: 8}
|
||||
- {name: Atomic_left_rear_air_conditioning_temperature_S, start_bit: 24, bit_length: 8}
|
||||
- {name: Atomic_right_rear_air_conditioning_temperature_S, start_bit: 32, bit_length: 8}
|
||||
- {name: Atomic_driver_air_conditioning_outlet_mode_S, start_bit: 56, bit_length: 4}
|
||||
- {name: Atomic_left_rear_air_conditioning_outlet_mode_S, start_bit: 60, bit_length: 4}
|
||||
- name: Climate_Control_064_06
|
||||
frame_id: 100
|
||||
minimum_payload_bytes: 4
|
||||
fixed_signals: {Sub_ID: 6}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Atomic_co_pilot_air_conditioning_outlet_mode_S, start_bit: 8, bit_length: 4}
|
||||
- {name: Atomic_right_rear_air_conditioning_outlet_mode_S, start_bit: 16, bit_length: 4}
|
||||
- name: Climate_Status_0DB
|
||||
frame_id: 219
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: ACHMC_AC_Open_Rslt_Fdbck_Infor_S, start_bit: 0, bit_length: 2}
|
||||
- {name: AV_Close_Results_Feedback_S, start_bit: 2, bit_length: 2}
|
||||
- {name: AC_Cycle_Mode_0DB_S, start_bit: 4, bit_length: 2}
|
||||
- {name: Medium_AC_Switch_S, start_bit: 6, bit_length: 2}
|
||||
- {name: ACHMC_Mast_Dri_Temp_Set_Rslt_S, start_bit: 8, bit_length: 5}
|
||||
- {name: Co_Pilot_AC_Temp_Set_S, start_bit: 16, bit_length: 5}
|
||||
- {name: AC_Wind_Model_0DB_S, start_bit: 28, bit_length: 4}
|
||||
- {name: IKEY_AC_Air_Gear_S, start_bit: 32, bit_length: 4}
|
||||
- {name: ACHMC_AC_Front_Defrosting_Fdbck_S, start_bit: 41, bit_length: 2}
|
||||
- name: Climate_Status_404
|
||||
frame_id: 1028
|
||||
minimum_payload_bytes: 4
|
||||
signals:
|
||||
- {name: ACHMC_AC_On_Status_S, start_bit: 8, bit_length: 2}
|
||||
- {name: AC_Cycle_Mode_404_S, start_bit: 12, bit_length: 2}
|
||||
- {name: AC_Wind_Model_404_S, start_bit: 16, bit_length: 4}
|
||||
- {name: Ac_wind_rnk_S, start_bit: 20, bit_length: 4}
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: climate-reference-vehicle-mapping
|
||||
name: Climate mapping from supplied CAN matrix
|
||||
version: 1.0.0
|
||||
description: Virtual-only mapping normalized from the supplied climate worksheet.
|
||||
spec:
|
||||
domain: climate
|
||||
platform: reference_vehicle
|
||||
dbc_ref: ../../../platforms/reference_vehicle/can/dbc/vehicle.dbc
|
||||
matrix_ref: can-matrix.yaml
|
||||
status: draft
|
||||
mappings:
|
||||
- {capability: climate.set_power, parameter: state, target_zone: vehicle, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_vehicle_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}, feedback_message: Climate_Status_404, feedback_signal: ACHMC_AC_On_Status_S, feedback_handler: compile_enum, feedback_value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: front_row, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_front_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: rear_row, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_rear_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: driver, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_driver_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: passenger, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_passenger_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: rear_left, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_left_rear_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_power, parameter: state, target_zone: rear_right, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_right_rear_air_conditioning_switch_S, value_map: {"on": 1, "off": 2}}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: vehicle, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_04, signal: Atomic_vehicle_air_conditioning_temperature_S, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: front_row, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_04, signal: Atomic_front_air_conditioning_temperature_S, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: rear_row, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_04, signal: Atomic_rear_air_conditioning_temperature_S, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: driver, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_driver_air_conditioning_temperature_S, feedback_message: Climate_Status_0DB, feedback_signal: ACHMC_Mast_Dri_Temp_Set_Rslt_S, feedback_handler: compile_temperature_feedback, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: passenger, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_passenger_air_conditioning_temperature_S, feedback_message: Climate_Status_0DB, feedback_signal: Co_Pilot_AC_Temp_Set_S, feedback_handler: compile_temperature_feedback, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: rear_left, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_left_rear_air_conditioning_temperature_S, unit: celsius}
|
||||
- {capability: climate.set_temperature, parameter: temperature_c, target_zone: rear_right, compiler_handler: compile_integer, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_right_rear_air_conditioning_temperature_S, unit: celsius}
|
||||
- {capability: climate.set_fan_level, parameter: level, target_zone: vehicle, compiler_handler: compile_level_plus_one, bus: cabin_can, message: Climate_Control_064_04, signal: Atomic_vehicle_air_volume_S, feedback_message: Climate_Status_404, feedback_signal: Ac_wind_rnk_S, feedback_handler: compile_integer}
|
||||
- {capability: climate.set_ac, parameter: state, target_zone: vehicle, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_AC_S, value_map: {"on": 1, "off": 2}, feedback_message: Climate_Status_0DB, feedback_signal: Medium_AC_Switch_S, feedback_handler: compile_enum, feedback_value_map: {"on": 2, "off": 1}}
|
||||
- {capability: climate.set_cycle_mode, parameter: mode, target_zone: vehicle, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_cycle_mode_S, value_map: {internal: 1, external: 2}, feedback_message: Climate_Status_0DB, feedback_signal: AC_Cycle_Mode_0DB_S, feedback_handler: compile_enum, feedback_value_map: {internal: 2, external: 1}}
|
||||
- {capability: climate.set_front_defrost, parameter: state, target_zone: vehicle, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_03, signal: Atomic_defrosting_S, value_map: {"on": 1, "off": 2}, feedback_message: Climate_Status_0DB, feedback_signal: ACHMC_AC_Front_Defrosting_Fdbck_S, feedback_handler: compile_enum, feedback_value_map: {"on": 2, "off": 1}}
|
||||
- {capability: climate.set_outlet_mode, parameter: mode, target_zone: driver, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_driver_air_conditioning_outlet_mode_S, value_map: {face: 1, feet: 2}, feedback_message: Climate_Status_0DB, feedback_signal: AC_Wind_Model_0DB_S, feedback_handler: compile_enum, feedback_value_map: {face: 1, feet: 3}}
|
||||
- {capability: climate.set_outlet_mode, parameter: mode, target_zone: rear_left, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_05, signal: Atomic_left_rear_air_conditioning_outlet_mode_S, value_map: {face: 1, feet: 2}}
|
||||
- {capability: climate.set_outlet_mode, parameter: mode, target_zone: passenger, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_06, signal: Atomic_co_pilot_air_conditioning_outlet_mode_S, value_map: {face: 1, feet: 2}, feedback_message: Climate_Status_0DB, feedback_signal: AC_Wind_Model_0DB_S, feedback_handler: compile_enum, feedback_value_map: {face: 1, feet: 3}}
|
||||
- {capability: climate.set_outlet_mode, parameter: mode, target_zone: rear_right, compiler_handler: compile_enum, bus: cabin_can, message: Climate_Control_064_06, signal: Atomic_right_rear_air_conditioning_outlet_mode_S, value_map: {face: 1, feet: 2}}
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: climate-capabilities
|
||||
name: Climate capabilities
|
||||
version: 1.0.0
|
||||
spec:
|
||||
domain: climate
|
||||
capabilities:
|
||||
- name: climate.set_power
|
||||
description: Turn HVAC power on or off for the requested cabin zone.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [driver, passenger, front_row, rear_row, rear_left, rear_right, vehicle]
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: ["on", "off"]
|
||||
description: Absolute HVAC power state.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_temperature
|
||||
description: Set an absolute HVAC temperature from 18 to 32 degrees Celsius.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [driver, passenger, front_row, rear_row, rear_left, rear_right, vehicle]
|
||||
parameters:
|
||||
- name: temperature_c
|
||||
type: number
|
||||
unit: celsius
|
||||
minimum: 18
|
||||
maximum: 32
|
||||
required: true
|
||||
description: Absolute requested temperature in degrees Celsius.
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_fan_level
|
||||
description: Set whole-cabin airflow to an absolute integer level from 0 to 7.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: level
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 7
|
||||
required: true
|
||||
description: Absolute fan level, where 0 is off and 7 is maximum.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_ac
|
||||
description: Turn the air-conditioning compressor request on or off.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: ["on", "off"]
|
||||
description: Absolute compressor request state.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_cycle_mode
|
||||
description: Select internal or external cabin air circulation.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: mode
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: [internal, external]
|
||||
description: Absolute circulation mode.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_outlet_mode
|
||||
description: Direct a zone outlet toward the face or feet.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [driver, passenger, rear_left, rear_right]
|
||||
parameters:
|
||||
- name: mode
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: [face, feet]
|
||||
description: Absolute outlet direction.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: climate.set_front_defrost
|
||||
description: Turn front windshield defrost on or off.
|
||||
target_devices: [hvac]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: ["on", "off"]
|
||||
description: Absolute front defrost state.
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: climate-comfort
|
||||
name: Climate comfort preset
|
||||
version: 1.0.0
|
||||
description: Virtual comfort values backed by the supplied partial matrix.
|
||||
spec:
|
||||
domain: climate
|
||||
operations:
|
||||
- id: set_cabin_temperature
|
||||
capability: climate.set_temperature
|
||||
target:
|
||||
device: hvac
|
||||
zone: vehicle
|
||||
parameters:
|
||||
temperature_c: 23
|
||||
transition:
|
||||
profile: device_default
|
||||
timeout_ms: 5000
|
||||
failure_policy: continue
|
||||
- id: set_cabin_fan
|
||||
capability: climate.set_fan_level
|
||||
target:
|
||||
device: hvac
|
||||
zone: vehicle
|
||||
parameters:
|
||||
level: 3
|
||||
transition:
|
||||
duration_ms: 1500
|
||||
profile: ease_in_out
|
||||
timeout_ms: 5000
|
||||
failure_policy: continue
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: DomainManifest
|
||||
metadata:
|
||||
id: climate-domain-plugin
|
||||
name: Climate domain plugin
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: climate
|
||||
entrypoint: climate_domain.plugin:create_plugin
|
||||
function_schema: schemas/function-parameters.schema.json
|
||||
capabilities: config/capabilities.yaml
|
||||
signal_mapping: can/signal-mapping.yaml
|
||||
supported_platforms:
|
||||
- reference_vehicle
|
||||
telemetry_required: true
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cognitive-agent.local/domains/climate/function-parameters.schema.json",
|
||||
"title": "ClimateFunctionParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"const": "climate"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"enum": ["climate.set_power", "climate.set_ac", "climate.set_front_defrost"]},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["state"],
|
||||
"properties": {"state": {"enum": ["on", "off"]}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "climate.set_temperature"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["temperature_c"],
|
||||
"properties": {"temperature_c": {"type": "number", "minimum": 18, "maximum": 32}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "climate.set_fan_level"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["level"],
|
||||
"properties": {"level": {"type": "integer", "minimum": 0, "maximum": 7}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "climate.set_cycle_mode"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode"],
|
||||
"properties": {"mode": {"enum": ["internal", "external"]}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "climate.set_outlet_mode"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["mode"],
|
||||
"properties": {"mode": {"enum": ["face", "feet"]}}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Climate Plugin Source
|
||||
|
||||
实现 `plugin.py`、`generator.py`、`validator.py`、`compiler.py` 和 `telemetry.py`。分区、摄氏温度、百分比风量必须在 Compiler 中转换为平台定义的 message/signal,不直接发送 CAN。
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Climate Tests
|
||||
|
||||
覆盖温度/风量边界、不同 zone、自动模式冲突、平台不支持、发送超时和状态回读。`integration/` 使用模拟 CAN Gateway;HIL/实车用例单独标记。
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
domain: roof
|
||||
primary_owner: roof_owner
|
||||
reviewers:
|
||||
- integration_owner
|
||||
- can_owner
|
||||
paths:
|
||||
- domains/roof/**
|
||||
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
# Roof Domain
|
||||
|
||||
天幕领域已根据根目录 `空调-天幕-氛围灯CAN信号矩阵表.xlsx` 的“天幕”工作表完成 dry-run 闭环。
|
||||
|
||||
当前能力包括调光天幕 0–100% 透光率、后天幕遮阳帘 0–100% 绝对位置和自动调光开关。透光率使用 `0x4C1/0x13`,遮阳帘使用 `0x3E6/0x04`;虚拟反馈分别使用 `0x4BB` 和 `0x47F`,并保留初始化、霍尔和热保护信号作为后续安全接入点。
|
||||
|
||||
本文约定遮阳帘 `position_pct` 为 0% 完全关闭、100% 完全打开。源表缺少车型适用性和完整安全互锁,当前严格禁止实车发送。
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
version: 1.0.0
|
||||
source:
|
||||
file: 空调-天幕-氛围灯CAN信号矩阵表.xlsx
|
||||
sheet: 天幕
|
||||
scope: normalized_roof_subset
|
||||
hardware_execution_allowed: false
|
||||
limitations:
|
||||
- Source workbook is partial and vehicle-variant applicability is not supplied.
|
||||
- Anti-pinch and motor fault signals are feedback-only safety evidence in this dry-run.
|
||||
messages:
|
||||
- name: Roof_Glass_Control_4C1_13
|
||||
frame_id: 1217
|
||||
minimum_payload_bytes: 4
|
||||
fixed_signals: {Sub_ID: 19}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Adjust_The_Skylight_Transmittance_Command_S, start_bit: 16, bit_length: 8}
|
||||
- {name: Canopy_Automatic_Dimming_0x4C1_S, start_bit: 24, bit_length: 2}
|
||||
- name: Roof_Shade_Control_3E6_04
|
||||
frame_id: 998
|
||||
minimum_payload_bytes: 5
|
||||
fixed_signals: {Sub_ID: 4}
|
||||
signals:
|
||||
- {name: Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Rear_Awning_Shade_Screen_Position_Percentage_S, start_bit: 32, bit_length: 8}
|
||||
- name: Roof_Glass_Status_4BB
|
||||
frame_id: 1211
|
||||
minimum_payload_bytes: 57
|
||||
signals:
|
||||
- {name: Current_Transmittance_Of_Skylight_S, start_bit: 444, bit_length: 4}
|
||||
- {name: Current_Status_Of_Dimming_Sunroof_S, start_bit: 448, bit_length: 3}
|
||||
- {name: Canopy_Automatic_Dimming_0x4BB_S, start_bit: 452, bit_length: 2}
|
||||
- name: Roof_Shade_Status_47F
|
||||
frame_id: 1151
|
||||
minimum_payload_bytes: 7
|
||||
signals:
|
||||
- {name: Percentage_Of_Rear_Canopy_Position_47F_S, start_bit: 0, bit_length: 8}
|
||||
- {name: The_Back_Canopy_Closed_State_47F_S, start_bit: 8, bit_length: 2}
|
||||
- {name: Back_Canopy_Initialization_State_47F_S, start_bit: 10, bit_length: 2}
|
||||
- {name: The_Current_Action_Status_Of_The_Back_Sky_47F_S, start_bit: 16, bit_length: 3}
|
||||
- {name: Hall_Signal_Of_Rear_Canopy_Motor_A47F_S, start_bit: 21, bit_length: 1}
|
||||
- {name: Hall_Signal_Of_Rear_Canopy_Motor_B47F_S, start_bit: 22, bit_length: 1}
|
||||
- {name: Thermal_Protection_Status_Of_Rear_Canopy_Motor_47F_S, start_bit: 48, bit_length: 1}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: roof-reference-vehicle-mapping
|
||||
name: Roof mapping from supplied CAN matrix
|
||||
version: 1.0.0
|
||||
description: Virtual-only mapping normalized from the supplied roof worksheet.
|
||||
spec:
|
||||
domain: roof
|
||||
platform: reference_vehicle
|
||||
dbc_ref: ../../../platforms/reference_vehicle/can/dbc/vehicle.dbc
|
||||
matrix_ref: can-matrix.yaml
|
||||
status: draft
|
||||
mappings:
|
||||
- capability: roof.set_shade_position
|
||||
parameter: position_pct
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_integer
|
||||
bus: cabin_can
|
||||
message: Roof_Shade_Control_3E6_04
|
||||
signal: Rear_Awning_Shade_Screen_Position_Percentage_S
|
||||
feedback_message: Roof_Shade_Status_47F
|
||||
feedback_signal: Percentage_Of_Rear_Canopy_Position_47F_S
|
||||
feedback_handler: compile_integer
|
||||
unit: percent
|
||||
- capability: roof.set_transmittance
|
||||
parameter: transmittance_pct
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_integer
|
||||
bus: cabin_can
|
||||
message: Roof_Glass_Control_4C1_13
|
||||
signal: Adjust_The_Skylight_Transmittance_Command_S
|
||||
feedback_message: Roof_Glass_Status_4BB
|
||||
feedback_signal: Current_Transmittance_Of_Skylight_S
|
||||
feedback_handler: compile_transmittance_bucket
|
||||
unit: percent
|
||||
- capability: roof.set_automatic_dimming
|
||||
parameter: state
|
||||
target_zone: vehicle
|
||||
compiler_handler: compile_enum
|
||||
value_map: {"on": 1, "off": 2}
|
||||
bus: cabin_can
|
||||
message: Roof_Glass_Control_4C1_13
|
||||
signal: Canopy_Automatic_Dimming_0x4C1_S
|
||||
feedback_message: Roof_Glass_Status_4BB
|
||||
feedback_signal: Canopy_Automatic_Dimming_0x4BB_S
|
||||
feedback_handler: compile_enum
|
||||
feedback_value_map: {"on": 1, "off": 2}
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: roof-capabilities
|
||||
name: Roof capabilities
|
||||
version: 1.0.0
|
||||
spec:
|
||||
domain: roof
|
||||
capabilities:
|
||||
- name: roof.set_shade_position
|
||||
description: Set the rear panoramic roof shade to an absolute position, 0 closed and 100 open.
|
||||
target_devices: [roof_shade]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
required: true
|
||||
description: Absolute shade opening percentage.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: roof.set_transmittance
|
||||
description: Set dimming canopy light transmittance, 0 darkest and 100 clearest.
|
||||
target_devices: [roof_glass]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: transmittance_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
required: true
|
||||
description: Absolute smart-glass transmittance percentage.
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: roof.set_automatic_dimming
|
||||
description: Turn automatic canopy dimming on or off.
|
||||
target_devices: [roof_glass]
|
||||
target_scopes: [vehicle]
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
allowed_values: ["on", "off"]
|
||||
description: Absolute automatic dimming state.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
Executable
+24
@@ -0,0 +1,24 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: roof-open-view
|
||||
name: Roof open-view preset
|
||||
version: 1.0.0
|
||||
description: Example preset pending platform safety approval.
|
||||
spec:
|
||||
domain: roof
|
||||
operations:
|
||||
- id: open_roof_shade
|
||||
capability: roof.set_shade_position
|
||||
target:
|
||||
device: roof_shade
|
||||
zone: vehicle
|
||||
parameters:
|
||||
position_pct: 100
|
||||
transition:
|
||||
profile: device_default
|
||||
timeout_ms: 15000
|
||||
failure_policy: abort
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
vehicle_speed_state: stationary
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: DomainManifest
|
||||
metadata:
|
||||
id: roof-domain-plugin
|
||||
name: Roof domain plugin
|
||||
version: 0.1.0
|
||||
spec:
|
||||
domain: roof
|
||||
entrypoint: roof_domain.plugin:create_plugin
|
||||
function_schema: schemas/function-parameters.schema.json
|
||||
capabilities: config/capabilities.yaml
|
||||
signal_mapping: can/signal-mapping.yaml
|
||||
supported_platforms:
|
||||
- reference_vehicle
|
||||
telemetry_required: true
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cognitive-agent.local/domains/roof/function-parameters.schema.json",
|
||||
"title": "RoofFunctionParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"const": "roof"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "roof.set_shade_position"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["position_pct"],
|
||||
"properties": {"position_pct": {"type": "integer", "minimum": 0, "maximum": 100}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "roof.set_transmittance"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["transmittance_pct"],
|
||||
"properties": {"transmittance_pct": {"type": "integer", "minimum": 0, "maximum": 100}}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"capability": {"const": "roof.set_automatic_dimming"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["state"],
|
||||
"properties": {"state": {"enum": ["on", "off"]}}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Roof Plugin Source
|
||||
|
||||
实现 `plugin.py`、`generator.py`、`validator.py`、`compiler.py` 和 `telemetry.py`。插件必须区分遮阳帘与调光玻璃,执行前检查平台能力和防夹等安全条件,并只返回 `CommandPlan`。
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Roof Tests
|
||||
|
||||
覆盖位置方向、百分比边界、平台能力缺失、防夹/车速/温度互锁、校准缺失、执行超时和回读不一致。
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
domain: seat
|
||||
primary_owner: seat_owner
|
||||
reviewers:
|
||||
- integration_owner
|
||||
- can_owner
|
||||
paths:
|
||||
- domains/seat/**
|
||||
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
# Seat Domain
|
||||
|
||||
负责座椅单领域能力。通风和加热基于根目录 `座椅部分功能CAN矩阵表.xlsx` 建立可审计的归一化矩阵;位置电机使用单独的虚构矩阵,仅验证模型理解、配置生成、CAN 编译和反馈闭环。
|
||||
|
||||
## 首个交付目标
|
||||
|
||||
- 支持显式指令以及“主驾有点热,有点闷”等舒适反馈。
|
||||
- GPT 在同一 `conversation_id` 下获取当前虚拟执行状态和前序成功动作,自行解析跟进、因果和作用域。
|
||||
- GPT 返回领域无关的通用能力请求;座椅适配器只校验并转换已注册能力,不用本地舒适或位置词条覆盖模型结果。
|
||||
- 主副驾分别支持水平、高度、坐垫俯仰、靠背、腿托升降和腿托伸缩六个虚构电机轴,会话状态保留每轴的 `0-100%` 绝对位置。
|
||||
- 混合请求中可执行的座椅动作独立 dry-run,未接入的其他域保留为 `deferred_requests`,不会阻断座椅子请求。
|
||||
- 生成只含 `domain: seat` 的 `FunctionConfig`。
|
||||
- 1–3 档目标进入 `FunctionConfig` 和会话状态;`0x42A` 仍只编译开关,档位由 `0x4FA` dry-run 模拟。
|
||||
- 使用模拟 CAN Gateway 测试正常执行、冲突、缺失能力和回读不一致。
|
||||
|
||||
## 待负责人补齐
|
||||
|
||||
- 0x42A 的正式报文名称、发送节点、周期、DLC、字节序、counter/checksum 和总线所有权。
|
||||
- 档位控制信号;当前“设置三档”只是认知目标和虚拟回读,不得视为已产生实车档位命令。
|
||||
- 行车状态、乘员状态和防夹等安全互锁。
|
||||
- DBC message/signal 以及命令保持、释放和真实回读策略。
|
||||
|
||||
`can/seat-thermal-matrix.yaml` 和 `can/seat-motion-virtual-matrix.yaml` 中 `hardware_execution_allowed: false` 是强制边界。后者的帧号、信号布局、时序和反馈行为全部为虚构,当前闭环只能 dry-run。
|
||||
|
||||
真实 T2G 台架使用独立的 `signal-mapping.hardware.yaml`、`seat-thermal-hardware-matrix.yaml` 和 `seat-motion-hardware-matrix.yaml`,不会解除上述虚拟矩阵的边界。仓库内 `firmware/t2g/Seat_Controller_demo` 是唯一硬件固件接口,真实路径只接受它的 `combined` profile。当前主驾与副驾都是 `t2g_primary_seat` 的逻辑别名:两种指令使用同一组实体电机、加热和通风,成功反馈会同步到两侧会话状态。完整接线、自动烧录、授权和反馈要求见 [T2G 座椅真实硬件台架接入](../../docs/workflows/t2g-seat-hardware-bench.md)。
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
version: 3.0.0
|
||||
source:
|
||||
project: Seat_Move_demo
|
||||
workbook: Seat_Move_demo/Docs/outputs/seat_move_four_motor_20260724/座椅四电机移动功能CAN矩阵表.xlsx
|
||||
test_report: Seat_Move_demo/Docs/TEST_REPORT_2026-07-24.md
|
||||
verified_on: 2026-07-24
|
||||
scope: t2g_driver_seat_four_motor_protocol
|
||||
hardware_execution_allowed: true
|
||||
bit_numbering: lsb0
|
||||
byte_order: intel_little_endian
|
||||
limitations:
|
||||
- Motor 0 through 2 were hardware verified; Motor 3 is disabled until it is wired and calibrated.
|
||||
- Cushion tilt has no actuator in Seat_Move_demo and is never eligible for hardware transmission.
|
||||
- Mechanical direction labels still require a final visual sign-off on the assembled seat.
|
||||
messages:
|
||||
- name: T2G_Seat_Move_Command_300
|
||||
frame_id: 0x300
|
||||
role: command
|
||||
minimum_payload_bytes: 8
|
||||
hardware_send_allowed: true
|
||||
signals:
|
||||
- {name: Opcode, start_bit: 0, bit_length: 8}
|
||||
- {name: Motor_Selector, start_bit: 8, bit_length: 2}
|
||||
- {name: Direction, start_bit: 10, bit_length: 2}
|
||||
- {name: Protocol_Version, start_bit: 12, bit_length: 2}
|
||||
- {name: Sequence, start_bit: 16, bit_length: 16}
|
||||
- {name: Param_A, start_bit: 32, bit_length: 16}
|
||||
- {name: Param_B, start_bit: 48, bit_length: 16}
|
||||
- name: T2G_Seat_Move_System_Status_301
|
||||
frame_id: 0x301
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Ack, start_bit: 0, bit_length: 3}
|
||||
- {name: Reject_Reason, start_bit: 3, bit_length: 3}
|
||||
- {name: Shutdown_Inhibit, start_bit: 6, bit_length: 1}
|
||||
- {name: Safety_Inhibit, start_bit: 7, bit_length: 1}
|
||||
- {name: Last_Opcode, start_bit: 8, bit_length: 4}
|
||||
- {name: Active_Mask, start_bit: 12, bit_length: 4}
|
||||
- {name: Last_Sequence, start_bit: 16, bit_length: 16}
|
||||
- {name: Retry_Mask, start_bit: 32, bit_length: 4}
|
||||
- {name: Record_Clean, start_bit: 36, bit_length: 1}
|
||||
- {name: Watchdog_Reset, start_bit: 37, bit_length: 1}
|
||||
- {name: Record_Valid, start_bit: 38, bit_length: 1}
|
||||
- {name: Safety_Fault, start_bit: 40, bit_length: 8}
|
||||
- {name: Config_Sequence, start_bit: 48, bit_length: 16}
|
||||
- name: T2G_Seat_Move_Motor_Status_302
|
||||
frame_id: 0x302
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Motor_Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Task_State, start_bit: 8, bit_length: 4}
|
||||
- {name: Result, start_bit: 12, bit_length: 4}
|
||||
- {name: Direction, start_bit: 16, bit_length: 2}
|
||||
- {name: Active, start_bit: 18, bit_length: 1}
|
||||
- {name: Retried, start_bit: 19, bit_length: 1}
|
||||
- {name: Position_Valid, start_bit: 20, bit_length: 1}
|
||||
- {name: Motor_Sequence, start_bit: 24, bit_length: 16}
|
||||
- {name: Value_X100, start_bit: 40, bit_length: 16}
|
||||
- {name: Duty_Percent, start_bit: 56, bit_length: 8}
|
||||
- name: T2G_Seat_Move_Diagnostic_303
|
||||
frame_id: 0x303
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Motor_Sub_ID, start_bit: 0, bit_length: 8}
|
||||
- {name: Last_Motor_Event, start_bit: 8, bit_length: 4}
|
||||
- {name: Hall_Count, start_bit: 16, bit_length: 16}
|
||||
- {name: Max_Hall_Count, start_bit: 32, bit_length: 16}
|
||||
- {name: Max_Speed_Pps_X100, start_bit: 48, bit_length: 16}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
version: 0.1.0
|
||||
source:
|
||||
kind: synthetic_demo
|
||||
file: seat-motion-virtual-matrix.yaml
|
||||
designed_on: 2026-07-23
|
||||
scope: driver_and_front_passenger_position_motors
|
||||
hardware_execution_allowed: false
|
||||
synthetic: true
|
||||
bit_numbering: lsb0
|
||||
byte_order: intel_little_endian
|
||||
limitations:
|
||||
- All frame IDs, signal layouts, timing and feedback behavior in this file are fictional.
|
||||
- Values are normalized positions, not physical millimeters or degrees.
|
||||
- No vehicle-state, occupant, anti-pinch, current-sensing or collision interlock is implemented.
|
||||
- These frames must never be sent to a vehicle, bench ECU or HIL network.
|
||||
messages:
|
||||
- name: Driver_Seat_Motion_Command_550
|
||||
frame_id: 0x550
|
||||
role: synthetic_command
|
||||
minimum_payload_bytes: 8
|
||||
hardware_send_allowed: false
|
||||
signals: &motion_command_signals
|
||||
- {name: Longitudinal_Target_Pct, description: "Fore-aft absolute target", start_bit: 0, bit_length: 7}
|
||||
- {name: Cushion_Tilt_Target_Pct, description: "Cushion pitch absolute target", start_bit: 7, bit_length: 7}
|
||||
- {name: Backrest_Recline_Target_Pct, description: "Backrest recline absolute target", start_bit: 14, bit_length: 7}
|
||||
- {name: Legrest_Height_Target_Pct, description: "Leg-rest lift absolute target", start_bit: 21, bit_length: 7}
|
||||
- {name: Legrest_Extension_Target_Pct, description: "Leg-rest extension absolute target", start_bit: 28, bit_length: 7}
|
||||
- {name: Motion_Enable_Mask, description: "One enable bit per position axis", start_bit: 35, bit_length: 5}
|
||||
- {name: Command_Counter, description: "Synthetic per-plan command sequence", start_bit: 40, bit_length: 4}
|
||||
- {name: Command_Mode, description: "0=no request, 1=absolute position", start_bit: 44, bit_length: 2}
|
||||
- {name: Command_Checksum, description: "Synthetic additive checksum", start_bit: 48, bit_length: 8}
|
||||
- name: Passenger_Seat_Motion_Command_551
|
||||
frame_id: 0x551
|
||||
role: synthetic_command
|
||||
minimum_payload_bytes: 8
|
||||
hardware_send_allowed: false
|
||||
signals: *motion_command_signals
|
||||
- name: Driver_Seat_Motion_Status_558
|
||||
frame_id: 0x558
|
||||
role: synthetic_feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals: &motion_status_signals
|
||||
- {name: Longitudinal_Actual_Pct, description: "Fore-aft measured position", start_bit: 0, bit_length: 7}
|
||||
- {name: Cushion_Tilt_Actual_Pct, description: "Cushion pitch measured position", start_bit: 7, bit_length: 7}
|
||||
- {name: Backrest_Recline_Actual_Pct, description: "Backrest recline measured position", start_bit: 14, bit_length: 7}
|
||||
- {name: Legrest_Height_Actual_Pct, description: "Leg-rest lift measured position", start_bit: 21, bit_length: 7}
|
||||
- {name: Legrest_Extension_Actual_Pct, description: "Leg-rest extension measured position", start_bit: 28, bit_length: 7}
|
||||
- {name: Moving_Mask, description: "One moving-state bit per position axis", start_bit: 35, bit_length: 5}
|
||||
- {name: Fault_Mask, description: "One synthetic motor-fault bit per position axis", start_bit: 40, bit_length: 5}
|
||||
- {name: Position_Valid_Mask, description: "One valid-feedback bit per position axis", start_bit: 45, bit_length: 5}
|
||||
- name: Passenger_Seat_Motion_Status_559
|
||||
frame_id: 0x559
|
||||
role: synthetic_feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals: *motion_status_signals
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
version: 1.0.0
|
||||
source:
|
||||
projects:
|
||||
- Seat_Backrest_Heater_demo
|
||||
- Seat_Fan_demo
|
||||
workbooks:
|
||||
- 座椅加热功能CAN矩阵表.xlsx
|
||||
- 座椅通风功能CAN矩阵表.xlsx
|
||||
verified_on: 2026-07-23
|
||||
scope: t2g_driver_seat_heating_and_ventilation
|
||||
hardware_execution_allowed: true
|
||||
bit_numbering: lsb0
|
||||
byte_order: intel_little_endian
|
||||
limitations:
|
||||
- Only the driver-seat backrest heater and ventilation fan are implemented by the verified demos.
|
||||
- The two demos are standalone firmware images; use one board per profile or a reviewed combined image.
|
||||
- Product deployment still requires command timeout, E2E protection and calibrated thermal/current limits.
|
||||
messages:
|
||||
- name: T2G_Seat_Thermal_Control_29E
|
||||
frame_id: 0x29E
|
||||
role: command
|
||||
minimum_payload_bytes: 8
|
||||
hardware_send_allowed: true
|
||||
signals:
|
||||
- {name: Driver_Seat_Ventilation_On, start_bit: 0, bit_length: 2}
|
||||
- {name: Driver_Seat_Ventilation_Mode, start_bit: 2, bit_length: 2}
|
||||
- {name: Driver_Seat_Heating_On, start_bit: 4, bit_length: 2}
|
||||
- {name: Driver_Seat_Ventilation_Gear, start_bit: 16, bit_length: 4}
|
||||
- {name: Driver_Seat_Heating_Gear, start_bit: 20, bit_length: 4}
|
||||
- name: T2G_Seat_Thermal_Mode_3CE
|
||||
frame_id: 0x3CE
|
||||
role: command
|
||||
minimum_payload_bytes: 8
|
||||
hardware_send_allowed: true
|
||||
signals:
|
||||
- {name: Driver_Seat_Thermal_Mode, start_bit: 4, bit_length: 2}
|
||||
- name: T2G_Seat_Thermal_Status_4FA
|
||||
frame_id: 0x4FA
|
||||
role: feedback
|
||||
minimum_payload_bytes: 8
|
||||
signals:
|
||||
- {name: Driver_Seat_Ventilation_Status, start_bit: 0, bit_length: 3}
|
||||
- {name: Driver_Seat_Heating_Status, start_bit: 3, bit_length: 3}
|
||||
Executable
+189
@@ -0,0 +1,189 @@
|
||||
version: 1.0.0
|
||||
source:
|
||||
file: ../../../座椅部分功能CAN矩阵表.xlsx
|
||||
sheet: Sheet1
|
||||
normalized_on: 2026-07-22
|
||||
scope: driver_and_front_passenger_heating_ventilation
|
||||
hardware_execution_allowed: false
|
||||
bit_numbering: lsb0_inferred_from_start_bit_and_bit_columns
|
||||
byte_order: intel_little_endian_inferred_not_confirmed
|
||||
limitations:
|
||||
- Message names are local aliases because the spreadsheet only provides frame IDs.
|
||||
- Transmitter, receiver, cycle time, DLC, byte order, counter and checksum are absent.
|
||||
- Payload lengths below are minimum lengths inferred from the highest occupied bit.
|
||||
- Frame 0x42A is treated as a control candidate only for virtual execution.
|
||||
- Gear signals are observable states; no gear command signal is identified.
|
||||
messages:
|
||||
- name: Seat_Thermal_Status_4FA
|
||||
frame_id: 0x4FA
|
||||
role: feedback
|
||||
minimum_payload_bytes: 2
|
||||
signals:
|
||||
- name: Pilot_Seat_Vtilat_Stat_S
|
||||
description: 主驾座椅通风工作状态
|
||||
start_bit: 0
|
||||
bit_length: 3
|
||||
values:
|
||||
0: invalid
|
||||
1: "off"
|
||||
2: low
|
||||
3: high
|
||||
4: level_3
|
||||
- name: Pilot_Seat_Heat_Stat_S
|
||||
description: 主驾座椅加热工作状态
|
||||
start_bit: 3
|
||||
bit_length: 3
|
||||
values:
|
||||
0: invalid
|
||||
1: "off"
|
||||
2: low
|
||||
3: high
|
||||
4: level_3
|
||||
- name: Copilot_Seat_Vtilat_Stat_4FA_S
|
||||
description: 副驾座椅通风工作状态
|
||||
start_bit: 8
|
||||
bit_length: 3
|
||||
values:
|
||||
0: invalid
|
||||
1: "off"
|
||||
2: low
|
||||
3: high
|
||||
4: level_3
|
||||
- name: Copilot_Seat_Heat_Stat_4FA_S
|
||||
description: 副驾座椅加热工作状态
|
||||
start_bit: 11
|
||||
bit_length: 3
|
||||
values:
|
||||
0: invalid
|
||||
1: "off"
|
||||
2: low
|
||||
3: high
|
||||
4: level_3
|
||||
|
||||
- name: Driver_Seat_Thermal_Status_29E
|
||||
frame_id: 0x29E
|
||||
role: feedback
|
||||
minimum_payload_bytes: 3
|
||||
signals:
|
||||
- name: Driver_Seat_Ventilation_On_S
|
||||
description: 主驾座椅通风开启状态
|
||||
start_bit: 0
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Driver_Seat_Ventilation_Control_Mode_S
|
||||
description: 主驾座椅通风控制模式
|
||||
start_bit: 2
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: manual, 2: automatic, 3: reserved}
|
||||
- name: The_Driver_Seat_Heating_Is_On_S
|
||||
description: 主驾座椅加热开启状态
|
||||
start_bit: 4
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Driver_Seat_Ventilation_Gear_S
|
||||
description: 主驾座椅通风档位
|
||||
start_bit: 16
|
||||
bit_length: 4
|
||||
values: {0: invalid, 1: level_1, 2: level_2, 3: level_3, 4: level_4, 5: level_5}
|
||||
- name: Heated_Driver_Seat_S
|
||||
description: 主驾座椅加热档位
|
||||
start_bit: 20
|
||||
bit_length: 4
|
||||
values: {0: invalid, 1: level_1, 2: level_2, 3: level_3, 4: level_4, 5: level_5}
|
||||
|
||||
- name: Driver_Seat_Thermal_Mode_3CE
|
||||
frame_id: 0x3CE
|
||||
role: feedback
|
||||
minimum_payload_bytes: 1
|
||||
signals:
|
||||
- name: Main_Driver_Seat_Ven_And_Hea_Control_Mode_S
|
||||
description: 主驾座椅通风加热控制模式
|
||||
start_bit: 4
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: manual, 2: automatic, 3: reserved}
|
||||
|
||||
- name: Passenger_Seat_Thermal_Status_3D3
|
||||
frame_id: 0x3D3
|
||||
role: feedback
|
||||
minimum_payload_bytes: 3
|
||||
signals:
|
||||
- name: Front_Pass_Seat_Ventilation_On_S
|
||||
description: 副驾座椅通风开启状态
|
||||
start_bit: 0
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Pas_Seat_Heating_On_S
|
||||
description: 副驾座椅加热开启状态
|
||||
start_bit: 2
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Front_Pass_Seat_Ven_And_Hea_Control_Mode_S
|
||||
description: 副驾座椅通风加热控制模式
|
||||
start_bit: 4
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: manual, 2: automatic, 3: reserved}
|
||||
- name: Passenger_Seat_Ventilation_Gear_3D3_S
|
||||
description: 副驾座椅通风档位
|
||||
start_bit: 12
|
||||
bit_length: 4
|
||||
values: {0: invalid, 1: level_1, 2: level_2, 3: level_3, 4: level_4, 5: level_5}
|
||||
- name: Heated_Passenger_Seat_3D3_S
|
||||
description: 副驾座椅加热档位
|
||||
start_bit: 16
|
||||
bit_length: 4
|
||||
values: {0: invalid, 1: level_1, 2: level_2, 3: level_3, 4: level_4, 5: level_5}
|
||||
|
||||
- name: Seat_Thermal_Configuration_2AA_01
|
||||
frame_id: 0x2AA
|
||||
sub_id: 0x01
|
||||
role: configuration_status
|
||||
minimum_payload_bytes: 3
|
||||
signals:
|
||||
- name: Copilot_Seat_Three_Speed_Ventilat_Config_S
|
||||
description: 副驾座椅通风配置
|
||||
start_bit: 8
|
||||
bit_length: 3
|
||||
values: {0: invalid, 1: level_1_config, 2: level_2_config, 3: level_3_config, 4: level_5_split_config}
|
||||
- name: Copilot_Seat_Three_Speed_Heat_Config_S
|
||||
description: 副驾座椅加热配置
|
||||
start_bit: 11
|
||||
bit_length: 3
|
||||
values: {0: invalid, 1: level_1_config, 2: level_2_config, 3: level_3_config, 4: level_5_split_config}
|
||||
- name: Driver_Seat_Three_Speed_Ventilat_Config_S
|
||||
description: 主驾座椅通风配置
|
||||
start_bit: 16
|
||||
bit_length: 3
|
||||
values: {0: invalid, 1: level_1_config, 2: level_2_config, 3: level_3_config, 4: level_5_split_config}
|
||||
- name: Driver_Seat_Three_Speed_Heat_Config_S
|
||||
description: 主驾座椅加热配置
|
||||
start_bit: 19
|
||||
bit_length: 3
|
||||
values: {0: invalid, 1: level_1_config, 2: level_2_config, 3: level_3_config, 4: level_5_split_config}
|
||||
|
||||
- name: Seat_Thermal_Control_42A
|
||||
frame_id: 0x42A
|
||||
role: command_candidate
|
||||
minimum_payload_bytes: 2
|
||||
hardware_send_allowed: false
|
||||
signals:
|
||||
- name: Driver_Seat_Ventilation_S
|
||||
description: 主驾通风开启/关闭
|
||||
start_bit: 0
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Driver_Seat_Heating_S
|
||||
description: 主驾加热开启/关闭
|
||||
start_bit: 2
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Front_Passenger_Seat_Ventilation_S
|
||||
description: 副驾通风开启/关闭
|
||||
start_bit: 12
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
- name: Front_Passenger_Seat_Heating_S
|
||||
description: 副驾加热开启/关闭
|
||||
start_bit: 14
|
||||
bit_length: 2
|
||||
values: {0: invalid, 1: "on", 2: "off", 3: reserved}
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: seat-t2g-bench-hardware-mapping
|
||||
name: Verified T2G seat bench mapping
|
||||
version: 1.1.0
|
||||
description: Mapping implemented by the unified Seat_Controller_demo IAR firmware.
|
||||
spec:
|
||||
domain: seat
|
||||
platform: t2g_seat_bench
|
||||
dbc_ref: ../../../platforms/t2g_seat_bench/can/dbc/seat.dbc
|
||||
matrix_ref: seat-thermal-hardware-matrix.yaml
|
||||
status: hil_verified
|
||||
mappings:
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Ventilation_On, Driver_Seat_Ventilation_Mode, Driver_Seat_Ventilation_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Ventilation_Status
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Heating_On, Driver_Seat_Heating_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Heating_Status
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Ventilation_On, Driver_Seat_Ventilation_Mode, Driver_Seat_Ventilation_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Ventilation_Status
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_thermal
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Thermal_Control_29E
|
||||
signals: [Driver_Seat_Heating_On, Driver_Seat_Heating_Gear]
|
||||
feedback_message: T2G_Seat_Thermal_Status_4FA
|
||||
feedback_signal: Driver_Seat_Heating_Status
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
physical_target_alias: t2g_primary_seat
|
||||
compiler_handler: compile_t2g_absolute_position
|
||||
bus: seat_can
|
||||
message: T2G_Seat_Move_Command_300
|
||||
signal: Param_A
|
||||
feedback_message: T2G_Seat_Move_Motor_Status_302
|
||||
feedback_signal: Value_X100
|
||||
calibration_ref: ../../../platforms/t2g_seat_bench/calibration/seat.yaml
|
||||
unit: percent
|
||||
Executable
+132
@@ -0,0 +1,132 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: SignalMapping
|
||||
metadata:
|
||||
id: seat-reference-vehicle-mapping
|
||||
name: Seat mapping for reference vehicle
|
||||
version: 1.1.0
|
||||
description: Supplied thermal matrix plus explicitly synthetic motion mappings; virtual execution only.
|
||||
spec:
|
||||
domain: seat
|
||||
platform: reference_vehicle
|
||||
dbc_ref: ../../../platforms/reference_vehicle/can/dbc/vehicle.dbc
|
||||
matrix_ref: seat-thermal-matrix.yaml
|
||||
status: draft
|
||||
mappings:
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Driver_Seat_Ventilation_S
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Driver_Seat_Heating_S
|
||||
- capability: seat.set_ventilation
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Front_Passenger_Seat_Ventilation_S
|
||||
- capability: seat.set_heating
|
||||
parameter: state
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_binary_control
|
||||
bus: body_can
|
||||
message: Seat_Thermal_Control_42A
|
||||
signal: Front_Passenger_Seat_Heating_S
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Longitudinal_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_cushion_tilt
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Cushion_Tilt_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Backrest_Recline_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Legrest_Height_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_left
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Driver_Seat_Motion_Command_550
|
||||
signal: Legrest_Extension_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_longitudinal_position
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Passenger_Seat_Motion_Command_551
|
||||
signal: Longitudinal_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_cushion_tilt
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Passenger_Seat_Motion_Command_551
|
||||
signal: Cushion_Tilt_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_backrest_recline
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Passenger_Seat_Motion_Command_551
|
||||
signal: Backrest_Recline_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_height
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Passenger_Seat_Motion_Command_551
|
||||
signal: Legrest_Height_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
- capability: seat.set_legrest_extension
|
||||
parameter: position_pct
|
||||
target_zone: front_right
|
||||
compiler_handler: compile_position_control
|
||||
bus: body_can
|
||||
message: Passenger_Seat_Motion_Command_551
|
||||
signal: Legrest_Extension_Target_Pct
|
||||
calibration_ref: seat-motion-virtual-matrix.yaml
|
||||
unit: percent
|
||||
Executable
+211
@@ -0,0 +1,211 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: CapabilityCatalog
|
||||
metadata:
|
||||
id: seat-capabilities
|
||||
name: Seat capabilities
|
||||
version: 1.2.0
|
||||
spec:
|
||||
domain: seat
|
||||
capabilities:
|
||||
- name: seat.set_ventilation
|
||||
description: >-
|
||||
Move air through one seat surface to reduce local occupant heat and moisture; this does not
|
||||
control general cabin airflow. Turn it on or off for the driver or front passenger seat.
|
||||
target_devices:
|
||||
- seat
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
description: Requested binary ventilation state.
|
||||
allowed_values:
|
||||
- "on"
|
||||
- "off"
|
||||
- name: level
|
||||
type: integer
|
||||
required: false
|
||||
description: Desired 1-3 comfort level; simulated until a level command signal is supplied.
|
||||
allowed_values:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_heating
|
||||
description: >-
|
||||
Warm one seat surface for local occupant thermal comfort. Turn it on or off for the driver
|
||||
or front passenger seat.
|
||||
target_devices:
|
||||
- seat
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: state
|
||||
type: string
|
||||
required: true
|
||||
description: Requested binary heating state.
|
||||
allowed_values:
|
||||
- "on"
|
||||
- "off"
|
||||
- name: level
|
||||
type: integer
|
||||
required: false
|
||||
description: Desired 1-3 comfort level; simulated until a level command signal is supplied.
|
||||
allowed_values:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
risk_level: medium
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_longitudinal_position
|
||||
description: >-
|
||||
Set one front seat's fore-aft rail position. position_pct is an absolute target: 0 is the
|
||||
rearmost mechanical limit and 100 is the forwardmost limit. Resolve relative requests such
|
||||
as "move back a little" against the current position supplied in runtime state.
|
||||
target_devices:
|
||||
- seat_longitudinal_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
required: true
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Absolute normalized fore-aft target.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_cushion_tilt
|
||||
description: >-
|
||||
Set the seat-cushion pitch for one front seat. position_pct is absolute: 0 places the front
|
||||
edge at its lowest limit and 100 raises the front edge to its highest tilt limit.
|
||||
target_devices:
|
||||
- seat_cushion_tilt_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
required: true
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Absolute normalized cushion front-edge height target.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_backrest_recline
|
||||
description: >-
|
||||
Set the backrest recline of one front seat. Use either position_pct or angle_deg.
|
||||
position_pct is absolute: 0 is the most upright limit and 100 is the most reclined limit.
|
||||
angle_deg is the included angle between the seat-cushion surface and backrest centreline,
|
||||
from 95 to 140 degrees. "Backrest backward" means a larger value.
|
||||
target_devices:
|
||||
- seat_backrest_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
required: false
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Absolute normalized backrest-recline target.
|
||||
- name: angle_deg
|
||||
type: number
|
||||
unit: degree
|
||||
required: false
|
||||
minimum: 95
|
||||
maximum: 140
|
||||
description: Exact included angle between the cushion surface and backrest centreline.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_legrest_height
|
||||
description: >-
|
||||
Raise or lower one front seat's powered leg rest. Use either position_pct or angle_deg.
|
||||
position_pct is absolute: 0 is fully lowered and 100 is fully raised. angle_deg is the
|
||||
included angle between the cushion surface and leg-rest surface, from 95 to 160 degrees.
|
||||
This axis does not change leg-rest extension.
|
||||
target_devices:
|
||||
- seat_legrest_lift_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
required: false
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Absolute normalized leg-rest lift target.
|
||||
- name: angle_deg
|
||||
type: number
|
||||
unit: degree
|
||||
required: false
|
||||
minimum: 95
|
||||
maximum: 160
|
||||
description: Exact included angle between the cushion surface and leg-rest surface.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.set_legrest_extension
|
||||
description: >-
|
||||
Extend or retract one front seat's powered leg rest. position_pct is absolute: 0 is fully
|
||||
retracted and 100 is fully extended. This axis does not change leg-rest height.
|
||||
target_devices:
|
||||
- seat_legrest_extension_motor
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: position_pct
|
||||
type: integer
|
||||
unit: percent
|
||||
required: true
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
description: Absolute normalized leg-rest extension target.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.save_posture_preset
|
||||
description: >-
|
||||
Save the current five-axis posture of one front seat as a conversation-owned named preset.
|
||||
This is a control-plane operation and does not emit a CAN command. Use only when the user
|
||||
explicitly asks to save, remember or overwrite the current posture.
|
||||
target_devices:
|
||||
- seat_preset_store
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: preset_name
|
||||
type: string
|
||||
required: true
|
||||
description: User-visible name of the posture preset to create or overwrite.
|
||||
risk_level: low
|
||||
confirmation_policy: on_ambiguity
|
||||
- name: seat.apply_posture_preset
|
||||
description: >-
|
||||
Apply a previously saved five-axis posture preset to one front seat. The runtime expands
|
||||
the stored FunctionPreset into the registered motion capabilities before CAN compilation.
|
||||
Use this capability only when the named preset exists in runtime conversation context.
|
||||
target_devices:
|
||||
- seat_preset_store
|
||||
target_scopes:
|
||||
- driver
|
||||
- passenger
|
||||
parameters:
|
||||
- name: preset_name
|
||||
type: string
|
||||
required: true
|
||||
description: Exact user-visible name of an existing posture preset.
|
||||
risk_level: high
|
||||
confirmation_policy: on_ambiguity
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
# Planned Seat Capabilities
|
||||
|
||||
零重力姿态和车型级自定义角度仍不属于实车可执行交付。现有 Excel 只覆盖主/副驾通风和加热相关信号,没有滑轨、靠背、坐垫或腿托控制信号,也没有运动学标定。
|
||||
|
||||
当前已使用 `seat-motion-virtual-matrix.yaml` 提供五轴 `0-100%` 虚构位置能力,用于模型理解和端到端 dry-run 验证。靠背/坐垫 `95–140°` 和坐垫/腿托 `95–160°` 采用显式 synthetic 线性标定,仅用于验证精确夹角语义、配置生成和孪生展示。该文件不是 DBC,不提供真实物理毫米/角度转换,也不具备防夹、电流、占位、车速或挡位互锁。
|
||||
|
||||
当前支持把某一会话中指定座位的五轴 dry-run 状态保存为命名 `FunctionPreset` 并再次调用;这是用户态控制面数据,不是车型出厂零重力标定。获得完整姿态 CAN 矩阵、坐标系、安全互锁和回读定义后,再交付硬件可执行的 `seat.set_posture` 及官方零重力 preset。
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: seat-driver-ventilation
|
||||
name: Driver seat ventilation
|
||||
version: 1.0.0
|
||||
description: Binary ventilation command supported by the partial CAN matrix.
|
||||
spec:
|
||||
domain: seat
|
||||
operations:
|
||||
- id: driver_ventilation_on
|
||||
capability: seat.set_ventilation
|
||||
target:
|
||||
device: seat
|
||||
zone: front_left
|
||||
instance: driver
|
||||
parameters:
|
||||
state: "on"
|
||||
timeout_ms: 3000
|
||||
failure_policy: abort
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: FunctionPreset
|
||||
metadata:
|
||||
id: seat-passenger-heating
|
||||
name: Front passenger seat heating
|
||||
version: 1.0.0
|
||||
description: Binary heating command supported by the partial CAN matrix.
|
||||
spec:
|
||||
domain: seat
|
||||
operations:
|
||||
- id: passenger_heating_on
|
||||
capability: seat.set_heating
|
||||
target:
|
||||
device: seat
|
||||
zone: front_right
|
||||
instance: passenger
|
||||
parameters:
|
||||
state: "on"
|
||||
timeout_ms: 3000
|
||||
failure_policy: abort
|
||||
locked_parameters: []
|
||||
applicability:
|
||||
platform: reference_vehicle
|
||||
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
apiVersion: cognitive-agent/v1alpha1
|
||||
kind: DomainManifest
|
||||
metadata:
|
||||
id: seat-domain-plugin
|
||||
name: Seat domain plugin
|
||||
version: 1.1.0
|
||||
spec:
|
||||
domain: seat
|
||||
entrypoint: seat_domain.plugin:create_plugin
|
||||
function_schema: schemas/function-parameters.schema.json
|
||||
capabilities: config/capabilities.yaml
|
||||
signal_mapping: can/signal-mapping.yaml
|
||||
supported_platforms:
|
||||
- reference_vehicle
|
||||
- t2g_seat_bench
|
||||
telemetry_required: true
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://cognitive-agent.local/domains/seat/function-parameters.schema.json",
|
||||
"title": "SeatThermalFunctionParameters",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"spec": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain": {"const": "seat"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {"const": "seat.set_ventilation"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["state"],
|
||||
"properties": {
|
||||
"state": {"type": "string", "enum": ["on", "off"]},
|
||||
"level": {"type": "integer", "minimum": 1, "maximum": 3}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {"const": "seat.set_heating"},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["state"],
|
||||
"properties": {
|
||||
"state": {"type": "string", "enum": ["on", "off"]},
|
||||
"level": {"type": "integer", "minimum": 1, "maximum": 3}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capability": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"seat.set_longitudinal_position",
|
||||
"seat.set_cushion_tilt",
|
||||
"seat.set_backrest_recline",
|
||||
"seat.set_legrest_height",
|
||||
"seat.set_legrest_extension"
|
||||
]
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["position_pct"],
|
||||
"properties": {
|
||||
"position_pct": {"type": "integer", "minimum": 0, "maximum": 100},
|
||||
"angle_deg": {"type": "number", "minimum": 95, "maximum": 160}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
# Seat Plugin Source
|
||||
|
||||
当前实现包含本地意图解析、配置生成、领域校验、确定性 Compiler、矩阵位编码和虚拟回读 Executor。Compiler 只返回 `CommandPlan`,虚拟 Executor 不打开 CAN socket。获得正式 DBC、发送属性、安全输入和审批前不得增加硬件发送路径。
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
"""Seat thermal domain plugin."""
|
||||
|
||||
from .compiler import SeatCompiler
|
||||
from .executor import VirtualSeatExecutor
|
||||
from .generator import build_function_config, build_function_preset, build_intent_ir
|
||||
from .hardware_compiler import T2GSeatCompiler
|
||||
from .hardware_executor import HardwareExecutionError, HardwareSeatExecutor
|
||||
from .intent import parse_local_intent, resolve_contextual_intent
|
||||
from .matrix import SeatCanMatrix
|
||||
from .validator import validate_semantics
|
||||
|
||||
__all__ = [
|
||||
"SeatCanMatrix",
|
||||
"SeatCompiler",
|
||||
"T2GSeatCompiler",
|
||||
"HardwareExecutionError",
|
||||
"HardwareSeatExecutor",
|
||||
"VirtualSeatExecutor",
|
||||
"build_function_config",
|
||||
"build_function_preset",
|
||||
"build_intent_ir",
|
||||
"parse_local_intent",
|
||||
"resolve_contextual_intent",
|
||||
"validate_semantics",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Executable
+133
@@ -0,0 +1,133 @@
|
||||
"""Deterministically compile seat FunctionConfig into a CommandPlan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
STATE_VALUES = {"on": 1, "off": 2}
|
||||
MOTION_AXIS_BITS = {
|
||||
"seat.set_longitudinal_position": 0,
|
||||
"seat.set_cushion_tilt": 1,
|
||||
"seat.set_backrest_recline": 2,
|
||||
"seat.set_legrest_height": 3,
|
||||
"seat.set_legrest_extension": 4,
|
||||
}
|
||||
MOTION_STATUS_MESSAGES = {
|
||||
"front_left": "Driver_Seat_Motion_Status_558",
|
||||
"front_right": "Passenger_Seat_Motion_Status_559",
|
||||
}
|
||||
|
||||
|
||||
class SeatCompiler:
|
||||
version = "1.1.0"
|
||||
|
||||
def __init__(self, mapping_path: Path):
|
||||
self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8"))
|
||||
self.mapping_version = self.mapping["metadata"]["version"]
|
||||
|
||||
def _mapping_for(self, capability: str, zone: str) -> dict[str, Any]:
|
||||
for mapping in self.mapping["spec"]["mappings"]:
|
||||
if mapping["capability"] == capability and mapping.get("target_zone") == zone:
|
||||
return mapping
|
||||
raise ValueError(f"No signal mapping for capability={capability}, zone={zone}")
|
||||
|
||||
def compile(self, function_config: dict[str, Any]) -> dict[str, Any]:
|
||||
thermal_groups: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
motion_commands: list[dict[str, Any]] = []
|
||||
motion_index = 0
|
||||
|
||||
for operation in function_config["spec"]["operations"]:
|
||||
mapping = self._mapping_for(operation["capability"], operation["target"]["zone"])
|
||||
handler = mapping["compiler_handler"]
|
||||
if handler == "compile_binary_control":
|
||||
key = (mapping["bus"], mapping["message"])
|
||||
group = thermal_groups.setdefault(key, {"signals": {}, "operation_ids": []})
|
||||
group["signals"][mapping["signal"]] = STATE_VALUES[
|
||||
operation["parameters"]["state"]
|
||||
]
|
||||
group["operation_ids"].append(operation["id"])
|
||||
continue
|
||||
if handler != "compile_position_control":
|
||||
raise ValueError(f"Unsupported compiler handler: {handler}")
|
||||
|
||||
target = int(operation["parameters"]["position_pct"])
|
||||
axis_bit = MOTION_AXIS_BITS[operation["capability"]]
|
||||
enable_mask = 1 << axis_bit
|
||||
counter = motion_index % 16
|
||||
checksum = (target + enable_mask + counter + 1) & 0xFF
|
||||
feedback_signal = mapping["signal"].replace("_Target_", "_Actual_")
|
||||
motion_commands.append(
|
||||
{
|
||||
"id": f"move_{operation['id']}",
|
||||
"operation_ref": operation["id"],
|
||||
"bus": mapping["bus"],
|
||||
"message": mapping["message"],
|
||||
"signals": dict(
|
||||
sorted(
|
||||
{
|
||||
mapping["signal"]: target,
|
||||
"Motion_Enable_Mask": enable_mask,
|
||||
"Command_Counter": counter,
|
||||
"Command_Mode": 1,
|
||||
"Command_Checksum": checksum,
|
||||
}.items()
|
||||
)
|
||||
),
|
||||
"send_offset_ms": motion_index * 50,
|
||||
"repeat": {"count": 3, "period_ms": 20},
|
||||
"completion": {
|
||||
"message": MOTION_STATUS_MESSAGES[operation["target"]["zone"]],
|
||||
"signal": feedback_signal,
|
||||
"operator": "within",
|
||||
"value": target,
|
||||
"tolerance": 1,
|
||||
"timeout_ms": int(operation.get("timeout_ms", 8000)),
|
||||
},
|
||||
}
|
||||
)
|
||||
motion_index += 1
|
||||
|
||||
canonical = json.dumps(function_config, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
config_hash = hashlib.sha256(canonical).hexdigest()
|
||||
metadata = function_config["metadata"]
|
||||
commands = [
|
||||
{
|
||||
"id": "seat_thermal_control" if index == 0 else f"seat_thermal_control_{index}",
|
||||
"operation_ref": ",".join(group["operation_ids"]),
|
||||
"bus": bus,
|
||||
"message": message,
|
||||
"signals": dict(sorted(group["signals"].items())),
|
||||
"send_offset_ms": 0,
|
||||
}
|
||||
for index, ((bus, message), group) in enumerate(thermal_groups.items())
|
||||
]
|
||||
commands.extend(motion_commands)
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "CommandPlan",
|
||||
"metadata": {
|
||||
"id": f"plan-{metadata['request_id']}",
|
||||
"name": "Seat CAN command plan",
|
||||
"version": "1.0.0",
|
||||
"request_id": metadata["request_id"],
|
||||
"labels": {"execution_scope": "virtual_only"},
|
||||
},
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"platform": "reference_vehicle",
|
||||
"source": {
|
||||
"config_id": metadata["id"],
|
||||
"config_version": metadata["version"],
|
||||
"config_sha256": config_hash,
|
||||
"compiler_version": self.version,
|
||||
"mapping_version": self.mapping_version,
|
||||
},
|
||||
"commands": commands,
|
||||
},
|
||||
}
|
||||
Executable
+199
@@ -0,0 +1,199 @@
|
||||
"""Virtual CAN executor and feedback simulator for the partial matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from .matrix import SeatCanMatrix
|
||||
|
||||
|
||||
COMMAND_TO_FEEDBACK = {
|
||||
"Driver_Seat_Ventilation_S": "Pilot_Seat_Vtilat_Stat_S",
|
||||
"Driver_Seat_Heating_S": "Pilot_Seat_Heat_Stat_S",
|
||||
"Front_Passenger_Seat_Ventilation_S": "Copilot_Seat_Vtilat_Stat_4FA_S",
|
||||
"Front_Passenger_Seat_Heating_S": "Copilot_Seat_Heat_Stat_4FA_S",
|
||||
}
|
||||
OPERATION_TO_COMMAND = {
|
||||
("front_left", "seat.set_ventilation"): "Driver_Seat_Ventilation_S",
|
||||
("front_left", "seat.set_heating"): "Driver_Seat_Heating_S",
|
||||
("front_right", "seat.set_ventilation"): "Front_Passenger_Seat_Ventilation_S",
|
||||
("front_right", "seat.set_heating"): "Front_Passenger_Seat_Heating_S",
|
||||
}
|
||||
MOTION_CAPABILITY_TO_AXIS = {
|
||||
"seat.set_longitudinal_position": "longitudinal",
|
||||
"seat.set_cushion_tilt": "cushion_tilt",
|
||||
"seat.set_backrest_recline": "backrest_recline",
|
||||
"seat.set_legrest_height": "legrest_height",
|
||||
"seat.set_legrest_extension": "legrest_extension",
|
||||
}
|
||||
MOTION_AXIS_BITS = {
|
||||
"longitudinal": 0,
|
||||
"cushion_tilt": 1,
|
||||
"backrest_recline": 2,
|
||||
"legrest_height": 3,
|
||||
"legrest_extension": 4,
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class VirtualSeatExecutor:
|
||||
def __init__(self, matrix: SeatCanMatrix):
|
||||
self.matrix = matrix
|
||||
|
||||
def execute(
|
||||
self,
|
||||
command_plan: dict[str, Any],
|
||||
function_config: dict[str, Any],
|
||||
actuator_state: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
started_at = _now()
|
||||
operation_by_id = {
|
||||
operation["id"]: operation
|
||||
for operation in function_config["spec"]["operations"]
|
||||
}
|
||||
results = []
|
||||
for command in command_plan["spec"]["commands"]:
|
||||
operation_ids = command["operation_ref"].split(",")
|
||||
operations = [operation_by_id[item] for item in operation_ids]
|
||||
if command["message"] == "Seat_Thermal_Control_42A":
|
||||
results.extend(self._execute_thermal(command, operations))
|
||||
else:
|
||||
if len(operations) != 1:
|
||||
raise ValueError("A motion command must reference exactly one operation")
|
||||
results.append(
|
||||
self._execute_motion(command, operations[0], actuator_state or {})
|
||||
)
|
||||
|
||||
request_id = function_config["metadata"]["request_id"]
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "ExecutionReport",
|
||||
"metadata": {
|
||||
"id": f"report-{request_id}",
|
||||
"name": "Seat virtual execution report",
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"labels": {"simulation": "true"},
|
||||
},
|
||||
"spec": {
|
||||
"mode": "dry_run",
|
||||
"status": "succeeded",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
},
|
||||
}
|
||||
|
||||
def _execute_thermal(
|
||||
self, command: dict[str, Any], operations: list[dict[str, Any]]
|
||||
) -> list[dict[str, Any]]:
|
||||
command_frame = self.matrix.encode(command["message"], command["signals"])
|
||||
operation_by_signal = {
|
||||
OPERATION_TO_COMMAND[(operation["target"]["zone"], operation["capability"])]: operation
|
||||
for operation in operations
|
||||
}
|
||||
feedback_values = {}
|
||||
for signal, value in command["signals"].items():
|
||||
operation = operation_by_signal[signal]
|
||||
level = int(operation["parameters"].get("level", 1))
|
||||
feedback_values[COMMAND_TO_FEEDBACK[signal]] = level + 1 if value == 1 else 1
|
||||
feedback_frame = self.matrix.encode("Seat_Thermal_Status_4FA", feedback_values)
|
||||
|
||||
results = []
|
||||
for operation in operations:
|
||||
command_signal = OPERATION_TO_COMMAND[
|
||||
(operation["target"]["zone"], operation["capability"])
|
||||
]
|
||||
feedback_signal = COMMAND_TO_FEEDBACK[command_signal]
|
||||
state = operation["parameters"]["state"]
|
||||
requested_level = operation["parameters"].get("level")
|
||||
effective_level = (requested_level or 1) if state == "on" else 0
|
||||
results.append(
|
||||
{
|
||||
"ref": operation["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
f"虚拟回读确认 {operation['target']['instance']} "
|
||||
f"{operation['capability']}={state}, level={effective_level}"
|
||||
),
|
||||
"telemetry": {
|
||||
"requested_state": state,
|
||||
"requested_level": requested_level,
|
||||
"effective_level": effective_level,
|
||||
"level_command_simulated": requested_level is not None,
|
||||
"command_signal": command_signal,
|
||||
"command_value": command["signals"][command_signal],
|
||||
"feedback_signal": feedback_signal,
|
||||
"feedback_value": feedback_values[feedback_signal],
|
||||
"command_frame": command_frame,
|
||||
"feedback_frame": feedback_frame,
|
||||
"simulated": True,
|
||||
},
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
def _execute_motion(
|
||||
self,
|
||||
command: dict[str, Any],
|
||||
operation: dict[str, Any],
|
||||
actuator_state: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
capability = operation["capability"]
|
||||
axis = MOTION_CAPABILITY_TO_AXIS[capability]
|
||||
instance = operation["target"]["instance"]
|
||||
target = int(operation["parameters"]["position_pct"])
|
||||
target_angle = operation["parameters"].get("angle_deg")
|
||||
start = int(
|
||||
actuator_state.get(instance, {})
|
||||
.get("position", {})
|
||||
.get(axis, {})
|
||||
.get("position_pct", target)
|
||||
)
|
||||
command_frame = self.matrix.encode(command["message"], command["signals"])
|
||||
axis_mask = 1 << MOTION_AXIS_BITS[axis]
|
||||
completion = command["completion"]
|
||||
feedback_values = {
|
||||
completion["signal"]: target,
|
||||
"Moving_Mask": 0,
|
||||
"Fault_Mask": 0,
|
||||
"Position_Valid_Mask": axis_mask,
|
||||
}
|
||||
feedback_frame = self.matrix.encode(completion["message"], feedback_values)
|
||||
midpoint = round((start + target) / 2)
|
||||
samples = list(dict.fromkeys([start, midpoint, target]))
|
||||
return {
|
||||
"ref": operation["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
f"虚构电机反馈确认 {instance} {axis} 从 {start}% 到达 {target}%"
|
||||
),
|
||||
"telemetry": {
|
||||
"motion_axis": axis,
|
||||
"start_position_pct": start,
|
||||
"requested_position_pct": target,
|
||||
"effective_position_pct": target,
|
||||
"requested_angle_deg": target_angle,
|
||||
"effective_angle_deg": target_angle,
|
||||
"position_tolerance_pct": completion.get("tolerance", 0),
|
||||
"motion_samples_pct": samples,
|
||||
"motion_state": "stopped",
|
||||
"moving_mask": 0,
|
||||
"fault_mask": 0,
|
||||
"position_valid_mask": axis_mask,
|
||||
"command_signal": next(
|
||||
name for name in command["signals"] if name.endswith("_Target_Pct")
|
||||
),
|
||||
"feedback_signal": completion["signal"],
|
||||
"command_frame": command_frame,
|
||||
"feedback_frame": feedback_frame,
|
||||
"matrix_source": "synthetic_demo",
|
||||
"simulated": True,
|
||||
},
|
||||
}
|
||||
Executable
+277
@@ -0,0 +1,277 @@
|
||||
"""Generate public intent and function configuration documents."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
|
||||
TARGETS = {
|
||||
"driver": {"device": "seat", "zone": "front_left", "instance": "driver"},
|
||||
"passenger": {"device": "seat", "zone": "front_right", "instance": "passenger"},
|
||||
}
|
||||
CAPABILITIES = {
|
||||
"ventilation": "seat.set_ventilation",
|
||||
"heating": "seat.set_heating",
|
||||
}
|
||||
MOTION_CAPABILITIES = {
|
||||
"longitudinal": "seat.set_longitudinal_position",
|
||||
"cushion_tilt": "seat.set_cushion_tilt",
|
||||
"backrest_recline": "seat.set_backrest_recline",
|
||||
"legrest_height": "seat.set_legrest_height",
|
||||
"legrest_extension": "seat.set_legrest_extension",
|
||||
}
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
def build_intent_ir(
|
||||
parsed: dict[str, Any], utterance: str, request_id: str, provider: str
|
||||
) -> dict[str, Any]:
|
||||
requests = []
|
||||
for action in parsed["actions"]:
|
||||
if action.get("action_type") == "position":
|
||||
parameters = {
|
||||
"axis": action["axis"],
|
||||
"position_pct": action["position_pct"],
|
||||
}
|
||||
if action.get("angle_deg") is not None:
|
||||
parameters["angle_deg"] = action["angle_deg"]
|
||||
requests.append(
|
||||
{
|
||||
"domain": "seat",
|
||||
"goal": (
|
||||
f"Set {action['target']} {action['axis']} position "
|
||||
f"to {action['position_pct']} percent"
|
||||
),
|
||||
"target": TARGETS[action["target"]],
|
||||
"parameters": parameters,
|
||||
"source_span": utterance,
|
||||
}
|
||||
)
|
||||
continue
|
||||
parameters: dict[str, Any] = {
|
||||
"function": action["function"],
|
||||
"state": action["state"],
|
||||
}
|
||||
if action.get("requested_level") is not None:
|
||||
parameters["requested_level"] = action["requested_level"]
|
||||
requests.append(
|
||||
{
|
||||
"domain": "seat",
|
||||
"goal": f"Set {action['target']} {action['function']} {action['state']}",
|
||||
"target": TARGETS[action["target"]],
|
||||
"parameters": parameters,
|
||||
"source_span": utterance,
|
||||
}
|
||||
)
|
||||
|
||||
for preset_action in parsed.get("preset_actions", []):
|
||||
requests.append(
|
||||
{
|
||||
"domain": "seat",
|
||||
"goal": (
|
||||
f"Save the current {preset_action['target']} posture as "
|
||||
f"{preset_action['preset_name']}"
|
||||
),
|
||||
"target": TARGETS[preset_action["target"]],
|
||||
"parameters": {
|
||||
"capability": "seat.save_posture_preset",
|
||||
"preset_name": preset_action["preset_name"],
|
||||
},
|
||||
"source_span": utterance,
|
||||
}
|
||||
)
|
||||
|
||||
for preset_event in parsed.get("preset_events", []):
|
||||
requests.append(
|
||||
{
|
||||
"domain": "seat",
|
||||
"goal": (
|
||||
f"Apply saved posture {preset_event['preset_name']} to "
|
||||
f"{preset_event['target']}"
|
||||
),
|
||||
"target": TARGETS[preset_event["target"]],
|
||||
"parameters": {
|
||||
"capability": "seat.apply_posture_preset",
|
||||
"preset_name": preset_event["preset_name"],
|
||||
},
|
||||
"source_span": utterance,
|
||||
}
|
||||
)
|
||||
|
||||
for deferred in parsed.get("deferred_requests", []):
|
||||
requests.append(
|
||||
{
|
||||
"domain": deferred["domain"],
|
||||
"goal": f"{deferred['desired_change']} {deferred['capability']}",
|
||||
"target": {
|
||||
"device": deferred["domain"],
|
||||
"zone": deferred["target_scope"],
|
||||
},
|
||||
"parameters": {
|
||||
"capability": deferred["capability"],
|
||||
"desired_change": deferred["desired_change"],
|
||||
"requested_value": deferred.get("requested_value"),
|
||||
"requested_parameters": deferred.get("parameters", {}),
|
||||
"availability": "catalog_only",
|
||||
},
|
||||
"source_span": utterance,
|
||||
}
|
||||
)
|
||||
|
||||
detected_domains = parsed.get("detected_domains", ["seat"])
|
||||
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "IntentIR",
|
||||
"metadata": {
|
||||
"id": f"intent-{request_id}",
|
||||
"name": "Seat function intent",
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"generated_at": _now(),
|
||||
"labels": {"provider": provider},
|
||||
},
|
||||
"spec": {
|
||||
"utterance": utterance,
|
||||
"language": "zh-CN",
|
||||
"mode": "cross_domain" if len(detected_domains) > 1 else "single_domain",
|
||||
"detected_domains": detected_domains,
|
||||
"requests": requests,
|
||||
"ambiguities": parsed["ambiguities"],
|
||||
"confidence": parsed["confidence"],
|
||||
"requires_confirmation": bool(parsed["ambiguities"]),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def build_function_config(
|
||||
parsed: dict[str, Any], request_id: str, provider: str
|
||||
) -> dict[str, Any] | None:
|
||||
if not parsed["actions"]:
|
||||
return None
|
||||
|
||||
operations = []
|
||||
assumptions = [
|
||||
"The supplied matrix is partial; this configuration is eligible for virtual execution only."
|
||||
]
|
||||
for index, action in enumerate(parsed["actions"], start=1):
|
||||
if action.get("action_type") == "position":
|
||||
operation_id = (
|
||||
f"{action['target']}_{action['axis']}_{action['position_pct']}pct"
|
||||
)
|
||||
if any(item["id"] == operation_id for item in operations):
|
||||
operation_id = f"{operation_id}_{index}"
|
||||
parameters: dict[str, Any] = {"position_pct": action["position_pct"]}
|
||||
if action.get("angle_deg") is not None:
|
||||
parameters["angle_deg"] = action["angle_deg"]
|
||||
operation = {
|
||||
"id": operation_id,
|
||||
"capability": MOTION_CAPABILITIES[action["axis"]],
|
||||
"target": TARGETS[action["target"]],
|
||||
"parameters": parameters,
|
||||
"timeout_ms": 8000,
|
||||
"failure_policy": "abort",
|
||||
}
|
||||
if action.get("motion_intent") is not None:
|
||||
operation["motion"] = dict(action["motion_intent"])
|
||||
operations.append(operation)
|
||||
assumptions.append(
|
||||
f"Operation {operation_id} uses an explicitly synthetic position CAN mapping; "
|
||||
"it is never eligible for hardware transmission."
|
||||
)
|
||||
continue
|
||||
operation_id = f"{action['target']}_{action['function']}_{action['state']}"
|
||||
if any(item["id"] == operation_id for item in operations):
|
||||
operation_id = f"{operation_id}_{index}"
|
||||
parameters: dict[str, Any] = {"state": action["state"]}
|
||||
if action.get("requested_level") is not None:
|
||||
parameters["level"] = action["requested_level"]
|
||||
operations.append(
|
||||
{
|
||||
"id": operation_id,
|
||||
"capability": CAPABILITIES[action["function"]],
|
||||
"target": TARGETS[action["target"]],
|
||||
"parameters": parameters,
|
||||
"timeout_ms": 3000,
|
||||
"failure_policy": "abort",
|
||||
}
|
||||
)
|
||||
if action.get("requested_level") is not None:
|
||||
assumptions.append(
|
||||
f"Operation {operation_id} targets level {action['requested_level']}; "
|
||||
"the level is simulated because the supplied matrix has no level command signal."
|
||||
)
|
||||
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "FunctionConfig",
|
||||
"metadata": {
|
||||
"id": f"seat-{request_id}",
|
||||
"name": "Seat function",
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"generated_at": _now(),
|
||||
"labels": {"provider": provider, "execution_scope": "virtual_only"},
|
||||
},
|
||||
"spec": {"domain": "seat", "operations": operations, "assumptions": assumptions},
|
||||
}
|
||||
|
||||
|
||||
def build_function_preset(
|
||||
target: str,
|
||||
preset_name: str,
|
||||
device_state: dict[str, Any],
|
||||
request_id: str,
|
||||
provider: str,
|
||||
) -> dict[str, Any]:
|
||||
operations = []
|
||||
for axis, capability in MOTION_CAPABILITIES.items():
|
||||
motion_state = device_state["position"][axis]
|
||||
parameters: dict[str, Any] = {
|
||||
"position_pct": int(motion_state["position_pct"])
|
||||
}
|
||||
if motion_state.get("angle_deg") is not None:
|
||||
parameters["angle_deg"] = motion_state["angle_deg"]
|
||||
operations.append(
|
||||
{
|
||||
"id": f"{target}_{axis}_preset",
|
||||
"capability": capability,
|
||||
"target": TARGETS[target],
|
||||
"parameters": parameters,
|
||||
"timeout_ms": 8000,
|
||||
"failure_policy": "abort",
|
||||
}
|
||||
)
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "FunctionPreset",
|
||||
"metadata": {
|
||||
"id": f"seat-preset-{request_id}",
|
||||
"name": preset_name,
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"generated_at": _now(),
|
||||
"description": f"User-saved five-axis posture for {target}.",
|
||||
"labels": {
|
||||
"provider": provider,
|
||||
"target": target,
|
||||
"storage_scope": "conversation_durable",
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"operations": operations,
|
||||
"locked_parameters": [
|
||||
f"operations.{index}.parameters.position_pct"
|
||||
for index in range(len(operations))
|
||||
],
|
||||
"applicability": {
|
||||
"target": target,
|
||||
"execution_mode": "dry_run",
|
||||
},
|
||||
},
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
"""Compile seat functions to the CAN protocol implemented by the T2G IAR demos."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
STATE_VALUES = {"on": 1, "off": 2}
|
||||
|
||||
|
||||
class T2GSeatCompiler:
|
||||
"""Deterministic compiler for the HIL-verified T2G seat bench protocol."""
|
||||
|
||||
version = "1.3.0"
|
||||
|
||||
def __init__(self, mapping_path: Path, calibration_path: Path):
|
||||
self.mapping = yaml.safe_load(mapping_path.read_text(encoding="utf-8"))
|
||||
self.calibration = yaml.safe_load(calibration_path.read_text(encoding="utf-8"))
|
||||
self.mapping_version = self.mapping["metadata"]["version"]
|
||||
self.axes = self.calibration["axes"]
|
||||
self.protocol = self.calibration["protocol"]
|
||||
self.supported = {
|
||||
(item["capability"], item.get("target_zone"))
|
||||
for item in self.mapping["spec"]["mappings"]
|
||||
}
|
||||
|
||||
def compile(
|
||||
self,
|
||||
function_config: dict[str, Any],
|
||||
actuator_state: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
operations = function_config["spec"]["operations"]
|
||||
self._validate_shared_physical_targets(operations)
|
||||
thermal_operations = [
|
||||
item
|
||||
for item in operations
|
||||
if item["capability"] in {"seat.set_ventilation", "seat.set_heating"}
|
||||
]
|
||||
motion_operations = [item for item in operations if item not in thermal_operations]
|
||||
commands: list[dict[str, Any]] = []
|
||||
|
||||
if thermal_operations:
|
||||
commands.extend(
|
||||
self._compile_thermal(thermal_operations, actuator_state or {})
|
||||
)
|
||||
commands.extend(self._compile_motion(motion_operations, len(commands)))
|
||||
|
||||
canonical = json.dumps(
|
||||
function_config, ensure_ascii=False, sort_keys=True
|
||||
).encode("utf-8")
|
||||
metadata = function_config["metadata"]
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "CommandPlan",
|
||||
"metadata": {
|
||||
"id": f"plan-t2g-{metadata['request_id']}",
|
||||
"name": "T2G seat hardware CAN command plan",
|
||||
"version": "1.0.0",
|
||||
"request_id": metadata["request_id"],
|
||||
"labels": {
|
||||
"execution_scope": "hardware_candidate",
|
||||
"protocol": "t2g_seat_v1",
|
||||
},
|
||||
},
|
||||
"spec": {
|
||||
"domain": "seat",
|
||||
"platform": "t2g_seat_bench",
|
||||
"source": {
|
||||
"config_id": metadata["id"],
|
||||
"config_version": metadata["version"],
|
||||
"config_sha256": hashlib.sha256(canonical).hexdigest(),
|
||||
"compiler_version": self.version,
|
||||
"mapping_version": self.mapping_version,
|
||||
},
|
||||
"commands": commands,
|
||||
},
|
||||
}
|
||||
|
||||
def _compile_thermal(
|
||||
self,
|
||||
operations: list[dict[str, Any]],
|
||||
actuator_state: dict[str, Any],
|
||||
) -> list[dict[str, Any]]:
|
||||
state = actuator_state.get("driver") or actuator_state.get("passenger", {})
|
||||
desired = {
|
||||
"ventilation": dict(state.get("ventilation", {"state": "off", "level": 0})),
|
||||
"heating": dict(state.get("heating", {"state": "off", "level": 0})),
|
||||
}
|
||||
for operation in operations:
|
||||
self._require_supported(operation)
|
||||
feature = operation["capability"].removeprefix("seat.set_")
|
||||
requested_state = str(operation["parameters"]["state"])
|
||||
previous_level = int(desired[feature].get("level", 0))
|
||||
level = (
|
||||
int(operation["parameters"].get("level", previous_level or 1))
|
||||
if requested_state == "on"
|
||||
else 0
|
||||
)
|
||||
desired[feature] = {"state": requested_state, "level": level}
|
||||
|
||||
operation_ref = ",".join(item["id"] for item in operations)
|
||||
commands: list[dict[str, Any]] = []
|
||||
if desired["heating"]["state"] == "on":
|
||||
commands.append(
|
||||
{
|
||||
"id": "t2g_seat_thermal_mode",
|
||||
"operation_ref": operation_ref,
|
||||
"bus": "seat_can",
|
||||
"message": "T2G_Seat_Thermal_Mode_3CE",
|
||||
"signals": {"Driver_Seat_Thermal_Mode": 1},
|
||||
"send_offset_ms": 0,
|
||||
}
|
||||
)
|
||||
commands.append(
|
||||
{
|
||||
"id": "t2g_seat_thermal_control",
|
||||
"operation_ref": operation_ref,
|
||||
"bus": "seat_can",
|
||||
"message": "T2G_Seat_Thermal_Control_29E",
|
||||
"signals": {
|
||||
"Driver_Seat_Heating_Gear": int(desired["heating"]["level"]),
|
||||
"Driver_Seat_Heating_On": STATE_VALUES[desired["heating"]["state"]],
|
||||
"Driver_Seat_Ventilation_Gear": int(desired["ventilation"]["level"]),
|
||||
"Driver_Seat_Ventilation_Mode": 1,
|
||||
"Driver_Seat_Ventilation_On": STATE_VALUES[
|
||||
desired["ventilation"]["state"]
|
||||
],
|
||||
},
|
||||
"send_offset_ms": 20 if commands else 0,
|
||||
}
|
||||
)
|
||||
return commands
|
||||
|
||||
def _compile_motion(
|
||||
self, operations: list[dict[str, Any]], command_offset: int
|
||||
) -> list[dict[str, Any]]:
|
||||
grouped: list[list[dict[str, Any]]] = []
|
||||
by_capability: dict[str, list[dict[str, Any]]] = {}
|
||||
for operation in operations:
|
||||
self._require_supported(operation)
|
||||
capability = operation["capability"]
|
||||
if capability not in by_capability:
|
||||
by_capability[capability] = []
|
||||
grouped.append(by_capability[capability])
|
||||
by_capability[capability].append(operation)
|
||||
|
||||
commands = []
|
||||
for index, physical_operations in enumerate(grouped):
|
||||
operation = physical_operations[0]
|
||||
capability = operation["capability"]
|
||||
try:
|
||||
axis = self.axes[capability]
|
||||
except KeyError as exc:
|
||||
raise ValueError(
|
||||
f"T2G IAR firmware has no calibrated actuator for {capability}"
|
||||
) from exc
|
||||
maximum = int(axis["maximum_value_x100"])
|
||||
position_pct = int(operation["parameters"]["position_pct"])
|
||||
target = round(maximum * position_pct / 100)
|
||||
tolerance = max(1, round(maximum / 100))
|
||||
motion = operation.get("motion")
|
||||
if motion is not None and motion.get("mode") == "relative":
|
||||
delta_pct = int(motion["delta_position_pct"])
|
||||
maximum_delta = int(axis["max_uncalibrated_relative_pct"])
|
||||
if delta_pct <= maximum_delta:
|
||||
commands.append(
|
||||
self._compile_uncalibrated_relative(
|
||||
operation,
|
||||
physical_operations,
|
||||
axis,
|
||||
command_offset + index,
|
||||
)
|
||||
)
|
||||
continue
|
||||
# A large relative request cannot safely be approximated by a
|
||||
# long open-loop duty command or by several chained commands.
|
||||
# Compile its already-resolved target as an absolute move. The
|
||||
# executor will check the board's live Position_Valid bit before
|
||||
# putting Opcode 1 on the bus.
|
||||
commands.append(
|
||||
{
|
||||
"id": f"t2g_move_{operation['id']}",
|
||||
"operation_ref": ",".join(
|
||||
item["id"] for item in physical_operations
|
||||
),
|
||||
"bus": "seat_can",
|
||||
"message": "T2G_Seat_Move_Command_300",
|
||||
"signals": {
|
||||
"Direction": 0,
|
||||
"Motor_Selector": int(axis["motor_selector"]),
|
||||
"Opcode": int(self.protocol["absolute_position_opcode"]),
|
||||
"Param_A": target,
|
||||
"Param_B": int(self.protocol["default_duty_percent"]),
|
||||
"Protocol_Version": int(self.protocol["version"]),
|
||||
# Allocated immediately before hardware transmission. Zero keeps
|
||||
# the compiled plan deterministic and is never put on the bus.
|
||||
"Sequence": 0,
|
||||
},
|
||||
"send_offset_ms": (command_offset + index) * 100,
|
||||
"completion": {
|
||||
"message": "T2G_Seat_Move_Motor_Status_302",
|
||||
"signal": "Value_X100",
|
||||
"operator": "within",
|
||||
"value": target,
|
||||
"tolerance": tolerance,
|
||||
"timeout_ms": int(operation.get("timeout_ms", 8000)),
|
||||
},
|
||||
}
|
||||
)
|
||||
return commands
|
||||
|
||||
def _compile_uncalibrated_relative(
|
||||
self,
|
||||
operation: dict[str, Any],
|
||||
physical_operations: list[dict[str, Any]],
|
||||
axis: dict[str, Any],
|
||||
command_index: int,
|
||||
) -> dict[str, Any]:
|
||||
motion = dict(operation["motion"])
|
||||
delta_pct = int(motion["delta_position_pct"])
|
||||
maximum_delta = int(axis["max_uncalibrated_relative_pct"])
|
||||
if delta_pct <= 0:
|
||||
raise ValueError(
|
||||
"relative seat movement resolved to zero travel; no motor command was generated"
|
||||
)
|
||||
if delta_pct > maximum_delta:
|
||||
raise ValueError(
|
||||
"internal compiler error: an oversized relative movement must be "
|
||||
"compiled as a calibrated absolute target"
|
||||
)
|
||||
|
||||
duration_ms = delta_pct * int(axis["timed_duty_ms_per_pct"])
|
||||
duration_ms = max(
|
||||
int(self.protocol["timed_duty_min_duration_ms"]), duration_ms
|
||||
)
|
||||
maximum_duration = int(self.protocol["timed_duty_max_duration_ms"])
|
||||
if duration_ms > maximum_duration:
|
||||
raise ValueError(
|
||||
"uncalibrated relative seat movement would exceed the bounded "
|
||||
f"{maximum_duration} ms timed-motion limit"
|
||||
)
|
||||
direction_name = str(motion["direction"])
|
||||
direction = int(self.protocol[f"{direction_name}_direction"])
|
||||
timeout_ms = max(
|
||||
int(operation.get("timeout_ms", 8000)), duration_ms + 3000
|
||||
)
|
||||
return {
|
||||
"id": f"t2g_move_{operation['id']}",
|
||||
"operation_ref": ",".join(
|
||||
item["id"] for item in physical_operations
|
||||
),
|
||||
"bus": "seat_can",
|
||||
"message": "T2G_Seat_Move_Command_300",
|
||||
"signals": {
|
||||
"Direction": direction,
|
||||
"Motor_Selector": int(axis["motor_selector"]),
|
||||
"Opcode": int(self.protocol["timed_duty_opcode"]),
|
||||
"Param_A": duration_ms,
|
||||
"Param_B": int(self.protocol["default_duty_percent"]),
|
||||
"Protocol_Version": int(self.protocol["version"]),
|
||||
"Sequence": 0,
|
||||
},
|
||||
"send_offset_ms": command_index * 100,
|
||||
"completion": {
|
||||
"message": "T2G_Seat_Move_Motor_Status_302",
|
||||
"signal": "Task_State",
|
||||
"operator": "eq",
|
||||
"value": 8,
|
||||
"timeout_ms": timeout_ms,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _validate_shared_physical_targets(
|
||||
operations: list[dict[str, Any]],
|
||||
) -> None:
|
||||
requested: dict[str, dict[str, Any]] = {}
|
||||
for operation in operations:
|
||||
capability = operation["capability"]
|
||||
request = {
|
||||
"parameters": dict(operation.get("parameters", {})),
|
||||
"motion": dict(operation["motion"]) if operation.get("motion") else None,
|
||||
}
|
||||
previous = requested.get(capability)
|
||||
if previous is not None and previous != request:
|
||||
raise ValueError(
|
||||
"driver and passenger aliases request conflicting values for "
|
||||
f"the same physical capability {capability}"
|
||||
)
|
||||
requested[capability] = request
|
||||
|
||||
def _require_supported(self, operation: dict[str, Any]) -> None:
|
||||
key = (operation["capability"], operation["target"]["zone"])
|
||||
if key not in self.supported:
|
||||
raise ValueError(
|
||||
"T2G hardware mapping does not support "
|
||||
f"capability={key[0]}, zone={key[1]}"
|
||||
)
|
||||
+713
@@ -0,0 +1,713 @@
|
||||
"""Hardware-in-the-loop executor for the verified T2G seat CAN protocol."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any, Callable
|
||||
|
||||
from cognitive_agent.infrastructure.can.gateway import CanBus, CanFrame
|
||||
from cognitive_agent.infrastructure.firmware import T2GFirmwareGuard
|
||||
|
||||
from .executor import MOTION_CAPABILITY_TO_AXIS
|
||||
from .matrix import SeatCanMatrix
|
||||
|
||||
|
||||
MOTOR_RESULT_NAMES = {
|
||||
0: "NONE",
|
||||
1: "RUNNING",
|
||||
2: "OK",
|
||||
3: "STOPPED",
|
||||
4: "STALL",
|
||||
5: "ENDPOINT",
|
||||
6: "TIMEOUT",
|
||||
7: "DRIVER_FAULT",
|
||||
8: "INVALID",
|
||||
9: "CALIBRATION_FAILED",
|
||||
}
|
||||
|
||||
MOTOR_EVENT_NAMES = {
|
||||
0: "NONE",
|
||||
1: "TARGET_REACHED",
|
||||
2: "NEAR_ENDPOINT",
|
||||
3: "FAR_ENDPOINT",
|
||||
4: "STALL",
|
||||
5: "TIMEOUT",
|
||||
6: "ABORTED",
|
||||
7: "DRIVER_FAULT",
|
||||
8: "CALIBRATION_FAILED",
|
||||
}
|
||||
|
||||
|
||||
class HardwareExecutionError(RuntimeError):
|
||||
"""Raised for rejected, faulted or timed-out physical operations."""
|
||||
|
||||
def __init__(
|
||||
self, message: str, *, telemetry: dict[str, Any] | None = None
|
||||
) -> None:
|
||||
super().__init__(message)
|
||||
self.telemetry = dict(telemetry or {})
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class HardwareSeatExecutor:
|
||||
"""Serialize access to one adapter and require real firmware feedback."""
|
||||
|
||||
_adapter_lock = threading.Lock()
|
||||
_sequence = 0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
matrix: SeatCanMatrix,
|
||||
bus_factory: Callable[[], CanBus],
|
||||
*,
|
||||
thermal_timeout_ms: int = 2_000,
|
||||
ack_timeout_ms: int = 1_500,
|
||||
axis_maximum_x100: dict[str, int] | None = None,
|
||||
firmware_guard: T2GFirmwareGuard | None = None,
|
||||
):
|
||||
self.matrix = matrix
|
||||
self.bus_factory = bus_factory
|
||||
self.thermal_timeout_ms = thermal_timeout_ms
|
||||
self.ack_timeout_ms = ack_timeout_ms
|
||||
self.axis_maximum_x100 = dict(axis_maximum_x100 or {})
|
||||
self.firmware_guard = firmware_guard
|
||||
|
||||
@classmethod
|
||||
def _next_sequence(cls) -> int:
|
||||
cls._sequence = (cls._sequence + 1) & 0xFFFF
|
||||
if cls._sequence == 0:
|
||||
cls._sequence = 1
|
||||
return cls._sequence
|
||||
|
||||
def execute(
|
||||
self,
|
||||
command_plan: dict[str, Any],
|
||||
function_config: dict[str, Any],
|
||||
actuator_state: dict[str, Any] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
del actuator_state # physical feedback, not virtual state, is authoritative here
|
||||
started_at = _now()
|
||||
operations = {
|
||||
item["id"]: item for item in function_config["spec"]["operations"]
|
||||
}
|
||||
results: list[dict[str, Any]] = []
|
||||
commands = command_plan["spec"]["commands"]
|
||||
has_motion = any(item["message"] == "T2G_Seat_Move_Command_300" for item in commands)
|
||||
bus: CanBus | None = None
|
||||
motion_armed = False
|
||||
|
||||
with self._adapter_lock:
|
||||
try:
|
||||
if self.firmware_guard is not None:
|
||||
bus, firmware_telemetry = self.firmware_guard.prepare_bus()
|
||||
results.append(
|
||||
{
|
||||
"ref": "seat-controller-firmware",
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
"repository Seat_Controller_demo was programmed and verified"
|
||||
if firmware_telemetry.get("programmed_this_request")
|
||||
else "repository Seat_Controller_demo identity was verified"
|
||||
),
|
||||
"telemetry": {
|
||||
**firmware_telemetry,
|
||||
"simulated": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
bus = self.bus_factory()
|
||||
if has_motion:
|
||||
self._stop_all(bus, require_ack=True)
|
||||
motion_armed = True
|
||||
previous_offset = 0
|
||||
for command in commands:
|
||||
offset = int(command.get("send_offset_ms", 0))
|
||||
if offset > previous_offset:
|
||||
time.sleep((offset - previous_offset) / 1000)
|
||||
previous_offset = offset
|
||||
operation_ids = command["operation_ref"].split(",")
|
||||
referenced = [operations[item] for item in operation_ids]
|
||||
if command["message"] == "T2G_Seat_Thermal_Mode_3CE":
|
||||
frame = self._send_encoded(bus, command["message"], command["signals"])
|
||||
results.append(
|
||||
{
|
||||
"ref": command["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": "T2G heater manual control mode sent to the physical CAN bus",
|
||||
"telemetry": {
|
||||
"control_step": "thermal_mode",
|
||||
"command_frame": frame.public(),
|
||||
"simulated": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif command["message"] == "T2G_Seat_Thermal_Control_29E":
|
||||
results.extend(self._execute_thermal(bus, command, referenced))
|
||||
elif command["message"] == "T2G_Seat_Move_Command_300":
|
||||
results.extend(self._execute_motion(bus, command, referenced))
|
||||
else:
|
||||
raise HardwareExecutionError(
|
||||
f"hardware executor refuses unknown message {command['message']}"
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - normalize transport and protocol failures
|
||||
if bus is not None and motion_armed:
|
||||
self._best_effort_stop(bus)
|
||||
operation_ref = self._first_unreported_operation(operations, results)
|
||||
results.append(
|
||||
{
|
||||
"ref": operation_ref,
|
||||
"domain": "seat",
|
||||
"status": "failed",
|
||||
"message": str(exc),
|
||||
"telemetry": {
|
||||
**getattr(exc, "telemetry", {}),
|
||||
"error_type": type(exc).__name__,
|
||||
"stop_all_attempted": bool(bus is not None and motion_armed),
|
||||
"simulated": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
finally:
|
||||
if bus is not None:
|
||||
bus.close()
|
||||
|
||||
succeeded = all(item["status"] == "succeeded" for item in results)
|
||||
request_id = function_config["metadata"]["request_id"]
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "ExecutionReport",
|
||||
"metadata": {
|
||||
"id": f"report-{request_id}",
|
||||
"name": "T2G seat hardware execution report",
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"labels": {"simulation": "false", "feedback": "physical_can"},
|
||||
},
|
||||
"spec": {
|
||||
"mode": "hil",
|
||||
"status": "succeeded" if succeeded else "failed",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
},
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _first_unreported_operation(
|
||||
operations: dict[str, dict[str, Any]], results: list[dict[str, Any]]
|
||||
) -> str:
|
||||
reported = {item["ref"] for item in results}
|
||||
return next((item for item in operations if item not in reported), next(iter(operations)))
|
||||
|
||||
def _send_encoded(
|
||||
self, bus: CanBus, message: str, signals: dict[str, int]
|
||||
) -> CanFrame:
|
||||
encoded = self.matrix.encode(message, signals)
|
||||
return bus.send(int(encoded["frame_id"], 16), bytes.fromhex(encoded["data_hex"]))
|
||||
|
||||
def _receive_until(
|
||||
self,
|
||||
bus: CanBus,
|
||||
arbitration_id: int,
|
||||
timeout_ms: int,
|
||||
predicate: Callable[[dict[str, int], CanFrame], bool],
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
deadline = time.monotonic() + timeout_ms / 1000
|
||||
while True:
|
||||
remaining_ms = round((deadline - time.monotonic()) * 1000)
|
||||
if remaining_ms <= 0:
|
||||
raise HardwareExecutionError(
|
||||
f"timed out after {timeout_ms} ms waiting for CAN 0x{arbitration_id:03X}"
|
||||
)
|
||||
frame = bus.receive(remaining_ms)
|
||||
if frame is None:
|
||||
raise HardwareExecutionError(
|
||||
f"timed out after {timeout_ms} ms waiting for CAN 0x{arbitration_id:03X}"
|
||||
)
|
||||
if frame.arbitration_id != arbitration_id or len(frame.data) < 8:
|
||||
continue
|
||||
values = self.matrix.decode_by_frame_id(arbitration_id, frame.data)
|
||||
if predicate(values, frame):
|
||||
return values, frame
|
||||
|
||||
def _wait_ack(
|
||||
self, bus: CanBus, sequence: int, opcode: int
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
def matches(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Last_Sequence"] != sequence or values["Last_Opcode"] != opcode:
|
||||
return False
|
||||
if values["Ack"] != 1:
|
||||
raise HardwareExecutionError(
|
||||
"T2G movement command rejected: "
|
||||
f"ack={values['Ack']}, reason={values['Reject_Reason']}, "
|
||||
f"safety_fault={values['Safety_Fault']}"
|
||||
)
|
||||
if values["Safety_Inhibit"] or values["Safety_Fault"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G movement safety inhibit/fault: {values['Safety_Fault']}"
|
||||
)
|
||||
return True
|
||||
|
||||
return self._receive_until(bus, 0x301, self.ack_timeout_ms, matches)
|
||||
|
||||
def _stop_all(self, bus: CanBus, *, require_ack: bool) -> tuple[CanFrame, CanFrame | None]:
|
||||
sequence = self._next_sequence()
|
||||
signals = {
|
||||
"Opcode": 9,
|
||||
"Motor_Selector": 3,
|
||||
"Direction": 0,
|
||||
"Protocol_Version": 1,
|
||||
"Sequence": sequence,
|
||||
"Param_A": 0,
|
||||
"Param_B": 0,
|
||||
}
|
||||
sent = self._send_encoded(bus, "T2G_Seat_Move_Command_300", signals)
|
||||
if not require_ack:
|
||||
return sent, None
|
||||
_, ack = self._wait_ack(bus, sequence, 9)
|
||||
return sent, ack
|
||||
|
||||
def _best_effort_stop(self, bus: CanBus) -> None:
|
||||
try:
|
||||
self._stop_all(bus, require_ack=False)
|
||||
except Exception: # noqa: BLE001 - already handling the primary failure
|
||||
pass
|
||||
|
||||
def _execute_thermal(
|
||||
self,
|
||||
bus: CanBus,
|
||||
command: dict[str, Any],
|
||||
operations: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
command_frame = self._send_encoded(bus, command["message"], command["signals"])
|
||||
expected: dict[str, int] = {}
|
||||
for operation in operations:
|
||||
feature = operation["capability"].removeprefix("seat.set_")
|
||||
signal = (
|
||||
"Driver_Seat_Heating_Status"
|
||||
if feature == "heating"
|
||||
else "Driver_Seat_Ventilation_Status"
|
||||
)
|
||||
state = operation["parameters"]["state"]
|
||||
level = int(operation["parameters"].get("level", 1))
|
||||
expected[signal] = 1 if state == "off" else min(level, 3) + 1
|
||||
|
||||
def matches(values: dict[str, int], _: CanFrame) -> bool:
|
||||
return all(values[name] == value for name, value in expected.items())
|
||||
|
||||
values, feedback = self._receive_until(
|
||||
bus, 0x4FA, self.thermal_timeout_ms, matches
|
||||
)
|
||||
output = []
|
||||
for operation in operations:
|
||||
feature = operation["capability"].removeprefix("seat.set_")
|
||||
signal = (
|
||||
"Driver_Seat_Heating_Status"
|
||||
if feature == "heating"
|
||||
else "Driver_Seat_Ventilation_Status"
|
||||
)
|
||||
raw = values[signal]
|
||||
output.append(
|
||||
{
|
||||
"ref": operation["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
f"physical 0x4FA confirmed shared front-seat {feature} "
|
||||
f"state={operation['parameters']['state']}, level={max(0, raw - 1)}"
|
||||
),
|
||||
"telemetry": {
|
||||
"requested_target": operation["target"]["instance"],
|
||||
"physical_target": "t2g_primary_seat",
|
||||
"requested_state": operation["parameters"]["state"],
|
||||
"requested_level": operation["parameters"].get("level"),
|
||||
"feedback_signal": signal,
|
||||
"feedback_value": raw,
|
||||
"command_frame": command_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"hardware_feedback": True,
|
||||
"simulated": False,
|
||||
},
|
||||
}
|
||||
)
|
||||
return output
|
||||
|
||||
def _execute_motion(
|
||||
self,
|
||||
bus: CanBus,
|
||||
command: dict[str, Any],
|
||||
operations: list[dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
if not operations:
|
||||
raise HardwareExecutionError(
|
||||
"a T2G motion frame must reference at least one operation"
|
||||
)
|
||||
operation = operations[0]
|
||||
if any(
|
||||
item["capability"] != operation["capability"]
|
||||
or item["parameters"] != operation["parameters"]
|
||||
or item.get("motion") != operation.get("motion")
|
||||
for item in operations[1:]
|
||||
):
|
||||
raise HardwareExecutionError(
|
||||
"one shared T2G motion frame cannot represent conflicting operations"
|
||||
)
|
||||
signals = dict(command["signals"])
|
||||
motor = int(signals["Motor_Selector"])
|
||||
opcode = int(signals["Opcode"])
|
||||
if opcode == 3:
|
||||
return self._execute_timed_motion(
|
||||
bus, command, operations, signals, motor
|
||||
)
|
||||
|
||||
preflight_values, preflight_frame = self._motor_status_snapshot(bus, motor)
|
||||
if not preflight_values["Position_Valid"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} absolute-position command blocked before send: "
|
||||
"controller Position_Valid=0. Run the explicit endpoint-calibration "
|
||||
"maintenance procedure with the bench clear and emergency stop ready, "
|
||||
"or use a small relative movement request.",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_valid": False,
|
||||
"calibration_required": True,
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
},
|
||||
)
|
||||
|
||||
sequence = self._next_sequence()
|
||||
signals["Sequence"] = sequence
|
||||
command_frame = self._send_encoded(bus, command["message"], signals)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, opcode)
|
||||
target = int(command["completion"]["value"])
|
||||
tolerance = int(command["completion"].get("tolerance", 0))
|
||||
|
||||
def complete(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Motor_Sub_ID"] != motor or values["Motor_Sequence"] != sequence:
|
||||
return False
|
||||
task_state = values["Task_State"]
|
||||
result = values["Result"]
|
||||
if task_state == 9 or result not in {1, 2}:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} failed: task_state={task_state}, result={result}"
|
||||
)
|
||||
if task_state != 8 or result != 2:
|
||||
return False
|
||||
if not values["Position_Valid"]:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} completed without a valid calibrated position"
|
||||
)
|
||||
if abs(values["Value_X100"] - target) > tolerance:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G motor {motor} stopped outside tolerance: "
|
||||
f"actual={values['Value_X100']}, target={target}, tolerance={tolerance}"
|
||||
)
|
||||
return True
|
||||
|
||||
values, feedback = self._receive_until(
|
||||
bus,
|
||||
0x302,
|
||||
int(command["completion"]["timeout_ms"]),
|
||||
complete,
|
||||
)
|
||||
axis = MOTION_CAPABILITY_TO_AXIS[operation["capability"]]
|
||||
maximum = self.axis_maximum_x100.get(operation["capability"])
|
||||
if maximum is None:
|
||||
requested_pct = int(operation["parameters"]["position_pct"])
|
||||
maximum = max(1, target * 100 // max(1, requested_pct))
|
||||
actual_pct = round(values["Value_X100"] * 100 / maximum)
|
||||
common_telemetry = {
|
||||
"motion_axis": axis,
|
||||
"motor_selector": motor,
|
||||
"physical_target": "t2g_primary_seat",
|
||||
"sequence": sequence,
|
||||
"requested_position_pct": int(operation["parameters"]["position_pct"]),
|
||||
"effective_position_pct": actual_pct,
|
||||
"requested_value_x100": target,
|
||||
"effective_value_x100": values["Value_X100"],
|
||||
"position_tolerance_x100": tolerance,
|
||||
"task_state": values["Task_State"],
|
||||
"motor_result": values["Result"],
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"system_ack": ack_values["Ack"],
|
||||
"preflight_frame": preflight_frame.public(),
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"hardware_feedback": True,
|
||||
"simulated": False,
|
||||
}
|
||||
return [
|
||||
{
|
||||
"ref": item["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
f"physical motor {motor} feedback confirmed shared front-seat "
|
||||
f"{axis} at {actual_pct}%"
|
||||
),
|
||||
"telemetry": {
|
||||
**common_telemetry,
|
||||
"requested_target": item["target"]["instance"],
|
||||
},
|
||||
}
|
||||
for item in operations
|
||||
]
|
||||
|
||||
def _motor_status_snapshot(
|
||||
self, bus: CanBus, motor: int
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
return self._receive_until(
|
||||
bus,
|
||||
0x302,
|
||||
max(self.ack_timeout_ms, 1_000),
|
||||
lambda values, _: values["Motor_Sub_ID"] == motor,
|
||||
)
|
||||
|
||||
def _motor_diagnostic_snapshot(
|
||||
self, bus: CanBus, motor: int
|
||||
) -> tuple[dict[str, int], CanFrame]:
|
||||
return self._receive_until(
|
||||
bus,
|
||||
0x303,
|
||||
max(self.ack_timeout_ms, 1_000),
|
||||
lambda values, _: values["Motor_Sub_ID"] == motor,
|
||||
)
|
||||
|
||||
def _execute_timed_motion(
|
||||
self,
|
||||
bus: CanBus,
|
||||
command: dict[str, Any],
|
||||
operations: list[dict[str, Any]],
|
||||
signals: dict[str, int],
|
||||
motor: int,
|
||||
) -> list[dict[str, Any]]:
|
||||
baseline, baseline_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
sequence = self._next_sequence()
|
||||
signals["Sequence"] = sequence
|
||||
command_frame = self._send_encoded(bus, command["message"], signals)
|
||||
ack_values, ack_frame = self._wait_ack(bus, sequence, 3)
|
||||
|
||||
def terminal(values: dict[str, int], _: CanFrame) -> bool:
|
||||
if values["Motor_Sub_ID"] != motor or values["Motor_Sequence"] != sequence:
|
||||
return False
|
||||
task_state = values["Task_State"]
|
||||
result = values["Result"]
|
||||
return task_state in {8, 9} or result not in {1, 2}
|
||||
|
||||
values, feedback = self._receive_until(
|
||||
bus,
|
||||
0x302,
|
||||
int(command["completion"]["timeout_ms"]),
|
||||
terminal,
|
||||
)
|
||||
diagnostic: dict[str, int] | None = None
|
||||
diagnostic_frame: CanFrame | None = None
|
||||
diagnostic_error: str | None = None
|
||||
try:
|
||||
diagnostic, diagnostic_frame = self._motor_diagnostic_snapshot(bus, motor)
|
||||
except HardwareExecutionError as exc:
|
||||
diagnostic_error = str(exc)
|
||||
|
||||
hall_before = int(baseline["Hall_Count"])
|
||||
hall_after = (
|
||||
int(diagnostic["Hall_Count"]) if diagnostic is not None else hall_before
|
||||
)
|
||||
direction = int(signals["Direction"])
|
||||
task_state = int(values["Task_State"])
|
||||
result = int(values["Result"])
|
||||
result_name = MOTOR_RESULT_NAMES.get(result, f"UNKNOWN_{result}")
|
||||
last_event = (
|
||||
int(diagnostic["Last_Motor_Event"])
|
||||
if diagnostic is not None
|
||||
else None
|
||||
)
|
||||
last_event_name = (
|
||||
MOTOR_EVENT_NAMES.get(last_event, f"UNKNOWN_{last_event}")
|
||||
if last_event is not None
|
||||
else "UNAVAILABLE"
|
||||
)
|
||||
hall_delta = abs(hall_after - hall_before)
|
||||
max_hall = max(
|
||||
1,
|
||||
int(diagnostic["Max_Hall_Count"])
|
||||
if diagnostic is not None
|
||||
else int(baseline["Max_Hall_Count"]),
|
||||
)
|
||||
|
||||
if task_state != 8 or result != 2:
|
||||
if result == 4 and hall_delta <= 2:
|
||||
failure_category = "stall_no_hall_activity"
|
||||
diagnostic_hint = (
|
||||
"No sustained Hall activity was observed. The commanded direction may "
|
||||
"already be at a mechanical endpoint or obstructed, or the Motor Hall "
|
||||
"power/signal path may be disconnected. With the bench clear, use a "
|
||||
"small command in the opposite direction to distinguish endpoint from "
|
||||
"a Hall wiring fault; do not run automatic full-range calibration."
|
||||
)
|
||||
elif result == 4:
|
||||
failure_category = "stall_after_hall_activity"
|
||||
diagnostic_hint = (
|
||||
"Hall activity was observed before the stall; inspect the mechanism for "
|
||||
"an endpoint or obstruction in the commanded direction."
|
||||
)
|
||||
elif result == 5 or last_event in {2, 3}:
|
||||
failure_category = "endpoint"
|
||||
diagnostic_hint = (
|
||||
"The controller reported an endpoint. Verify the requested direction "
|
||||
"and establish endpoint calibration before absolute positioning."
|
||||
)
|
||||
else:
|
||||
failure_category = result_name.lower()
|
||||
diagnostic_hint = (
|
||||
"Inspect the controller result, last motor event, driver supply and Hall "
|
||||
"feedback before retrying the movement."
|
||||
)
|
||||
raise HardwareExecutionError(
|
||||
f"T2G timed motor {motor} {result_name}: task_state={task_state}, "
|
||||
f"Hall={hall_before}->{hall_after}, last_event={last_event_name}. "
|
||||
f"{diagnostic_hint}",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"motion_axis": MOTION_CAPABILITY_TO_AXIS[
|
||||
operations[0]["capability"]
|
||||
],
|
||||
"motion_mode": "relative_timed_duty",
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"task_state": task_state,
|
||||
"motor_result": result,
|
||||
"motor_result_name": result_name,
|
||||
"last_motor_event": last_event,
|
||||
"last_motor_event_name": last_event_name,
|
||||
"failure_category": failure_category,
|
||||
"diagnostic_hint": diagnostic_hint,
|
||||
"hall_before": hall_before,
|
||||
"hall_after": hall_after,
|
||||
"hall_delta": hall_delta,
|
||||
"max_hall_count": max_hall,
|
||||
"direction": direction,
|
||||
"duration_ms": int(signals["Param_A"]),
|
||||
"duty_percent": int(signals["Param_B"]),
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_before_frame": baseline_frame.public(),
|
||||
"diagnostic_frame": (
|
||||
diagnostic_frame.public()
|
||||
if diagnostic_frame is not None
|
||||
else None
|
||||
),
|
||||
"diagnostic_read_error": diagnostic_error,
|
||||
},
|
||||
)
|
||||
|
||||
if diagnostic is None or diagnostic_frame is None:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G timed motor {motor} completed but diagnostic Hall feedback "
|
||||
f"was unavailable: {diagnostic_error}",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"task_state": task_state,
|
||||
"motor_result": result,
|
||||
"motor_result_name": result_name,
|
||||
"hall_before": hall_before,
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_before_frame": baseline_frame.public(),
|
||||
"diagnostic_read_error": diagnostic_error,
|
||||
},
|
||||
)
|
||||
|
||||
direction_verified: bool | None
|
||||
if direction == 1:
|
||||
direction_verified = hall_after > hall_before
|
||||
elif hall_before > 0:
|
||||
direction_verified = hall_after < hall_before
|
||||
else:
|
||||
# At an uncalibrated zero estimate, NEAR movement cannot decrement
|
||||
# the public Hall position. COMPLETE/OK still proves raw Hall edges
|
||||
# because the firmware refuses timed success without one.
|
||||
direction_verified = None
|
||||
if direction_verified is False:
|
||||
raise HardwareExecutionError(
|
||||
f"T2G timed motor {motor} completed but Hall direction did not match "
|
||||
f"the command: before={hall_before}, after={hall_after}, direction={direction}",
|
||||
telemetry={
|
||||
"motor_selector": motor,
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"hall_before": hall_before,
|
||||
"hall_after": hall_after,
|
||||
"hall_direction_verified": False,
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_frame": diagnostic_frame.public(),
|
||||
},
|
||||
)
|
||||
|
||||
axis = MOTION_CAPABILITY_TO_AXIS[operations[0]["capability"]]
|
||||
common_telemetry = {
|
||||
"motion_axis": axis,
|
||||
"motion_mode": "relative_timed_duty",
|
||||
"motor_selector": motor,
|
||||
"physical_target": "t2g_primary_seat",
|
||||
"sequence": sequence,
|
||||
"requested_position_pct": int(
|
||||
operations[0]["parameters"]["position_pct"]
|
||||
),
|
||||
"requested_delta_position_pct": int(
|
||||
operations[0].get("motion", {}).get("delta_position_pct", 0)
|
||||
),
|
||||
"requested_delta_angle_deg": operations[0]
|
||||
.get("motion", {})
|
||||
.get("delta_angle_deg"),
|
||||
"duration_ms": int(signals["Param_A"]),
|
||||
"duty_percent": int(signals["Param_B"]),
|
||||
"direction": direction,
|
||||
"task_state": task_state,
|
||||
"motor_result": result,
|
||||
"motor_result_name": result_name,
|
||||
"last_motor_event": last_event,
|
||||
"last_motor_event_name": last_event_name,
|
||||
"position_valid": bool(values["Position_Valid"]),
|
||||
"absolute_position_available": bool(values["Position_Valid"]),
|
||||
"hall_before": hall_before,
|
||||
"hall_after": hall_after,
|
||||
"hall_delta": hall_delta,
|
||||
"max_hall_count": max_hall,
|
||||
"observed_delta_position_pct": round(hall_delta * 100 / max_hall),
|
||||
"hall_direction_verified": direction_verified,
|
||||
"system_ack": ack_values["Ack"],
|
||||
"command_frame": command_frame.public(),
|
||||
"ack_frame": ack_frame.public(),
|
||||
"feedback_frame": feedback.public(),
|
||||
"diagnostic_before_frame": baseline_frame.public(),
|
||||
"diagnostic_frame": diagnostic_frame.public(),
|
||||
"hardware_feedback": True,
|
||||
"simulated": False,
|
||||
}
|
||||
return [
|
||||
{
|
||||
"ref": item["id"],
|
||||
"domain": "seat",
|
||||
"status": "succeeded",
|
||||
"message": (
|
||||
f"physical motor {motor} COMPLETE/OK confirmed relative {axis} "
|
||||
f"movement with Hall {hall_before}->{hall_after}; absolute position "
|
||||
"remains unavailable until endpoint calibration"
|
||||
),
|
||||
"telemetry": {
|
||||
**common_telemetry,
|
||||
"requested_target": item["target"]["instance"],
|
||||
},
|
||||
}
|
||||
for item in operations
|
||||
]
|
||||
Executable
+433
@@ -0,0 +1,433 @@
|
||||
"""Deterministic fallback parser for the supported seat thermal vocabulary."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
TARGET_PATTERN = re.compile(r"主副驾|前排|主驾|驾驶位|司机位|副驾|乘客位")
|
||||
FEATURE_PATTERN = re.compile(r"通风|加热")
|
||||
STATE_PATTERN = re.compile(r"关闭|关掉|停止|取消|打开|开启|启动|开")
|
||||
LEVEL_PATTERN = re.compile(r"([1-5一二三四五])\s*档")
|
||||
VENTILATION_FEEDBACK_PATTERN = re.compile(r"(?<!不)(?<!加)热|(?<!不)闷|出汗")
|
||||
HEATING_FEEDBACK_PATTERN = re.compile(r"(?<!不)冷|(?<!不)凉")
|
||||
MILD_PATTERN = re.compile(r"有点|一点|稍微|微微")
|
||||
STRONG_PATTERN = re.compile(r"很|太|非常|特别|难受")
|
||||
AIRFLOW_CAUSE_PATTERN = re.compile(r"吹|风太大|风吹")
|
||||
FIRST_PERSON_PATTERN = re.compile(r"我|自己")
|
||||
VEHICLE_SCOPE_PATTERN = re.compile(r"车上|车内|车里|全车|整车|整个车")
|
||||
|
||||
TARGETS = {
|
||||
"主驾": ["driver"],
|
||||
"驾驶位": ["driver"],
|
||||
"司机位": ["driver"],
|
||||
"副驾": ["passenger"],
|
||||
"乘客位": ["passenger"],
|
||||
"主副驾": ["driver", "passenger"],
|
||||
"前排": ["driver", "passenger"],
|
||||
}
|
||||
FEATURES = {"通风": "ventilation", "加热": "heating"}
|
||||
STATES = {
|
||||
"打开": "on",
|
||||
"开启": "on",
|
||||
"启动": "on",
|
||||
"开": "on",
|
||||
"关闭": "off",
|
||||
"关掉": "off",
|
||||
"停止": "off",
|
||||
"取消": "off",
|
||||
}
|
||||
LEVELS = {"一": 1, "二": 2, "三": 3, "四": 4, "五": 5}
|
||||
|
||||
|
||||
def _nearest_preceding_target(clause: str, position: int) -> str | None:
|
||||
candidates = [match for match in TARGET_PATTERN.finditer(clause) if match.start() <= position]
|
||||
if candidates:
|
||||
return candidates[-1].group(0)
|
||||
matches = list(TARGET_PATTERN.finditer(clause))
|
||||
return matches[0].group(0) if len(matches) == 1 else None
|
||||
|
||||
|
||||
def _state_for_feature(clause: str, position: int) -> str | None:
|
||||
states = list(STATE_PATTERN.finditer(clause))
|
||||
if not states:
|
||||
return None
|
||||
following = [match for match in states if match.start() >= position]
|
||||
if following and following[0].start() - position <= 8:
|
||||
return STATES[following[0].group(0)]
|
||||
if len(states) == 1:
|
||||
return STATES[states[0].group(0)]
|
||||
preceding = [match for match in states if match.start() < position]
|
||||
return STATES[preceding[-1].group(0)] if preceding else None
|
||||
|
||||
|
||||
def _level_for_clause(clause: str) -> int | None:
|
||||
match = LEVEL_PATTERN.search(clause)
|
||||
if not match:
|
||||
return None
|
||||
token = match.group(1)
|
||||
return int(token) if token.isdigit() else LEVELS[token]
|
||||
|
||||
|
||||
def _context_device(
|
||||
context: dict[str, Any] | None, target: str, function: str
|
||||
) -> dict[str, Any]:
|
||||
if not context:
|
||||
return {"state": "off", "level": 0}
|
||||
return (
|
||||
context.get("devices", {})
|
||||
.get(target, {})
|
||||
.get(function, {"state": "off", "level": 0})
|
||||
)
|
||||
|
||||
|
||||
def _feedback_targets(
|
||||
text: str, context: dict[str, Any] | None, parsed: dict[str, Any]
|
||||
) -> tuple[list[str], str]:
|
||||
matches = list(TARGET_PATTERN.finditer(text))
|
||||
if matches:
|
||||
targets: list[str] = []
|
||||
for match in matches:
|
||||
for target in TARGETS[match.group(0)]:
|
||||
if target not in targets:
|
||||
targets.append(target)
|
||||
return targets, "explicit"
|
||||
parsed_targets = list(
|
||||
dict.fromkeys(
|
||||
action.get("target")
|
||||
for action in parsed.get("actions", [])
|
||||
if action.get("target") in {"driver", "passenger"}
|
||||
)
|
||||
)
|
||||
if parsed_targets:
|
||||
return parsed_targets, "model"
|
||||
last_target = context.get("last_target") if context else None
|
||||
if last_target in {"driver", "passenger"}:
|
||||
return [last_target], "history"
|
||||
if FIRST_PERSON_PATTERN.search(text):
|
||||
return ["driver"], "speaker_default"
|
||||
return [], "missing"
|
||||
|
||||
|
||||
def _inferred_level(text: str) -> tuple[int, str]:
|
||||
explicit_level = _level_for_clause(text)
|
||||
if explicit_level is not None:
|
||||
return explicit_level, "explicit"
|
||||
if STRONG_PATTERN.search(text):
|
||||
return 3, "strong"
|
||||
if MILD_PATTERN.search(text):
|
||||
return 1, "mild"
|
||||
return 2, "normal"
|
||||
|
||||
|
||||
def _decrease_action(
|
||||
target: str, function: str, current: dict[str, Any]
|
||||
) -> tuple[dict[str, Any], int, int]:
|
||||
previous_level = int(current.get("level") or 0)
|
||||
if current.get("state") == "on" and previous_level > 1:
|
||||
next_level = previous_level - 1
|
||||
return (
|
||||
{
|
||||
"target": target,
|
||||
"function": function,
|
||||
"state": "on",
|
||||
"requested_level": next_level,
|
||||
},
|
||||
previous_level,
|
||||
next_level,
|
||||
)
|
||||
return (
|
||||
{
|
||||
"target": target,
|
||||
"function": function,
|
||||
"state": "off",
|
||||
"requested_level": None,
|
||||
},
|
||||
previous_level,
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def resolve_contextual_intent(
|
||||
parsed: dict[str, Any], utterance: str, context: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Resolve implicit comfort feedback against the current virtual seat state."""
|
||||
|
||||
result = deepcopy(parsed)
|
||||
text = re.sub(r"\s+", "", utterance.strip())
|
||||
ventilation_cues = list(dict.fromkeys(VENTILATION_FEEDBACK_PATTERN.findall(text)))
|
||||
cold_cues = list(dict.fromkeys(HEATING_FEEDBACK_PATTERN.findall(text)))
|
||||
if not ventilation_cues and not cold_cues:
|
||||
return result
|
||||
|
||||
if VEHICLE_SCOPE_PATTERN.search(text) and not TARGET_PATTERN.search(text):
|
||||
result["actions"] = []
|
||||
result["ambiguities"] = [
|
||||
{
|
||||
"field": "domain",
|
||||
"question": "该请求作用于整车舱空气,需要由空调/新风域执行;当前座椅单域不能代替执行。",
|
||||
}
|
||||
]
|
||||
result["decision_summary"] = [
|
||||
"“车上/车内”表示整车舱范围,不是主驾或副驾单座位。",
|
||||
"“太闷”属于舱内空气质量/新风需求,应路由到 climate 域。",
|
||||
"当前未接入空调执行器,未将整车请求错误下发到任一座椅。",
|
||||
]
|
||||
result["detected_domains"] = ["climate"]
|
||||
result["scope"] = "vehicle"
|
||||
result["confidence"] = max(float(result.get("confidence", 0)), 0.96)
|
||||
return result
|
||||
|
||||
targets, target_source = _feedback_targets(text, context, result)
|
||||
if not targets:
|
||||
if not any(item.get("field") == "target" for item in result["ambiguities"]):
|
||||
result["ambiguities"].append(
|
||||
{"field": "target", "question": "该舒适反馈需要作用于主驾还是副驾?"}
|
||||
)
|
||||
return result
|
||||
|
||||
labels = {
|
||||
"driver": "主驾",
|
||||
"passenger": "副驾",
|
||||
"ventilation": "通风",
|
||||
"heating": "加热",
|
||||
}
|
||||
summaries: list[str] = []
|
||||
if target_source == "history":
|
||||
summaries.append(f"本轮省略了座位,沿用上一轮的{labels[targets[0]]}目标。")
|
||||
elif target_source == "speaker_default":
|
||||
summaries.append("当前为座椅单域驾驶员交互,将第一人称“我”解析为主驾。")
|
||||
|
||||
planned_actions: list[dict[str, Any]] = []
|
||||
desired_level, intensity = _inferred_level(text)
|
||||
airflow_is_cause = bool(AIRFLOW_CAUSE_PATTERN.search(text))
|
||||
is_cold = bool(cold_cues)
|
||||
is_hot = any(cue in {"热", "出汗"} for cue in ventilation_cues)
|
||||
for target in targets:
|
||||
ventilation = _context_device(context, target, "ventilation")
|
||||
heating = _context_device(context, target, "heating")
|
||||
cue_text = "、".join(cold_cues if is_cold else ventilation_cues)
|
||||
summaries.append(f"检测到{labels[target]}“{cue_text}”舒适反馈,开始核对当前执行器状态。")
|
||||
|
||||
if cold_cues and ventilation_cues and not airflow_is_cause:
|
||||
balanced_level = min(desired_level, 3)
|
||||
planned_actions.extend(
|
||||
[
|
||||
{
|
||||
"target": target,
|
||||
"function": "heating",
|
||||
"state": "on",
|
||||
"requested_level": balanced_level,
|
||||
},
|
||||
{
|
||||
"target": target,
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": balanced_level,
|
||||
},
|
||||
]
|
||||
)
|
||||
summaries[-1] = (
|
||||
f"检测到{labels[target]}“{'、'.join(cold_cues + ventilation_cues)}”两个并存的舒适维度,"
|
||||
"不将其判定为语义冲突。"
|
||||
)
|
||||
summaries.append(
|
||||
f"冷感由{labels[target]}加热{balanced_level}档补偿,闷感由"
|
||||
f"{labels[target]}通风{balanced_level}档缓解;生成可审计的组合目标。"
|
||||
)
|
||||
continue
|
||||
|
||||
if is_cold and (airflow_is_cause or ventilation.get("state") == "on"):
|
||||
action, previous_level, next_level = _decrease_action(
|
||||
target, "ventilation", ventilation
|
||||
)
|
||||
planned_actions.append(action)
|
||||
if airflow_is_cause:
|
||||
summaries.append(
|
||||
f"“{labels[target]}吹得冷”明确将{labels[target]}通风指定为致冷来源,不映射为加热请求。"
|
||||
)
|
||||
if previous_level > 1:
|
||||
summaries.append(
|
||||
f"当前{labels[target]}通风为{previous_level}档,先降低到{next_level}档,不同时调整加热。"
|
||||
)
|
||||
elif previous_level == 1:
|
||||
summaries.append(
|
||||
f"当前{labels[target]}通风为1档,冷感更可能由已开启通风造成;"
|
||||
"先关闭通风,不立即开启加热。"
|
||||
)
|
||||
else:
|
||||
summaries.append(
|
||||
f"当前状态显示{labels[target]}通风已关闭,保持关闭;不会因“吹”反向调整加热。"
|
||||
)
|
||||
continue
|
||||
|
||||
if is_cold:
|
||||
current_level = int(heating.get("level") or 0)
|
||||
if heating.get("state") == "on" and current_level > 0:
|
||||
next_level = min(max(current_level + 1, desired_level), 3)
|
||||
planned_actions.append(
|
||||
{
|
||||
"target": target,
|
||||
"function": "heating",
|
||||
"state": "on",
|
||||
"requested_level": next_level,
|
||||
}
|
||||
)
|
||||
summaries.append(
|
||||
f"{labels[target]}通风已关闭,加热已开启{current_level}档但仍感到冷,"
|
||||
f"将加热提升到{next_level}档。"
|
||||
)
|
||||
else:
|
||||
planned_actions.append(
|
||||
{
|
||||
"target": target,
|
||||
"function": "heating",
|
||||
"state": "on",
|
||||
"requested_level": desired_level,
|
||||
}
|
||||
)
|
||||
summaries.append(
|
||||
f"{labels[target]}通风已关闭且加热未开启,冷感仍存在;"
|
||||
f"按{'轻微' if intensity == 'mild' else '当前'}强度开启加热{desired_level}档。"
|
||||
)
|
||||
continue
|
||||
|
||||
if is_hot and heating.get("state") == "on":
|
||||
action, previous_level, next_level = _decrease_action(target, "heating", heating)
|
||||
planned_actions.append(action)
|
||||
summaries.append(
|
||||
f"当前{labels[target]}加热为{previous_level}档,热感优先视为已开启加热的结果;"
|
||||
f"先{'关闭加热' if next_level == 0 else f'降低到{next_level}档'},不立即开启通风。"
|
||||
)
|
||||
continue
|
||||
|
||||
current_level = int(ventilation.get("level") or 0)
|
||||
if ventilation.get("state") == "on" and current_level > 0:
|
||||
next_level = min(max(current_level + 1, desired_level), 3)
|
||||
summaries.append(
|
||||
f"{labels[target]}通风已开启{current_level}档但舒适问题仍存在,提升到{next_level}档。"
|
||||
)
|
||||
else:
|
||||
next_level = desired_level
|
||||
summaries.append(
|
||||
f"{labels[target]}没有已开启的加热致因,按当前强度开启通风{next_level}档。"
|
||||
)
|
||||
planned_actions.append(
|
||||
{
|
||||
"target": target,
|
||||
"function": "ventilation",
|
||||
"state": "on",
|
||||
"requested_level": next_level,
|
||||
}
|
||||
)
|
||||
|
||||
result["actions"] = planned_actions
|
||||
result["ambiguities"] = [
|
||||
item
|
||||
for item in result["ambiguities"]
|
||||
if item.get("field") not in {"capability", "state", "target"}
|
||||
]
|
||||
result["decision_summary"] = summaries
|
||||
result["confidence"] = max(float(result.get("confidence", 0)), 0.9)
|
||||
return result
|
||||
|
||||
|
||||
def parse_local_intent(
|
||||
utterance: str, context: dict[str, Any] | None = None
|
||||
) -> dict[str, Any]:
|
||||
"""Parse common Chinese seat commands without making hardware assumptions."""
|
||||
|
||||
text = re.sub(r"\s+", "", utterance.strip())
|
||||
clauses = [part for part in re.split(r"[,,。;;!!??]", text) if part]
|
||||
actions: list[dict[str, Any]] = []
|
||||
ambiguities: list[dict[str, str]] = []
|
||||
|
||||
for clause in clauses:
|
||||
level = _level_for_clause(clause)
|
||||
for feature_match in FEATURE_PATTERN.finditer(clause):
|
||||
feature_zh = feature_match.group(0)
|
||||
target_zh = _nearest_preceding_target(clause, feature_match.start())
|
||||
state = _state_for_feature(clause, feature_match.end())
|
||||
if target_zh is None:
|
||||
ambiguities.append(
|
||||
{"field": "target", "question": f"“{feature_zh}”需要作用于主驾还是副驾?"}
|
||||
)
|
||||
continue
|
||||
if state is None:
|
||||
ambiguities.append(
|
||||
{"field": "state", "question": f"请确认{target_zh}{feature_zh}是打开还是关闭。"}
|
||||
)
|
||||
continue
|
||||
for target in TARGETS[target_zh]:
|
||||
actions.append(
|
||||
{
|
||||
"target": target,
|
||||
"function": FEATURES[feature_zh],
|
||||
"state": state,
|
||||
"requested_level": level,
|
||||
}
|
||||
)
|
||||
|
||||
merged: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
for action in actions:
|
||||
key = (action["target"], action["function"])
|
||||
previous = merged.get(key)
|
||||
if previous and previous["state"] != action["state"]:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": f"{key[0]}.{key[1]}.state",
|
||||
"question": "同一个座椅功能同时出现打开和关闭指令,请确认最终状态。",
|
||||
}
|
||||
)
|
||||
merged[key] = action
|
||||
actions = list(merged.values())
|
||||
|
||||
parsed = resolve_contextual_intent(
|
||||
{
|
||||
"actions": actions,
|
||||
"ambiguities": ambiguities,
|
||||
"decision_summary": [],
|
||||
"confidence": 0.96 if actions and not ambiguities else 0.45,
|
||||
},
|
||||
utterance,
|
||||
context,
|
||||
)
|
||||
actions = parsed["actions"]
|
||||
ambiguities = parsed["ambiguities"]
|
||||
|
||||
if not actions and not ambiguities:
|
||||
ambiguities.append(
|
||||
{
|
||||
"field": "capability",
|
||||
"question": "当前仅支持主/副驾座椅通风或加热的打开与关闭。",
|
||||
}
|
||||
)
|
||||
|
||||
summary = ["识别为座椅热舒适单域请求。", *parsed["decision_summary"]]
|
||||
labels = {
|
||||
"driver": "主驾",
|
||||
"passenger": "副驾",
|
||||
"ventilation": "通风",
|
||||
"heating": "加热",
|
||||
"on": "打开",
|
||||
"off": "关闭",
|
||||
}
|
||||
summary.extend(
|
||||
f"目标:{labels[action['target']]}座椅{labels[action['function']]}{labels[action['state']]}。"
|
||||
for action in actions
|
||||
)
|
||||
if any(action["requested_level"] is not None for action in actions):
|
||||
summary.append("目标档位已写入功能配置;现有 CAN 矩阵仅支持档位虚拟回读。")
|
||||
|
||||
response = {
|
||||
"actions": actions,
|
||||
"ambiguities": ambiguities,
|
||||
"decision_summary": summary,
|
||||
"confidence": max(parsed["confidence"], 0.96) if actions and not ambiguities else 0.45,
|
||||
}
|
||||
for field in ("detected_domains", "scope"):
|
||||
if field in parsed:
|
||||
response[field] = parsed[field]
|
||||
return response
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
"""Load and encode the normalized partial seat CAN matrix."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from collections.abc import Iterable
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class MatrixError(ValueError):
|
||||
"""Raised when a command cannot be represented by the normalized matrix."""
|
||||
|
||||
|
||||
class SeatCanMatrix:
|
||||
def __init__(self, matrix_path: Path | Iterable[Path]):
|
||||
paths = [matrix_path] if isinstance(matrix_path, Path) else list(matrix_path)
|
||||
if not paths:
|
||||
raise MatrixError("At least one matrix document is required")
|
||||
self.path = paths[0]
|
||||
self.paths = paths
|
||||
documents = [yaml.safe_load(path.read_text(encoding="utf-8")) for path in paths]
|
||||
if len(documents) == 1:
|
||||
self.data = documents[0]
|
||||
else:
|
||||
self.data = {
|
||||
"version": "+".join(str(item.get("version", "unknown")) for item in documents),
|
||||
"source": {"files": [str(path) for path in paths]},
|
||||
"scope": "combined_virtual_seat_capabilities",
|
||||
"hardware_execution_allowed": all(
|
||||
item.get("hardware_execution_allowed") is True for item in documents
|
||||
),
|
||||
"limitations": [
|
||||
limitation
|
||||
for document in documents
|
||||
for limitation in document.get("limitations", [])
|
||||
],
|
||||
"messages": [
|
||||
message
|
||||
for document in documents
|
||||
for message in document.get("messages", [])
|
||||
],
|
||||
}
|
||||
self.messages = {}
|
||||
for item in self.data["messages"]:
|
||||
if item["name"] in self.messages:
|
||||
raise MatrixError(f"Duplicate matrix message: {item['name']}")
|
||||
self.messages[item["name"]] = item
|
||||
|
||||
@property
|
||||
def hardware_execution_allowed(self) -> bool:
|
||||
return self.data.get("hardware_execution_allowed") is True
|
||||
|
||||
def message(self, name: str) -> dict[str, Any]:
|
||||
try:
|
||||
return self.messages[name]
|
||||
except KeyError as exc:
|
||||
raise MatrixError(f"Unknown matrix message: {name}") from exc
|
||||
|
||||
def signal(self, message_name: str, signal_name: str) -> dict[str, Any]:
|
||||
message = self.message(message_name)
|
||||
for signal in message["signals"]:
|
||||
if signal["name"] == signal_name:
|
||||
return signal
|
||||
raise MatrixError(f"Unknown signal {signal_name} in {message_name}")
|
||||
|
||||
def encode(self, message_name: str, values: dict[str, int]) -> dict[str, Any]:
|
||||
message = self.message(message_name)
|
||||
raw_value = 0
|
||||
for signal_name, value in values.items():
|
||||
signal = self.signal(message_name, signal_name)
|
||||
width = int(signal["bit_length"])
|
||||
if not isinstance(value, int) or value < 0 or value >= 1 << width:
|
||||
raise MatrixError(f"Value {value!r} does not fit {signal_name}[{width}]")
|
||||
start_bit = int(signal["start_bit"])
|
||||
mask = ((1 << width) - 1) << start_bit
|
||||
raw_value = (raw_value & ~mask) | (value << start_bit)
|
||||
|
||||
payload_size = int(message["minimum_payload_bytes"])
|
||||
payload = raw_value.to_bytes(payload_size, byteorder="little", signed=False)
|
||||
return {
|
||||
"message": message_name,
|
||||
"frame_id": f"0x{int(message['frame_id']):03X}",
|
||||
"payload_size": payload_size,
|
||||
"data_hex": " ".join(f"{byte:02X}" for byte in payload),
|
||||
"signals": values,
|
||||
"encoding": "lsb0/intel inferred for virtual execution",
|
||||
}
|
||||
|
||||
def decode(self, message_name: str, payload: bytes) -> dict[str, int]:
|
||||
"""Decode every declared little-endian lsb0 signal from a CAN payload."""
|
||||
message = self.message(message_name)
|
||||
minimum = int(message["minimum_payload_bytes"])
|
||||
if len(payload) < minimum:
|
||||
raise MatrixError(
|
||||
f"Payload for {message_name} must contain at least {minimum} bytes"
|
||||
)
|
||||
raw_value = int.from_bytes(payload[:minimum], byteorder="little", signed=False)
|
||||
return {
|
||||
signal["name"]: (
|
||||
raw_value >> int(signal["start_bit"])
|
||||
) & ((1 << int(signal["bit_length"])) - 1)
|
||||
for signal in message["signals"]
|
||||
}
|
||||
|
||||
def decode_by_frame_id(self, frame_id: int, payload: bytes) -> dict[str, int]:
|
||||
matches = [
|
||||
item for item in self.data["messages"] if int(item["frame_id"]) == frame_id
|
||||
]
|
||||
if len(matches) != 1:
|
||||
raise MatrixError(
|
||||
f"Expected exactly one matrix message for frame 0x{frame_id:03X}"
|
||||
)
|
||||
return self.decode(matches[0]["name"], payload)
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
"""Read the supplied XLSX matrix using the standard OOXML structures."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree
|
||||
|
||||
|
||||
MAIN_NS = "http://schemas.openxmlformats.org/spreadsheetml/2006/main"
|
||||
CELL_REFERENCE = re.compile(r"([A-Z]+)([0-9]+)")
|
||||
|
||||
|
||||
def _column_index(reference: str) -> int:
|
||||
match = CELL_REFERENCE.fullmatch(reference)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid XLSX cell reference: {reference}")
|
||||
result = 0
|
||||
for character in match.group(1):
|
||||
result = result * 26 + ord(character) - ord("A") + 1
|
||||
return result - 1
|
||||
|
||||
|
||||
def _shared_strings(archive: zipfile.ZipFile) -> list[str]:
|
||||
root = ElementTree.fromstring(archive.read("xl/sharedStrings.xml"))
|
||||
return [
|
||||
"".join(node.text or "" for node in item.iter(f"{{{MAIN_NS}}}t"))
|
||||
for item in root.findall(f"{{{MAIN_NS}}}si")
|
||||
]
|
||||
|
||||
|
||||
def read_source_signals(path: Path) -> list[dict[str, Any]]:
|
||||
"""Return the 23 signal rows needed for source-to-normalized contract checks."""
|
||||
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
strings = _shared_strings(archive)
|
||||
worksheet = ElementTree.fromstring(archive.read("xl/worksheets/sheet1.xml"))
|
||||
|
||||
rows: list[list[str]] = []
|
||||
for row_node in worksheet.iter(f"{{{MAIN_NS}}}row"):
|
||||
values: dict[int, str] = {}
|
||||
for cell in row_node.findall(f"{{{MAIN_NS}}}c"):
|
||||
value_node = cell.find(f"{{{MAIN_NS}}}v")
|
||||
if value_node is None or value_node.text is None:
|
||||
continue
|
||||
value = value_node.text
|
||||
if cell.get("t") == "s":
|
||||
value = strings[int(value)]
|
||||
values[_column_index(str(cell.get("r")))] = value
|
||||
if values:
|
||||
width = max(values) + 1
|
||||
rows.append([values.get(index, "") for index in range(width)])
|
||||
|
||||
records = []
|
||||
for row in rows[1:]:
|
||||
if len(row) < 6 or not row[0]:
|
||||
continue
|
||||
records.append(
|
||||
{
|
||||
"name": row[0],
|
||||
"description": row[1],
|
||||
"frame_id": int(row[2], 16),
|
||||
"sub_id": None if row[3] in {"", "null"} else int(row[3], 16),
|
||||
"start_bit": int(float(row[4])),
|
||||
"bit_length": int(float(row[5])),
|
||||
}
|
||||
)
|
||||
return records
|
||||
|
||||
Executable
+256
@@ -0,0 +1,256 @@
|
||||
"""Seat-specific semantic validation beyond JSON Schema."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
SUPPORTED_TARGETS = {"driver", "passenger"}
|
||||
SUPPORTED_FUNCTIONS = {"ventilation", "heating"}
|
||||
SUPPORTED_STATES = {"on", "off"}
|
||||
SUPPORTED_MOTION_AXES = {
|
||||
"longitudinal",
|
||||
"cushion_tilt",
|
||||
"backrest_recline",
|
||||
"legrest_height",
|
||||
"legrest_extension",
|
||||
}
|
||||
ANGLE_RANGES = {
|
||||
"backrest_recline": (95, 140),
|
||||
"legrest_height": (95, 160),
|
||||
}
|
||||
|
||||
|
||||
def validate_semantics(
|
||||
parsed: dict[str, Any], function_config: dict[str, Any] | None
|
||||
) -> list[dict[str, str]]:
|
||||
issues: list[dict[str, str]] = []
|
||||
|
||||
detected_domains = set(parsed.get("detected_domains", ["seat"]))
|
||||
if parsed.get("actions") and "seat" not in detected_domains:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "DOMAIN_ACTION_MISMATCH",
|
||||
"path": "detected_domains",
|
||||
"message": "GPT 生成了座椅动作,但 detected_domains 中没有 seat。",
|
||||
}
|
||||
)
|
||||
|
||||
for index, deferred in enumerate(parsed.get("deferred_requests", [])):
|
||||
domain = str(deferred.get("domain"))
|
||||
capability = str(deferred.get("capability"))
|
||||
if domain not in detected_domains or not capability.startswith(f"{domain}."):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "DEFERRED_REQUEST_MISMATCH",
|
||||
"path": f"deferred_requests[{index}]",
|
||||
"message": "暂缓请求的领域、能力名和 detected_domains 不一致。",
|
||||
}
|
||||
)
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "UNAVAILABLE_DOMAIN",
|
||||
"path": f"deferred_requests[{index}]",
|
||||
"message": f"已理解 {capability} 目标,但 {domain} 域当前仅有能力目录,未接入执行器。",
|
||||
}
|
||||
)
|
||||
|
||||
for ambiguity in parsed.get("ambiguities", []):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "AMBIGUOUS_INTENT",
|
||||
"path": ambiguity["field"],
|
||||
"message": ambiguity["question"],
|
||||
}
|
||||
)
|
||||
|
||||
seen: dict[tuple[str, str], str] = {}
|
||||
active_functions: dict[str, set[str]] = {}
|
||||
has_motion = False
|
||||
has_driver_motion = False
|
||||
for index, action in enumerate(parsed.get("actions", [])):
|
||||
path = f"actions[{index}]"
|
||||
if action.get("target") not in SUPPORTED_TARGETS:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_TARGET",
|
||||
"path": f"{path}.target",
|
||||
"message": "当前只支持主驾和前排副驾。",
|
||||
}
|
||||
)
|
||||
if action.get("action_type") == "position":
|
||||
has_motion = True
|
||||
has_driver_motion = has_driver_motion or action.get("target") == "driver"
|
||||
axis = action.get("axis")
|
||||
position = action.get("position_pct")
|
||||
if axis not in SUPPORTED_MOTION_AXES:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_MOTION_AXIS",
|
||||
"path": f"{path}.axis",
|
||||
"message": "请求的座椅位置轴未在当前能力目录中注册。",
|
||||
}
|
||||
)
|
||||
if (
|
||||
not isinstance(position, int)
|
||||
or isinstance(position, bool)
|
||||
or not 0 <= position <= 100
|
||||
):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_POSITION_RANGE",
|
||||
"path": f"{path}.position_pct",
|
||||
"message": "座椅电机绝对目标必须是 0-100 的整数百分比。",
|
||||
}
|
||||
)
|
||||
angle = action.get("angle_deg")
|
||||
if angle is not None:
|
||||
angle_range = ANGLE_RANGES.get(str(axis))
|
||||
if (
|
||||
angle_range is None
|
||||
or not isinstance(angle, (int, float))
|
||||
or isinstance(angle, bool)
|
||||
or not angle_range[0] <= float(angle) <= angle_range[1]
|
||||
):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_ANGLE_RANGE",
|
||||
"path": f"{path}.angle_deg",
|
||||
"message": "座椅夹角超出当前虚拟标定范围。",
|
||||
}
|
||||
)
|
||||
key = (str(action.get("target")), str(axis))
|
||||
previous_position = seen.get(key)
|
||||
if previous_position is not None and previous_position != str(position):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "CONFLICTING_OPERATIONS",
|
||||
"path": path,
|
||||
"message": "同一个座椅电机轴不能在一份配置中包含不同目标位置。",
|
||||
}
|
||||
)
|
||||
seen[key] = str(position)
|
||||
continue
|
||||
if action.get("function") not in SUPPORTED_FUNCTIONS:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_CAPABILITY",
|
||||
"path": f"{path}.function",
|
||||
"message": "当前只支持座椅通风和加热。",
|
||||
}
|
||||
)
|
||||
if action.get("state") not in SUPPORTED_STATES:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_STATE",
|
||||
"path": f"{path}.state",
|
||||
"message": "当前控制信号只支持打开或关闭。",
|
||||
}
|
||||
)
|
||||
requested_level = action.get("requested_level")
|
||||
if requested_level is not None and requested_level not in {1, 2, 3}:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "UNSUPPORTED_LEVEL_RANGE",
|
||||
"path": f"{path}.requested_level",
|
||||
"message": "当前舒适推断和 0x4FA 虚拟回读仅支持 1-3 档。",
|
||||
}
|
||||
)
|
||||
elif requested_level is not None:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "LEVEL_COMMAND_SIMULATED",
|
||||
"path": f"{path}.requested_level",
|
||||
"message": (
|
||||
"目标档位已写入 FunctionConfig 和会话状态;"
|
||||
"0x42A 仅编译开关信号,档位回读仅在 dry-run 中模拟。"
|
||||
),
|
||||
}
|
||||
)
|
||||
key = (str(action.get("target")), str(action.get("function")))
|
||||
previous_state = seen.get(key)
|
||||
if previous_state is not None and previous_state != action.get("state"):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "CONFLICTING_OPERATIONS",
|
||||
"path": path,
|
||||
"message": "同一个座椅功能不能在一份配置中同时打开和关闭。",
|
||||
}
|
||||
)
|
||||
seen[key] = str(action.get("state"))
|
||||
if action.get("state") == "on":
|
||||
active_functions.setdefault(str(action.get("target")), set()).add(
|
||||
str(action.get("function"))
|
||||
)
|
||||
|
||||
if has_motion:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "SYNTHETIC_MOTION_CAN",
|
||||
"path": "platform.reference_vehicle.seat_motion",
|
||||
"message": "座椅位置控制和反馈 CAN 矩阵完全为演示虚构,仅用于验证模型与配置链路。",
|
||||
}
|
||||
)
|
||||
if has_driver_motion:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "DRIVER_MOTION_INTERLOCK_NOT_IMPLEMENTED",
|
||||
"path": "platform.reference_vehicle.vehicle_state",
|
||||
"message": "未接入车速、挡位、乘员和防夹状态,主驾移动只能虚拟执行。",
|
||||
}
|
||||
)
|
||||
|
||||
for target, functions in active_functions.items():
|
||||
if functions == {"ventilation", "heating"}:
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "COMBINED_THERMAL_SIMULATED",
|
||||
"path": f"actions.{target}",
|
||||
"message": (
|
||||
"同一座位的加热与通风组合目标仅用于 dry-run;"
|
||||
"实车前必须确认 ECU 是否允许两功能同时开启。"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
function_config is None
|
||||
and not parsed.get("deferred_requests")
|
||||
and not parsed.get("ambiguities")
|
||||
and not parsed.get("preset_actions")
|
||||
):
|
||||
issues.append(
|
||||
{
|
||||
"severity": "error",
|
||||
"code": "NO_EXECUTABLE_OPERATION",
|
||||
"path": "spec.operations",
|
||||
"message": "没有生成可执行的座椅操作。",
|
||||
}
|
||||
)
|
||||
|
||||
issues.append(
|
||||
{
|
||||
"severity": "warning",
|
||||
"code": "VIRTUAL_EXECUTION_ONLY",
|
||||
"path": "platform.reference_vehicle",
|
||||
"message": "CAN 元数据不完整,当前仅允许虚拟执行,禁止实车下发。",
|
||||
}
|
||||
)
|
||||
return issues
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
# Seat Tests
|
||||
|
||||
`unit/` 覆盖姿态参数和运动学转换,`contract/` 覆盖配置与插件契约,`integration/` 使用模拟 CAN Gateway 覆盖命令、回读、互锁和超时。实车范围需与平台安全标定分别测试,不能只依赖 JSON Schema 的宽范围。
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user