Files
Cognitive_Agent/src/cognitive_agent/application/native_can_runtime.py
T
2026-08-24 14:15:53 +08:00

1734 lines
71 KiB
Python

"""Trusted compilation and execution support for CAN_DEV vehicle controls."""
from __future__ import annotations
import hashlib
import json
import os
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Callable
import yaml
from cognitive_agent.application.native_can_feedback import (
decode_feedback,
feedback_matches,
)
from cognitive_agent.infrastructure.can.gateway import CanBus, CanFrame
def _now() -> str:
return datetime.now(UTC).isoformat().replace("+00:00", "Z")
def _set_bits(payload: bytearray, start: int, length: int, value: int) -> None:
if length <= 0 or start < 0 or start + length > len(payload) * 8:
raise ValueError("invalid bit range")
if value < 0 or value >= (1 << length):
raise ValueError(f"value {value} does not fit in {length} bits")
for offset in range(length):
bit = start + offset
mask = 1 << (bit % 8)
if value & (1 << offset):
payload[bit // 8] |= mask
else:
payload[bit // 8] &= ~mask
@dataclass(frozen=True)
class NativeFrameSpec:
frame_id: int
payload: bytes
is_fd: bool = False
bitrate_switch: bool = False
repeat_count: int = 1
period_ms: int = 0
feedback_ids: tuple[int, ...] = ()
expected_feedback: str = ""
special_handler: str = ""
special_parameters: dict[str, Any] | None = None
class NativeCanCompileError(ValueError):
"""Raised when a request is outside the CAN_DEV verified vocabulary."""
DRIVER_SEAT_CAPABILITIES = frozenset(
{
"seat.set_driver_ventilation_native",
"seat.set_driver_heating_native",
"seat.set_driver_massage_native",
"seat.set_driver_axis_native",
"seat.set_driver_position_native",
"seat.recall_driver_memory_native",
}
)
DRIVER_SEAT_TARGETS = frozenset({"driver", "front_left"})
UNSUPPORTED_CH1_DOOR_LOCK_CAPABILITIES = frozenset(
{"door.set_vehicle_lock"}
)
def _unsupported_ch1_door_lock_message(capability: str) -> str:
if capability == "door.set_vehicle_lock":
return (
"door.set_vehicle_lock is unsupported for physical execution on the "
"current CH1; the 0x496 smoke request caused no 0x407 vehicle-lock or "
"four-door transition, so CH1 door-lock support is observational only"
)
return f"{capability} is unsupported for physical execution on the current CH1"
class NativeCanRuntime:
"""Compile named capabilities into byte-exact CAN_DEV frames."""
compiler_version = "1.0.0"
mapping_version = "can-dev-verified-body-can-v5"
def __init__(self, repository_root: Path):
self.root = repository_root
calibration_path = (
repository_root
/ "platforms"
/ "t2g_cabin_bench"
/ "calibration"
/ "body_can.yaml"
)
self.calibration_path = calibration_path
self.calibration = yaml.safe_load(calibration_path.read_text(encoding="utf-8"))
self.capabilities: dict[str, dict[str, Any]] = {}
catalog_paths = [
*(repository_root / "domains").glob("*/config/capabilities.yaml"),
*(repository_root / "config" / "native_can_capabilities").glob("*.yaml"),
]
for path in sorted(catalog_paths):
catalog = yaml.safe_load(path.read_text(encoding="utf-8"))
for capability in catalog.get("spec", {}).get("capabilities", []):
self.capabilities[capability["name"]] = capability
@property
def supported_capabilities(self) -> frozenset[str]:
return frozenset(
{
"window.set_motion",
"window.set_position",
"climate.set_power",
"climate.set_ac",
"climate.set_fan_level",
"climate.set_outlet_mode",
"climate.set_temperature",
"climate.set_control_mode",
"climate.set_cycle_mode",
"door.set_rear_lock",
"outlet.set_position_percent",
"outlet.set_calibrated_position",
*DRIVER_SEAT_CAPABILITIES,
"interior_light.set_indoor_power",
"interior_light.set_reading_power",
"ambient_light.set_power",
"ambient_light.set_color_index",
"ambient_light.set_brightness_level",
"roof.set_shade_motion",
"roof.set_shade_position",
}
)
def supports(self, action: dict[str, Any]) -> bool:
capability = str(action.get("capability"))
if capability not in self.supported_capabilities:
return False
if capability in DRIVER_SEAT_CAPABILITIES:
target = str(action.get("target", action.get("target_scope", "")))
return target in DRIVER_SEAT_TARGETS
if capability == "interior_light.set_reading_power":
target = str(action.get("target", action.get("target_scope", "")))
return target == "all"
return True
def build_function_config(
self,
domain: str,
actions: list[dict[str, Any]],
request_id: str,
provider: str,
) -> dict[str, Any]:
operations = []
for index, action in enumerate(actions, start=1):
capability_name = str(action["capability"])
if capability_name not in self.supported_capabilities:
if capability_name in UNSUPPORTED_CH1_DOOR_LOCK_CAPABILITIES:
raise NativeCanCompileError(
_unsupported_ch1_door_lock_message(capability_name)
)
raise NativeCanCompileError(
f"CAN_DEV does not implement {capability_name}"
)
capability = self.capabilities[capability_name]
target = str(action.get("target", action.get("target_scope", "vehicle")))
if target not in capability.get("target_scopes", []):
raise NativeCanCompileError(
f"{capability_name} does not support target {target}"
)
suffix = capability_name.split(".", 1)[1]
operation_id = f"{target}_{suffix}_{index}"[:63]
operations.append(
{
"id": operation_id,
"capability": capability_name,
"target": {
"device": capability["target_devices"][0],
"zone": target,
},
"parameters": dict(action.get("parameters", {})),
"parse_source": str(
action.get("parse_source", "local_rule")
),
"timeout_ms": (
20_000
if capability_name
in {
"window.set_position",
"roof.set_shade_position",
"seat.set_driver_position_native",
"seat.recall_driver_memory_native",
}
else 4_000
),
"failure_policy": "abort",
}
)
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "FunctionConfig",
"metadata": {
"id": f"native-{domain}-{request_id}",
"name": f"{domain.replace('_', ' ').title()} native CAN function",
"version": "1.0.0",
"request_id": request_id,
"generated_at": _now(),
"labels": {
"provider": provider,
"execution_scope": "vehicle_can_channel_1",
"source_branch": "CAN_DEV",
},
},
"spec": {
"domain": domain,
"operations": operations,
"assumptions": [
"Only named, range-checked CAN_DEV capabilities may reach the body CAN channel.",
"The body CAN channel uses ISO CAN FD at 500 kbit/s arbitration and 2 Mbit/s data rate.",
],
},
}
def compile(self, function_config: dict[str, Any]) -> dict[str, Any]:
commands: list[dict[str, Any]] = []
for operation_index, operation in enumerate(
function_config["spec"]["operations"], start=1
):
specs = self._frame_specs(operation)
for frame_index, spec in enumerate(specs, start=1):
signals: dict[str, int | bool | str] = {
"frame_id": f"0x{spec.frame_id:03X}",
"data_hex": " ".join(f"{byte:02X}" for byte in spec.payload),
"is_fd": spec.is_fd,
"bitrate_switch": spec.bitrate_switch,
"feedback_ids": ",".join(
f"0x{frame_id:03X}" for frame_id in spec.feedback_ids
),
"expected_feedback": spec.expected_feedback,
"debug_command": self.operation_debug_command(operation),
}
if spec.special_handler:
signals["special_handler"] = spec.special_handler
signals["special_parameters"] = json.dumps(
spec.special_parameters or {},
ensure_ascii=False,
sort_keys=True,
)
command: dict[str, Any] = {
"id": f"native_{operation_index}_{frame_index}_control",
"operation_ref": operation["id"],
"bus": "body_can",
"message": f"Native_0x{spec.frame_id:03X}",
"signals": signals,
"send_offset_ms": (operation_index - 1) * 20,
}
if spec.repeat_count > 1:
command["repeat"] = {
"count": spec.repeat_count,
"period_ms": spec.period_ms,
}
commands.append(command)
canonical = json.dumps(
function_config, ensure_ascii=False, sort_keys=True
).encode("utf-8")
metadata = function_config["metadata"]
domain = function_config["spec"]["domain"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "CommandPlan",
"metadata": {
"id": f"plan-native-{domain}-{metadata['request_id']}",
"name": f"{domain.replace('_', ' ').title()} native CAN command plan",
"version": "1.0.0",
"request_id": metadata["request_id"],
"labels": {
"execution_scope": "vehicle_can_channel_1",
"source_branch": "CAN_DEV",
},
},
"spec": {
"domain": domain,
"platform": "t2g_cabin_bench",
"source": {
"config_id": metadata["id"],
"config_version": metadata["version"],
"config_sha256": hashlib.sha256(canonical).hexdigest(),
"compiler_version": self.compiler_version,
"mapping_version": self.mapping_version,
},
"commands": commands,
},
}
def execute_virtual(
self,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
started_at = _now()
operations = {
operation["id"]: operation
for operation in function_config["spec"]["operations"]
}
results = []
for command in command_plan["spec"]["commands"]:
operation = operations[command["operation_ref"]]
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "succeeded",
"message": "已完成 CAN_DEV 帧编译;dry-run 未打开物理 CAN。",
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": self.command_frame_public(command),
"source_branch": "CAN_DEV",
"simulated": True,
},
}
)
return self._report(function_config, results, started_at, "dry_run")
@staticmethod
def command_frame_public(command: dict[str, Any]) -> dict[str, Any]:
signals = command["signals"]
public = {
"frame_id": signals["frame_id"],
"payload_size": len(bytes.fromhex(str(signals["data_hex"]))),
"data_hex": signals["data_hex"],
"channel": 1,
"frame_format": "can_fd" if signals["is_fd"] else "classic_can",
"bitrate_switch": bool(signals["bitrate_switch"]),
"repeat": command.get("repeat", {"count": 1, "period_ms": 0}),
}
if signals.get("special_handler") == "driver_seat_axis":
special = json.loads(str(signals["special_parameters"]))
public["safety"] = {
"forced_stop": True,
"stop_data_hex": special["stop_data_hex"],
"stop_repeat_count": special["stop_repeat_count"],
"stop_period_ms": special["stop_period_ms"],
}
elif signals.get("special_handler") == "driver_seat_memory_recall":
special = json.loads(str(signals["special_parameters"]))
public["sequence"] = {
"frame_count": len(special["frame_sequence"]),
"period_ms": special["period_ms"],
"data_hex": list(special["frame_sequence"]),
"requires_fresh_initialized_0x494": True,
}
return public
@staticmethod
def operation_debug_command(operation: dict[str, Any]) -> str:
"""Return the trusted canonical command represented by one operation."""
capability = str(operation["capability"])
parameters = operation.get("parameters", {})
target = str(operation.get("target", {}).get("zone", "vehicle")).replace(
"_", "-"
)
if capability == "window.set_motion":
return f"window {target} {parameters['motion']}"
if capability == "window.set_position":
return f"window {target} position {parameters['position_pct']}"
if capability == "climate.set_power":
return f"ac {parameters['state']}"
if capability == "climate.set_ac":
return f"ac compressor {parameters['state']}"
if capability == "climate.set_fan_level":
return f"ac fan {parameters['level']}"
if capability == "climate.set_outlet_mode":
return f"ac mode {str(parameters['mode']).replace('_', '-')}"
if capability == "climate.set_temperature":
return f"ac temp {target} {parameters['temperature_c']}"
if capability == "climate.set_control_mode":
return f"ac control {parameters['mode']}"
if capability == "climate.set_cycle_mode":
return f"ac cycle {parameters['mode']}"
if capability == "door.set_rear_lock":
return f"door rear {parameters['state']}"
if capability == "outlet.set_position_percent":
return (
f"outlet {target} percent {parameters['horizontal_pct']} "
f"{parameters['vertical_pct']}"
)
if capability == "outlet.set_calibrated_position":
return (
f"outlet {target} position {parameters['horizontal_value']} "
f"{parameters['vertical_value']}"
)
if capability in {
"seat.set_driver_heating_native",
"seat.set_driver_ventilation_native",
}:
feature = (
"heat"
if capability == "seat.set_driver_heating_native"
else "vent"
)
setting = "off"
if parameters["state"] == "on":
setting = {1: "high", 2: "low"}[int(parameters.get("level", 2))]
return f"seat driver {feature} {setting}"
if capability == "seat.set_driver_massage_native":
action = str(parameters["action"])
if action == "off":
return "seat driver massage off"
if action == "strength":
strength = {1: "weak", 2: "medium", 3: "strong"}[
int(parameters["strength"])
]
return f"seat driver massage strength {strength}"
return f"seat driver massage mode {parameters['mode']}"
if capability == "seat.set_driver_axis_native":
if parameters["direction"] == "stop":
return f"seat driver {parameters['axis']} stop"
return (
f"seat driver {parameters['axis']} {parameters['direction']} "
f"{parameters.get('duration_ms', 500)}"
)
if capability == "seat.set_driver_position_native":
return f"seat driver position {parameters['slot']}"
if capability == "seat.recall_driver_memory_native":
return f"seat driver memory recall {parameters['slot']}"
if capability == "interior_light.set_indoor_power":
return f"light indoor {parameters['state']}"
if capability == "interior_light.set_reading_power":
return f"light reading all {parameters['state']}"
if capability == "ambient_light.set_power":
return f"light ambient {parameters['state']}"
if capability == "ambient_light.set_color_index":
return f"light ambient color {parameters['color_index']}"
if capability == "ambient_light.set_brightness_level":
return f"light ambient brightness {parameters['brightness_level']}"
if capability == "roof.set_shade_motion":
return f"shade {parameters['motion']}"
if capability == "roof.set_shade_position":
return f"shade position {parameters['position_pct']}"
return capability
@staticmethod
def _report(
function_config: dict[str, Any],
results: list[dict[str, Any]],
started_at: str,
mode: str,
) -> dict[str, Any]:
succeeded = all(item["status"] == "succeeded" for item in results)
request_id = function_config["metadata"]["request_id"]
domain = function_config["spec"]["domain"]
return {
"apiVersion": "cognitive-agent/v1alpha1",
"kind": "ExecutionReport",
"metadata": {
"id": f"report-native-{domain}-{request_id}",
"name": f"{domain.replace('_', ' ').title()} native CAN execution report",
"version": "1.0.0",
"request_id": request_id,
"labels": {
"simulation": "false" if mode == "vehicle" else "true",
"source_branch": "CAN_DEV",
},
},
"spec": {
"mode": mode,
"status": "succeeded" if succeeded else "failed",
"started_at": started_at,
"finished_at": _now(),
"results": results,
},
}
def _frame_specs(self, operation: dict[str, Any]) -> list[NativeFrameSpec]:
capability = str(operation["capability"])
if capability in UNSUPPORTED_CH1_DOOR_LOCK_CAPABILITIES:
raise NativeCanCompileError(
_unsupported_ch1_door_lock_message(capability)
)
target = operation["target"]["zone"]
parameters = operation["parameters"]
if capability == "door.set_rear_lock":
self._reject_unexpected_parameters(parameters, {"state"})
state = self._enum(parameters, "state", {"lock": 1, "unlock": 2})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 14, 2, state)
return [
self._classic(
0x32B,
payload,
1,
0,
(0x407,),
f"rear_lock:{parameters['state']}",
)
]
if capability in {"climate.set_power", "climate.set_ac"}:
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
_set_bits(payload, 28, 2, state)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_power:{parameters['state']}")]
if capability == "climate.set_fan_level":
level = self._integer(parameters, "level", 1, 7)
payload = bytearray(8)
_set_bits(payload, 4, 4, level)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_fan:{level}")]
if capability == "climate.set_outlet_mode":
mode = self._enum(
parameters,
"mode",
{
"face": 1,
"face_feet": 2,
"feet": 3,
"feet_defrost": 4,
"defrost": 5,
"face_feet_defrost": 6,
"face_defrost": 7,
},
)
payload = bytearray(8)
_set_bits(payload, 0, 4, mode)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_mode:{mode}")]
if capability == "climate.set_control_mode":
mode = self._enum(parameters, "mode", {"manual": 1, "auto": 2})
payload = bytearray(8)
_set_bits(payload, 16, 2, mode)
return [self._classic(0x1DE, payload, 3, 100, (0x404,), f"climate_control:{mode}")]
if capability == "climate.set_cycle_mode":
self._reject_unexpected_parameters(parameters, {"mode"})
mode = self._enum(parameters, "mode", {"internal": 1, "external": 2})
payload = bytearray(8)
_set_bits(payload, 19, 2, mode)
return [
self._classic(
0x1DE,
payload,
3,
100,
(0x404,),
f"climate_cycle:{parameters['mode']}",
)
]
if capability == "climate.set_temperature":
temperature = self._integer(parameters, "temperature_c", 18, 32)
starts = {
"driver": (32,),
"passenger": (40,),
"rear": (48,),
"rear_row": (48,),
"rear_left": (48,),
"rear_right": (48,),
"vehicle": (32, 40),
"front_row": (32, 40),
}
if target not in starts:
raise NativeCanCompileError(
f"climate.set_temperature does not support target {target}"
)
specs = []
for start in starts[target]:
payload = bytearray(8)
_set_bits(payload, start, 8, temperature)
label = "driver" if start == 32 else "passenger" if start == 40 else "rear"
specs.append(
self._classic(
0x1DE,
payload,
1 if start == 48 else 3,
100,
(0x404,),
f"climate_temperature:{label}:{temperature}",
)
)
return specs
if capability == "window.set_motion":
motion = self._enum(parameters, "motion", {"open": 1, "close": 2, "stop": 3})
payload = self._window_payload(target, motion)
return [self._fd(0x431, payload, 3, 100, (0x388,), f"window_motion:{target}:{parameters['motion']}")]
if capability == "window.set_position":
position = self._integer(parameters, "position_pct", 0, 100)
if target not in {"front_left", "rear_left", "front_right", "rear_right"}:
raise NativeCanCompileError("absolute window position requires one window")
return [
NativeFrameSpec(
frame_id=0x431,
payload=self._window_payload(target, 3),
is_fd=True,
bitrate_switch=True,
feedback_ids=(0x388,),
expected_feedback=f"window_position:{target}:{position}",
special_handler="window_position",
special_parameters={"target": target, "position_pct": position},
)
]
if capability in {
"seat.set_driver_heating_native",
"seat.set_driver_ventilation_native",
}:
self._require_driver_seat_target(target)
return [self._driver_seat_thermal_frame(capability, parameters)]
if capability == "seat.set_driver_massage_native":
self._require_driver_seat_target(target)
return [self._driver_seat_massage_frame(parameters)]
if capability == "seat.set_driver_axis_native":
self._require_driver_seat_target(target)
return [self._driver_seat_axis_frame(parameters)]
if capability in {
"seat.set_driver_position_native",
"seat.recall_driver_memory_native",
}:
self._require_driver_seat_target(target)
return [self._driver_seat_memory_frame(parameters)]
if capability in {"outlet.set_position_percent", "outlet.set_calibrated_position"}:
return [self._outlet_frame(capability, target, parameters)]
if capability == "interior_light.set_indoor_power":
return [self._all_reading_light_frame(parameters, "indoor_light")]
if capability == "interior_light.set_reading_power":
if target != "all":
raise NativeCanCompileError(
"only the all-reading-lights target is verified on CH1"
)
return [self._all_reading_light_frame(parameters, "indoor_light")]
if capability == "ambient_light.set_power":
self._reject_unexpected_parameters(parameters, {"state"})
state = str(parameters.get("state"))
self._enum(parameters, "state", {"off": 0, "on": 1})
payload = bytes(
(0x00, 0x36 if state == "on" else 0x31, 0x00, 0x75, 0x00, 0x80, 0x00, 0x00)
)
return [self._fd(0x3FC, payload, 3, 100, (0x42E,), f"ambient_power:{state}")]
if capability == "ambient_light.set_color_index":
self._reject_unexpected_parameters(parameters, {"color_index"})
color = self._integer(parameters, "color_index", 1, 127)
payload = bytes((color, 0x30, 0x00, 0x75, 0x00, 0x00, 0x00, 0x00))
return [self._fd(0x3FC, payload, 3, 100, (0x42E,), f"ambient_color:{color}")]
if capability == "ambient_light.set_brightness_level":
self._reject_unexpected_parameters(parameters, {"brightness_level"})
level = self._integer(parameters, "brightness_level", 1, 5)
payload = bytes((0x00, 0x31 + level, 0x00, 0x75, 0x00, 0x00, 0x00, 0x00))
return [self._fd(0x3FC, payload, 3, 100, (0x42E,), f"ambient_brightness:{level}")]
if capability == "roof.set_shade_motion":
motion = self._enum(parameters, "motion", {"open": 1, "close": 2, "half": 3, "stop": 4})
return [self._shade_preset_frame(motion)]
if capability == "roof.set_shade_position":
position = self._integer(parameters, "position_pct", 0, 100)
presets = {0: 2, 50: 3, 100: 1}
if position in presets:
return [self._shade_preset_frame(presets[position])]
return [self._shade_position_frame(position)]
raise NativeCanCompileError(f"CAN_DEV does not implement {capability}")
@staticmethod
def _classic(
frame_id: int,
payload: bytes | bytearray,
repeat: int = 1,
period_ms: int = 0,
feedback_ids: tuple[int, ...] = (),
expected: str = "",
) -> NativeFrameSpec:
return NativeFrameSpec(
frame_id,
bytes(payload),
repeat_count=repeat,
period_ms=period_ms,
feedback_ids=feedback_ids,
expected_feedback=expected,
)
@staticmethod
def _fd(
frame_id: int,
payload: bytes | bytearray,
repeat: int = 1,
period_ms: int = 0,
feedback_ids: tuple[int, ...] = (),
expected: str = "",
) -> NativeFrameSpec:
return NativeFrameSpec(
frame_id,
bytes(payload),
is_fd=True,
bitrate_switch=True,
repeat_count=repeat,
period_ms=period_ms,
feedback_ids=feedback_ids,
expected_feedback=expected,
)
@staticmethod
def _enum(parameters: dict[str, Any], name: str, values: dict[str, int]) -> int:
value = str(parameters.get(name))
try:
return values[value]
except KeyError as exc:
raise NativeCanCompileError(
f"{name} must be one of {', '.join(values)}"
) from exc
@staticmethod
def _integer(
parameters: dict[str, Any], name: str, minimum: int, maximum: int
) -> int:
value = parameters.get(name)
if isinstance(value, bool) or not isinstance(value, (int, float)):
raise NativeCanCompileError(f"{name} must be an integer")
integer = int(value)
if integer != value or not minimum <= integer <= maximum:
raise NativeCanCompileError(
f"{name} must be between {minimum} and {maximum}"
)
return integer
@staticmethod
def _window_payload(target: str, motion: int) -> bytes:
masks = {
"front_left": 0b0001,
"rear_left": 0b0010,
"front_right": 0b0100,
"rear_right": 0b1000,
"front": 0b0101,
"rear": 0b1010,
"all": 0b1111,
}
try:
mask = masks[target]
except KeyError as exc:
raise NativeCanCompileError(f"unsupported window target {target}") from exc
starts = (48, 56, 51, 59)
payload = bytearray(8)
for index, start in enumerate(starts):
if mask & (1 << index):
_set_bits(payload, start, 3, motion)
return bytes(payload)
def _outlet_frame(
self, capability: str, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
ranges = self.calibration["air_outlet"]["ranges"]
if target not in ranges:
raise NativeCanCompileError(f"unsupported outlet target {target}")
if capability == "outlet.set_position_percent":
horizontal_pct = self._integer(parameters, "horizontal_pct", 0, 100)
vertical_pct = self._integer(parameters, "vertical_pct", 0, 100)
horizontal_range = ranges[target]["horizontal"]
vertical_range = ranges[target]["vertical"]
horizontal = horizontal_range[0] + (
(horizontal_range[1] - horizontal_range[0]) * horizontal_pct + 50
) // 100
vertical = vertical_range[0] + (
(vertical_range[1] - vertical_range[0]) * vertical_pct + 50
) // 100
else:
horizontal = self._integer(parameters, "horizontal_value", -128, 126)
vertical = self._integer(parameters, "vertical_value", -128, 126)
horizontal_range = ranges[target]["horizontal"]
vertical_range = ranges[target]["vertical"]
if not horizontal_range[0] <= horizontal <= horizontal_range[1]:
raise NativeCanCompileError("horizontal_value exceeds the calibrated range")
if not vertical_range[0] <= vertical <= vertical_range[1]:
raise NativeCanCompileError("vertical_value exceeds the calibrated range")
side, position = target.split("_", 1)
sub_id = 0x02 if side == "driver" else 0x03
starts = {
"left": (24, 8),
"right": (32, 16),
}
horizontal_start, vertical_start = starts[position]
payload = bytearray((sub_id, 0xFF, 0xFF, 0xFF, 0xFF, 0, 0, 0))
if sub_id == 0x02:
payload[6] = 0xFF
payload[7] = 0xFF
_set_bits(payload, horizontal_start, 8, horizontal + 128)
_set_bits(payload, vertical_start, 8, vertical + 128)
return self._fd(
0x4EF,
payload,
3,
100,
(0x2F5, 0x37B),
f"outlet:{target}:{horizontal}:{vertical}",
)
@staticmethod
def _require_driver_seat_target(target: str) -> None:
if target not in DRIVER_SEAT_TARGETS:
raise NativeCanCompileError(
"CH1 driver-seat controls only allow driver/front_left; "
f"refusing target {target}"
)
@staticmethod
def _reject_unexpected_parameters(
parameters: dict[str, Any], allowed: set[str]
) -> None:
unexpected = set(parameters) - allowed
if unexpected:
raise NativeCanCompileError(
f"unsupported parameter(s): {', '.join(sorted(unexpected))}"
)
def _driver_seat_thermal_frame(
self, capability: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
self._reject_unexpected_parameters(parameters, {"state", "level"})
state = str(parameters.get("state"))
if state not in {"on", "off"}:
raise NativeCanCompileError("state must be on or off")
if state == "off":
if parameters.get("level") is not None:
raise NativeCanCompileError("level must be omitted when state is off")
control_value = 3
feedback_value = 1
else:
level = parameters.get("level", 2)
parameters_with_default = {**parameters, "level": level}
control_value = self._integer(parameters_with_default, "level", 1, 2)
feedback_value = 3 if control_value == 1 else 2
feature = (
"heating"
if capability == "seat.set_driver_heating_native"
else "ventilation"
)
payload = bytearray(8)
payload[1] = (
control_value << 4 if feature == "heating" else control_value
)
return self._fd(
0x431,
payload,
3,
100,
(0x4FA,),
f"driver_seat_{feature}:{feedback_value}",
)
def _driver_seat_massage_frame(
self, parameters: dict[str, Any]
) -> NativeFrameSpec:
action = str(parameters.get("action"))
payload = bytearray(8)
payload[0] = 0x12
if action == "off":
self._reject_unexpected_parameters(parameters, {"action"})
payload[3] = 1
expected = "driver_seat_massage_mode:1"
elif action == "strength":
self._reject_unexpected_parameters(parameters, {"action", "strength"})
strength = self._integer(parameters, "strength", 1, 3)
payload[1] = strength
expected = f"driver_seat_massage_strength:{8 + strength}"
elif action == "mode":
self._reject_unexpected_parameters(parameters, {"action", "mode"})
mode = self._enum(
parameters,
"mode",
{
"wave": 2,
"pulse": 3,
"stretch": 4,
"slack": 5,
"relax": 6,
},
)
payload[3] = mode
expected = f"driver_seat_massage_mode:{mode}"
else:
raise NativeCanCompileError("action must be off, strength, or mode")
return self._fd(0x4C1, payload, 3, 100, (0x4FA,), expected)
def _driver_seat_axis_frame(
self, parameters: dict[str, Any]
) -> NativeFrameSpec:
self._reject_unexpected_parameters(
parameters, {"axis", "direction", "duration_ms"}
)
axis = str(parameters.get("axis"))
starts = {
"horizontal": 8,
"height": 10,
"backrest": 12,
"basin": 14,
}
if axis not in starts:
raise NativeCanCompileError(
"axis must be horizontal, height, backrest, or basin"
)
direction = str(parameters.get("direction"))
allowed_directions = (
{"forward": 1, "backward": 2, "stop": 0}
if axis in {"horizontal", "backrest"}
else {"up": 1, "down": 2, "stop": 0}
)
if direction not in allowed_directions:
raise NativeCanCompileError(
f"direction {direction} is not valid for {axis}"
)
duration_parameters = {
**parameters,
"duration_ms": parameters.get("duration_ms", 500),
}
duration_ms = self._integer(
duration_parameters, "duration_ms", 100, 2000
)
stop_payload = bytearray(8)
stop_payload[0] = 0x05
payload = bytearray(stop_payload)
_set_bits(payload, starts[axis], 2, allowed_directions[direction])
expected = f"driver_seat_axis:{axis}:{direction}"
if direction == "stop":
return self._fd(
0x4C1, stop_payload, 3, 100, (0x494,), expected
)
repeat_count = (duration_ms + 99) // 100
return NativeFrameSpec(
frame_id=0x4C1,
payload=bytes(payload),
is_fd=True,
bitrate_switch=True,
repeat_count=repeat_count,
period_ms=100,
feedback_ids=(0x494,),
expected_feedback=expected,
special_handler="driver_seat_axis",
special_parameters={
"duration_ms": duration_ms,
"final_action_hold_ms": 100,
"stop_data_hex": " ".join(
f"{value:02X}" for value in stop_payload
),
"stop_repeat_count": 3,
"stop_period_ms": 100,
},
)
def _driver_seat_memory_frame(
self, parameters: dict[str, Any]
) -> NativeFrameSpec:
"""Compile one learned PAD memory slot without inventing payload bytes."""
self._reject_unexpected_parameters(parameters, {"slot"})
slot = self._integer(parameters, "slot", 1, 3)
configured = os.getenv(
"COGNITIVE_AGENT_DRIVER_SEAT_MEMORY_PATH", ""
).strip()
path = (
Path(configured).expanduser()
if configured
else Path("/home/orin/project511/project-code/canfd/driver_seat_memory.json")
)
try:
document = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError) as exc:
raise NativeCanCompileError(
f"cannot load learned driver-seat memory file {path}: {exc}"
) from exc
if document.get("version") != 1 or not isinstance(
document.get("positions"), dict
):
raise NativeCanCompileError(
"driver-seat memory file must use version 1 with positions"
)
learned = document["positions"].get(str(slot))
if not isinstance(learned, dict):
raise NativeCanCompileError(
f"driver-seat memory slot {slot} has not been learned from PAD"
)
def learned_payload(name: str, sub_id: int) -> bytes:
raw = learned.get(name)
try:
payload = bytes.fromhex(str(raw))
except ValueError as exc:
raise NativeCanCompileError(
f"driver-seat memory slot {slot} has invalid {name}"
) from exc
if len(payload) != 8 or payload[0] != sub_id:
raise NativeCanCompileError(
f"driver-seat memory slot {slot} {name} must be an 8-byte "
f"0x{sub_id:02X} learned frame"
)
return payload
payload_15 = learned_payload("payload_15", 0x15)
payload_16 = learned_payload("payload_16", 0x16)
sequence = [
value.hex(" ").upper()
for _ in range(10)
for value in (payload_16, payload_15)
]
return NativeFrameSpec(
frame_id=0x4C1,
payload=payload_16,
is_fd=True,
bitrate_switch=True,
feedback_ids=(0x494,),
expected_feedback=f"driver_seat_memory_status:{slot}",
special_handler="driver_seat_memory_recall",
special_parameters={
"slot": slot,
"fresh_status_timeout_ms": 2_500,
"post_status_timeout_ms": 1_200,
"period_ms": 50,
"frame_sequence": sequence,
},
)
def _all_reading_light_frame(
self, parameters: dict[str, Any], feedback_family: str
) -> NativeFrameSpec:
self._reject_unexpected_parameters(parameters, {"state"})
state = self._enum(parameters, "state", {"off": 1, "on": 2})
payload = bytearray((0, 0, 3, 0xFF, 0xFF, 0, 0, 0))
_set_bits(payload, 50, 2, state)
return self._fd(
0x4F5,
payload,
3,
100,
(0x42E,),
f"{feedback_family}:{parameters['state']}",
)
@staticmethod
def _shade_payload(motion: int) -> bytes:
payload = bytearray(8)
_set_bits(payload, 4, 4, motion)
return bytes(payload)
def _shade_preset_frame(self, motion: int) -> NativeFrameSpec:
target = {1: 100, 2: 0, 3: 50}.get(motion)
expected = "shade_stopped" if motion == 4 else f"shade_position:{target}"
return NativeFrameSpec(
frame_id=0x431,
payload=self._shade_payload(motion),
feedback_ids=(0x41A,),
expected_feedback=expected,
special_handler="shade_preset",
special_parameters={
"motion": motion,
"target_position_pct": target,
"motion_timeout_ms": 15_000,
"tolerance_pct": 2 if target in {0, 100} else 3,
},
)
def _shade_position_frame(self, position: int) -> NativeFrameSpec:
return NativeFrameSpec(
frame_id=0x431,
payload=self._shade_payload(4),
feedback_ids=(0x41A,),
expected_feedback=f"shade_position:{position}",
special_handler="shade_position",
special_parameters={
"position_pct": position,
"fresh_feedback_timeout_ms": 2_500,
"motion_timeout_ms": 15_000,
"tolerance_pct": 2,
"open_data_hex": "10 00 00 00 00 00 00 00",
"close_data_hex": "20 00 00 00 00 00 00 00",
"stop_data_hex": "40 00 00 00 00 00 00 00",
},
)
class NativeCanExecutor:
"""Submit compiled vehicle frames and capture post-command feedback."""
def __init__(
self,
bus_factory: Callable[[], CanBus],
*,
feedback_timeout_ms: int = 1_200,
feedback_required: bool = True,
):
self.bus_factory = bus_factory
self.feedback_timeout_ms = feedback_timeout_ms
self.feedback_required = feedback_required
def execute(
self,
runtime: NativeCanRuntime,
command_plan: dict[str, Any],
function_config: dict[str, Any],
) -> dict[str, Any]:
started_at = _now()
operations = {
operation["id"]: operation
for operation in function_config["spec"]["operations"]
}
results: list[dict[str, Any]] = []
bus = self.bus_factory()
try:
for command in command_plan["spec"]["commands"]:
operation = operations[command["operation_ref"]]
publication_start = len(
getattr(bus, "published_messages", ())
)
try:
set_debug_command = getattr(bus, "set_debug_command", None)
if callable(set_debug_command):
set_debug_command(command["signals"].get("debug_command", ""))
raw_feedback_ids = str(
command["signals"].get("feedback_ids", "")
).strip()
expected_feedback = str(
command["signals"].get("expected_feedback", "")
).strip()
if not raw_feedback_ids or not expected_feedback:
raise RuntimeError(
"physical CH1 execution requires an authoritative "
"feedback ID and a target-value match contract"
)
special_handler = command["signals"].get("special_handler")
feedback_required = self.feedback_required
if not feedback_required and special_handler in {
None,
"shade_preset",
}:
# Wireless MQTT uses the state topic as an asynchronous
# source of truth. Fixed frames and self-terminating
# shade presets must not hold the HTTP request open
# while waiting for the same state stream synchronously.
sent_frames = self._send_command(bus, command)
feedback = None
elif special_handler == "window_position":
sent_frames, feedback = self._execute_window_position(bus, command)
elif special_handler == "driver_seat_axis":
sent_frames, feedback = self._execute_driver_seat_axis(
bus,
command,
wait_for_feedback=feedback_required,
)
elif special_handler == "driver_seat_memory_recall":
sent_frames, feedback = self._execute_driver_seat_memory(
bus,
command,
wait_for_completion=feedback_required,
)
elif special_handler == "shade_preset":
sent_frames, feedback = self._execute_shade_preset(
bus, command
)
elif special_handler == "shade_position":
sent_frames, feedback = self._execute_shade_position(
bus, command
)
else:
sent_frames = self._send_command(bus, command)
feedback = self._wait_feedback(bus, command)
feedback_match = self._feedback_matches(command, feedback)
verified = feedback is not None and feedback_match is True
delivered = bool(sent_frames)
succeeded = verified or (delivered and not feedback_required)
physical_channel = (
feedback.channel
if feedback is not None
else sent_frames[-1].channel
if sent_frames
else None
)
mqtt_messages = list(
getattr(bus, "published_messages", ())[
publication_start:
]
)
delivery_status = "submitted"
if not feedback_required and delivered:
delivery_status = (
"published"
if mqtt_messages
and all(
message.get("delivery_status") == "published"
for message in mqtt_messages
)
else "queued"
)
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "succeeded" if succeeded else "failed",
"message": (
"MQTT 白名单控制帧已由 Broker 确认;车辆效果以状态话题回读为准。"
if not feedback_required and delivery_status == "published"
else "MQTT 白名单控制帧已进入客户端发布队列;车辆效果以状态话题回读为准。"
if not feedback_required
else "CAN 盒已提交车端控制帧并观察到匹配反馈。"
if feedback_match is True
else "CAN 盒已提交车端控制帧,但反馈字段与目标值不一致。"
if feedback_match is False
else "CAN 盒已提交车端控制帧并观察到状态帧,但该帧没有可证明动作完成的字段。"
if feedback is not None
else "CAN 盒已提交车端控制帧;当前反馈窗口未观察到对应状态帧。"
),
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": runtime.command_frame_public(command),
"sent_frames": [frame.public() for frame in sent_frames],
"feedback_frame": feedback.public() if feedback else None,
"feedback_observed": feedback is not None,
"feedback_match": feedback_match,
"feedback_state": self._feedback_state(feedback),
"feedback_required": feedback_required,
"delivery_status": delivery_status,
"physical_effect_confirmed": verified,
"control_gate": (
"compiled_action_and_frame_whitelist"
if not feedback_required
else "stationary_vehicle_and_feedback"
),
"channel": physical_channel,
"simulated": False,
**(
{"mqtt_messages": mqtt_messages}
if mqtt_messages
else {}
),
},
}
)
except Exception as exc: # noqa: BLE001 - normalize adapter faults
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "failed",
"message": str(exc),
"telemetry": {
"target": operation["target"]["zone"],
"requested_parameters": operation["parameters"],
"command_frame": runtime.command_frame_public(command),
"channel": 1,
"simulated": False,
},
}
)
break
finally:
bus.close()
return runtime._report(function_config, results, started_at, "vehicle")
@staticmethod
def _send_command(bus: CanBus, command: dict[str, Any]) -> list[CanFrame]:
signals = command["signals"]
frame_id = int(str(signals["frame_id"]), 0)
payload = bytes.fromhex(str(signals["data_hex"]))
repeat = command.get("repeat", {"count": 1, "period_ms": 0})
sent: list[CanFrame] = []
for index in range(int(repeat["count"])):
sent.append(
bus.send(
frame_id,
payload,
is_fd=bool(signals["is_fd"]),
bitrate_switch=bool(signals["bitrate_switch"]),
)
)
if index + 1 < int(repeat["count"]):
time.sleep(int(repeat["period_ms"]) / 1000)
return sent
def _execute_driver_seat_axis(
self,
bus: CanBus,
command: dict[str, Any],
*,
wait_for_feedback: bool = True,
) -> tuple[list[CanFrame], CanFrame | None]:
"""Send bounded motion and attempt all three stop frames in a finally path."""
signals = command["signals"]
special = json.loads(str(signals["special_parameters"]))
frame_id = int(str(signals["frame_id"]), 0)
action_payload = bytes.fromhex(str(signals["data_hex"]))
stop_payload = bytes.fromhex(str(special["stop_data_hex"]))
repeat = command.get("repeat", {"count": 1, "period_ms": 0})
sent: list[CanFrame] = []
action_error: Exception | None = None
stop_errors: list[Exception] = []
try:
for index in range(int(repeat["count"])):
sent.append(
bus.send(
frame_id,
action_payload,
is_fd=True,
bitrate_switch=True,
)
)
if index + 1 < int(repeat["count"]):
time.sleep(int(repeat["period_ms"]) / 1000)
time.sleep(int(special["final_action_hold_ms"]) / 1000)
except Exception as exc: # noqa: BLE001 - stop is mandatory after adapter faults
action_error = exc
finally:
for index in range(int(special["stop_repeat_count"])):
try:
sent.append(
bus.send(
frame_id,
stop_payload,
is_fd=True,
bitrate_switch=True,
)
)
except Exception as exc: # noqa: BLE001 - attempt remaining stop frames
stop_errors.append(exc)
if index + 1 < int(special["stop_repeat_count"]):
time.sleep(int(special["stop_period_ms"]) / 1000)
if action_error is not None or stop_errors:
details = []
if action_error is not None:
details.append(f"motion send failed: {action_error}")
if stop_errors:
details.append(
f"{len(stop_errors)}/3 forced-stop frame(s) failed"
)
raise RuntimeError("; ".join(details))
return sent, self._wait_feedback(bus, command) if wait_for_feedback else None
def _execute_driver_seat_memory(
self,
bus: CanBus,
command: dict[str, Any],
*,
wait_for_completion: bool = True,
) -> tuple[list[CanFrame], CanFrame | None]:
"""Require fresh initialization, then replay the learned 16/15 sequence."""
signals = command["signals"]
special = json.loads(str(signals["special_parameters"]))
before = self._wait_for_id(
bus, 0x494, int(special["fresh_status_timeout_ms"])
)
initialized = (
decode_feedback(before)
.get("actuator_state", {})
.get("driver", {})
.get("position_feedback", {})
.get("initialized", {})
if before is not None
else {}
)
axes = ("horizontal", "backrest", "height", "basin")
if not all(initialized.get(axis) is True for axis in axes):
raise RuntimeError(
"no fresh 0x494 proving all four driver-seat axes initialized; "
"refusing memory recall"
)
frame_id = int(str(signals["frame_id"]), 0)
sequence = [
bytes.fromhex(str(value)) for value in special["frame_sequence"]
]
if (
len(sequence) != 20
or any(len(payload) != 8 for payload in sequence)
or any(
payload[0] != (0x16 if index % 2 == 0 else 0x15)
for index, payload in enumerate(sequence)
)
):
raise RuntimeError("compiled driver-seat memory sequence is invalid")
sent: list[CanFrame] = []
for index, payload in enumerate(sequence):
sent.append(
bus.send(
frame_id,
payload,
is_fd=True,
bitrate_switch=True,
)
)
if index + 1 < len(sequence):
time.sleep(int(special["period_ms"]) / 1_000)
feedback = (
self._wait_for_id(
bus, 0x494, int(special["post_status_timeout_ms"])
)
if wait_for_completion
else None
)
return sent, feedback
def _execute_shade_preset(
self, bus: CanBus, command: dict[str, Any]
) -> tuple[list[CanFrame], CanFrame | None]:
"""Submit a preset and prove the shade is stopped at its target."""
special = json.loads(str(command["signals"]["special_parameters"]))
sent = self._send_command(bus, command)
feedback = self._wait_matching_feedback(
bus,
command,
timeout_ms=int(special["motion_timeout_ms"]),
)
if feedback is None:
return sent, None
roof = decode_feedback(feedback).get("domain_state", {}).get("roof", {})
target = special.get("target_position_pct")
if roof.get("motion_state") == "stopped" or target is None:
return sent, feedback
sent.append(bus.send(0x431, NativeCanRuntime._shade_payload(4)))
final = self._wait_for_shade_stopped(
bus,
target=int(target),
tolerance=int(special["tolerance_pct"]),
timeout_ms=self.feedback_timeout_ms,
)
if final is None:
raise RuntimeError(
"shade reached the preset position, but STOP state was not confirmed"
)
return sent, final
def _execute_shade_position(
self, bus: CanBus, command: dict[str, Any]
) -> tuple[list[CanFrame], CanFrame | None]:
"""Drive toward a non-preset position and always issue a STOP frame."""
special = json.loads(str(command["signals"]["special_parameters"]))
target = int(special["position_pct"])
tolerance = int(special["tolerance_pct"])
before = self._wait_for_id(
bus, 0x41A, int(special["fresh_feedback_timeout_ms"])
)
before_state = decode_feedback(before) if before is not None else {}
roof = before_state.get("domain_state", {}).get("roof", {})
current = roof.get("shade_position_pct")
if not isinstance(current, int) or not 0 <= current <= 100:
raise RuntimeError(
"no fresh valid 0x41A shade position; refusing blind motion"
)
if roof.get("thermal_protection") is True:
raise RuntimeError(
"0x41A reports shade thermal protection; refusing motion"
)
if abs(current - target) <= tolerance:
if roof.get("motion_state") == "stopped":
return [], before
stopped = bus.send(
0x431, bytes.fromhex(str(special["stop_data_hex"]))
)
final = self._wait_for_shade_stopped(
bus,
target=target,
tolerance=tolerance,
timeout_ms=self.feedback_timeout_ms,
)
if final is None:
raise RuntimeError(
"shade is at the target but STOP state was not confirmed"
)
return [stopped], final
moving_open = target > current
start_payload = bytes.fromhex(
str(
special[
"open_data_hex" if moving_open else "close_data_hex"
]
)
)
stop_payload = bytes.fromhex(str(special["stop_data_hex"]))
sent: list[CanFrame] = []
reached: CanFrame | None = None
action_error: Exception | None = None
stop_error: Exception | None = None
try:
sent.append(bus.send(0x431, start_payload))
deadline = time.monotonic() + int(special["motion_timeout_ms"]) / 1_000
while time.monotonic() < deadline:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1_000))
frame = self._wait_for_id(bus, 0x41A, min(remaining_ms, 200))
if frame is None:
continue
state = decode_feedback(frame).get("domain_state", {}).get("roof", {})
position = state.get("shade_position_pct")
if not isinstance(position, int):
continue
if state.get("thermal_protection") is True:
raise RuntimeError("shade entered thermal protection during motion")
if (moving_open and position >= target) or (
not moving_open and position <= target
):
reached = frame
break
except Exception as exc: # noqa: BLE001 - STOP remains mandatory
action_error = exc
finally:
try:
sent.append(bus.send(0x431, stop_payload))
except Exception as exc: # noqa: BLE001 - normalize after action error
stop_error = exc
if action_error is not None or stop_error is not None:
details = []
if action_error is not None:
details.append(f"shade motion failed: {action_error}")
if stop_error is not None:
details.append(f"mandatory shade STOP failed: {stop_error}")
raise RuntimeError("; ".join(details))
if reached is None:
raise RuntimeError(
"shade did not cross the requested position before timeout; STOP sent"
)
# Confirm both the final position and that the mechanism reports STOP.
final = self._wait_for_shade_stopped(
bus,
target=target,
tolerance=tolerance,
timeout_ms=self.feedback_timeout_ms,
)
if final is None:
raise RuntimeError(
"shade crossed the target and STOP was sent, but no stopped 0x41A "
"within the target tolerance was observed"
)
return sent, final
def _wait_feedback(
self, bus: CanBus, command: dict[str, Any]
) -> CanFrame | None:
raw_ids = str(command["signals"].get("feedback_ids", ""))
feedback_ids = {
int(value, 0) for value in raw_ids.split(",") if value.strip()
}
if not feedback_ids:
return None
expected = str(command["signals"].get("expected_feedback", ""))
last_candidate: CanFrame | None = None
deadline = time.monotonic() + self.feedback_timeout_ms / 1000
while time.monotonic() < deadline:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1000))
frame = bus.receive(min(remaining_ms, 100), include_fd=True)
if frame is None or frame.arbitration_id not in feedback_ids:
continue
last_candidate = frame
if feedback_matches(expected, frame) is True:
return frame
return last_candidate
@staticmethod
def _wait_matching_feedback(
bus: CanBus,
command: dict[str, Any],
*,
timeout_ms: int,
) -> CanFrame | None:
raw_ids = str(command["signals"].get("feedback_ids", ""))
feedback_ids = {
int(value, 0) for value in raw_ids.split(",") if value.strip()
}
expected = str(command["signals"].get("expected_feedback", ""))
deadline = time.monotonic() + max(1, timeout_ms) / 1_000
last_candidate: CanFrame | None = None
while time.monotonic() < deadline:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1_000))
frame = bus.receive(min(remaining_ms, 100), include_fd=True)
if frame is None or frame.arbitration_id not in feedback_ids:
continue
last_candidate = frame
if feedback_matches(expected, frame) is True:
return frame
return last_candidate
@staticmethod
def _wait_for_shade_stopped(
bus: CanBus,
*,
target: int,
tolerance: int,
timeout_ms: int,
) -> CanFrame | None:
deadline = time.monotonic() + max(1, timeout_ms) / 1_000
while time.monotonic() < deadline:
remaining_ms = max(1, int((deadline - time.monotonic()) * 1_000))
frame = bus.receive(min(remaining_ms, 100), include_fd=True)
if frame is None or frame.arbitration_id != 0x41A:
continue
roof = decode_feedback(frame).get("domain_state", {}).get("roof", {})
position = roof.get("shade_position_pct")
if (
isinstance(position, int)
and abs(position - target) <= tolerance
and roof.get("motion_state") == "stopped"
):
return frame
return None
@staticmethod
def _feedback_state(frame: CanFrame | None) -> dict[str, Any] | None:
if frame is None:
return None
decoded = decode_feedback(frame)
source = f"0x{frame.arbitration_id:03X}"
if frame.arbitration_id == 0x4FA:
if len(frame.data) < 8:
return {"source": source, "valid": False}
thermal = {1: "off", 2: "low", 3: "high"}
massage_modes = {
1: "off",
2: "wave",
3: "pulse",
4: "stretch",
5: "slack",
6: "relax",
}
massage_strengths = {8: "off", 9: "weak", 10: "medium", 11: "strong"}
ventilation_raw = frame.data[0] & 0x07
heating_raw = (frame.data[0] >> 3) & 0x07
massage_mode_raw = frame.data[5] & 0x0F
massage_strength_raw = (frame.data[5] >> 4) & 0x0F
return {
**decoded,
"feedback_frame_id": source,
"valid": True,
"target": "driver",
"ventilation": thermal.get(
ventilation_raw, f"unknown({ventilation_raw})"
),
"heating": thermal.get(heating_raw, f"unknown({heating_raw})"),
"massage_mode": massage_modes.get(
massage_mode_raw, f"unknown({massage_mode_raw})"
),
"massage_strength": massage_strengths.get(
massage_strength_raw, f"unknown({massage_strength_raw})"
),
}
if frame.arbitration_id == 0x494:
if len(frame.data) < 8:
return {"source": source, "valid": False}
initialized = {
"horizontal": bool((frame.data[7] >> 2) & 1),
"backrest": bool((frame.data[7] >> 3) & 1),
"height": bool((frame.data[7] >> 4) & 1),
"basin": bool((frame.data[7] >> 5) & 1),
}
return {
**decoded,
"feedback_frame_id": source,
"valid": True,
"target": "driver",
"initialized": initialized,
"execution_confirmation": "status_frame_only",
"limitation": "0x494 has no absolute motor position",
}
return {**decoded, "feedback_frame_id": source, "valid": True}
def _execute_window_position(
self, bus: CanBus, command: dict[str, Any]
) -> tuple[list[CanFrame], CanFrame | None]:
parameters = json.loads(command["signals"]["special_parameters"])
target = str(parameters["target"])
goal = int(parameters["position_pct"])
indexes = {"front_left": 0, "rear_left": 1, "front_right": 2, "rear_right": 3}
index = indexes[target]
before = self._wait_for_id(bus, 0x388, 2_500)
if before is None or len(before.data) < 6 or before.data[2 + index] > 100:
raise RuntimeError("未收到有效且新鲜的 0x388 车窗位置,拒绝启动机构")
current = int(before.data[2 + index])
if abs(current - goal) <= 3:
return [], before
motion = 1 if goal > current else 2
start_payload = NativeCanRuntime._window_payload(target, motion)
stop_payload = NativeCanRuntime._window_payload(target, 3)
sent: list[CanFrame] = []
for attempt in range(3):
sent.append(bus.send(0x431, start_payload, is_fd=True, bitrate_switch=True))
if attempt < 2:
time.sleep(0.1)
deadline = time.monotonic() + 12
reached: CanFrame | None = None
try:
while time.monotonic() < deadline:
frame = self._wait_for_id(bus, 0x388, 200)
if frame is None or len(frame.data) < 6 or frame.data[2 + index] > 100:
continue
if abs(int(frame.data[2 + index]) - goal) <= 3:
reached = frame
break
finally:
for attempt in range(3):
sent.append(bus.send(0x431, stop_payload, is_fd=True, bitrate_switch=True))
if attempt < 2:
time.sleep(0.1)
if reached is None:
raise RuntimeError("车窗在 12 秒内未到达目标,已发送停止序列")
return sent, reached
@staticmethod
def _wait_for_id(bus: CanBus, frame_id: int, timeout_ms: int) -> CanFrame | None:
deadline = time.monotonic() + timeout_ms / 1000
while time.monotonic() < deadline:
remaining = max(1, int((deadline - time.monotonic()) * 1000))
frame = bus.receive(min(remaining, 100), include_fd=True)
if frame is not None and frame.arbitration_id == frame_id:
return frame
return None
@staticmethod
def _feedback_matches(
command: dict[str, Any], frame: CanFrame | None
) -> bool | None:
expected = str(command["signals"].get("expected_feedback", ""))
return feedback_matches(expected, frame)