feat: integrate cabin CAN and verified dynamic seat modes

This commit is contained in:
vee
2026-08-04 17:51:38 +08:00
parent 155fbb59ba
commit dc7c91800e
99 changed files with 51897 additions and 3618 deletions
@@ -1,4 +1,4 @@
"""Load model-facing capability context from domain-owned catalogs."""
"""Load model-facing capability context from domain and native-CAN catalogs."""
from __future__ import annotations
@@ -21,18 +21,25 @@ class CapabilityRegistry:
def _load(self) -> dict[str, Any]:
domains = []
for catalog_path in sorted(
(self.root / "domains").glob("*/config/capabilities.yaml")
):
catalog_paths = [
*(self.root / "domains").glob("*/config/capabilities.yaml"),
*(self.root / "config" / "native_can_capabilities").glob("*.yaml"),
]
for catalog_path in sorted(catalog_paths):
if catalog_path.parts[-3] == "_template":
continue
catalog = yaml.safe_load(catalog_path.read_text(encoding="utf-8"))
domain = catalog["spec"]["domain"]
native_can_catalog = catalog_path.parent.name == "native_can_capabilities"
domains.append(
{
"domain": domain,
"availability": (
"executable" if domain in self.executable_domains else "catalog_only"
"native_can"
if native_can_catalog
else "executable"
if domain in self.executable_domains
else "catalog_only"
),
"capabilities": catalog["spec"]["capabilities"],
}
@@ -45,6 +52,9 @@ class CapabilityRegistry:
"execution_policy": {
"executable_domains": sorted(self.executable_domains),
"catalog_only_behavior": "return_as_deferred_request",
"native_can_behavior": (
"compile_for_body_can_when_hardware_mode_is_authorized"
),
"planning_environment": "dry_run_candidate_synthesis",
"named_preset_required": False,
"fuzzy_goal_behavior": (
@@ -67,6 +67,13 @@ def _new_state(
axis: _motion_state(axis, position)
for axis, position in MOTION_INITIAL_POSITIONS.items()
},
"dynamic_mode": {
"mode_id": 0,
"name": "",
"state": "idle",
"repeat": "none",
"max_runtime_ms": 600_000,
},
}
for target in TARGETS
},
@@ -246,6 +253,28 @@ class ConversationStateStore:
state["last_target"] = action["target"]
continue
target = action["target"]
if action.get("action_type") == "dynamic_mode_stop":
state["devices"][target]["dynamic_mode"] = {
"mode_id": 0,
"name": "",
"state": "idle",
"repeat": "none",
"max_runtime_ms": 600_000,
}
state["last_target"] = target
continue
if action.get("action_type") == "dynamic_mode":
state["devices"][target]["dynamic_mode"] = {
"mode_id": int(action["mode_id"]),
"name": str(action.get("mode_name", "")),
"state": "running",
"repeat": str(action.get("repeat", "infinite")),
"max_runtime_ms": int(
action.get("max_runtime_ms", 600_000)
),
}
state["last_target"] = target
continue
if action.get("action_type") == "position":
axis = action["axis"]
position_state = _motion_state(axis, action["position_pct"])
+207 -2
View File
@@ -102,6 +102,195 @@ class ModeOperation:
}
@dataclass(frozen=True)
class ModeTimelinePhase:
id: str
name: str
duration_ms: int | None
transition: str
targets: dict[str, Any]
effects: dict[str, Any]
completion: str
timeout_ms: int | None
motion: dict[str, Any]
arrival_grace_ms: int = 0
@classmethod
def from_dict(cls, document: dict[str, Any]) -> "ModeTimelinePhase":
phase_id = str(document.get("id", "")).strip()
if not phase_id:
raise ValueError("mode timeline phase id must not be empty")
duration_ms = document.get("duration_ms")
completion = str(
document.get(
"completion",
"elapsed_time" if duration_ms is not None else "all_targets_reached",
)
).strip()
if completion not in {"elapsed_time", "all_targets_reached"}:
raise ValueError(
f"mode timeline phase {phase_id} completion must be elapsed_time or all_targets_reached"
)
if completion == "elapsed_time":
if (
not isinstance(duration_ms, int)
or isinstance(duration_ms, bool)
or duration_ms <= 0
):
raise ValueError(
f"mode timeline phase {phase_id} duration_ms must be a positive integer"
)
elif duration_ms is not None:
raise ValueError(
f"mode timeline phase {phase_id} duration_ms must be omitted for all_targets_reached"
)
timeout_ms = document.get("timeout_ms")
if timeout_ms is not None and (
not isinstance(timeout_ms, int)
or isinstance(timeout_ms, bool)
or timeout_ms <= 0
):
raise ValueError(
f"mode timeline phase {phase_id} timeout_ms must be a positive integer"
)
if completion == "all_targets_reached" and timeout_ms is None:
raise ValueError(
f"mode timeline phase {phase_id} timeout_ms is required for all_targets_reached"
)
arrival_grace_ms = document.get("arrival_grace_ms", 0)
if (
not isinstance(arrival_grace_ms, int)
or isinstance(arrival_grace_ms, bool)
or arrival_grace_ms < 0
):
raise ValueError(
f"mode timeline phase {phase_id} arrival_grace_ms must be a non-negative integer"
)
targets = document.get("targets", {})
effects = document.get("effects", {})
motion = document.get("motion", {})
if (
not isinstance(targets, dict)
or not isinstance(effects, dict)
or not isinstance(motion, dict)
):
raise ValueError(
f"mode timeline phase {phase_id} targets/effects/motion must be objects"
)
return cls(
id=phase_id,
name=str(document.get("name", phase_id)),
duration_ms=duration_ms,
transition=str(document.get("transition", "hold")),
targets=dict(targets),
effects=dict(effects),
completion=completion,
timeout_ms=timeout_ms,
motion=dict(motion),
arrival_grace_ms=arrival_grace_ms,
)
def to_yaml(self) -> dict[str, Any]:
result = {
"id": self.id,
"name": self.name,
"transition": self.transition,
"targets": dict(self.targets),
"effects": dict(self.effects),
"completion": self.completion,
}
if self.duration_ms is not None:
result["duration_ms"] = self.duration_ms
if self.timeout_ms is not None:
result["timeout_ms"] = self.timeout_ms
if self.motion:
result["motion"] = dict(self.motion)
if self.arrival_grace_ms:
result["arrival_grace_ms"] = self.arrival_grace_ms
return result
@dataclass(frozen=True)
class ModeTimeline:
repeat: str
cycle_duration_ms: int | None
entry: dict[str, Any]
phases: tuple[ModeTimelinePhase, ...]
max_runtime_ms: int | None = None
@classmethod
def from_dict(cls, document: dict[str, Any]) -> "ModeTimeline":
repeat = str(document.get("repeat", "")).strip()
if repeat not in {"once", "infinite", "until_stopped_or_timeout"}:
raise ValueError(
"mode timeline repeat must be once, infinite, or until_stopped_or_timeout"
)
max_runtime_ms = document.get("max_runtime_ms")
if max_runtime_ms is not None and (
not isinstance(max_runtime_ms, int)
or isinstance(max_runtime_ms, bool)
or max_runtime_ms <= 0
):
raise ValueError("mode timeline max_runtime_ms must be a positive integer")
if repeat == "until_stopped_or_timeout" and max_runtime_ms is None:
raise ValueError(
"mode timeline max_runtime_ms is required for until_stopped_or_timeout"
)
duration_ms = document.get("cycle_duration_ms")
if duration_ms is not None and (
not isinstance(duration_ms, int)
or isinstance(duration_ms, bool)
or duration_ms <= 0
):
raise ValueError("mode timeline cycle_duration_ms must be positive")
raw_entry = document.get("entry", {})
raw_phases = document.get("phases", [])
if not isinstance(raw_entry, dict):
raise ValueError("mode timeline entry must be an object")
if not isinstance(raw_phases, list) or not raw_phases:
raise ValueError("mode timeline must contain at least one phase")
phases = tuple(ModeTimelinePhase.from_dict(item) for item in raw_phases)
phase_ids = [phase.id for phase in phases]
if len(set(phase_ids)) != len(phase_ids):
raise ValueError("mode timeline phase ids must be unique")
timed_phases = all(phase.duration_ms is not None for phase in phases)
if timed_phases:
calculated_duration = sum(
int(phase.duration_ms or 0) for phase in phases
)
if calculated_duration != duration_ms:
raise ValueError(
"mode timeline cycle_duration_ms does not equal the sum of phases: "
f"declared={duration_ms}, calculated={calculated_duration}"
)
elif duration_ms is not None:
raise ValueError(
"mode timeline cycle_duration_ms must be omitted when a phase completes by target arrival"
)
return cls(
repeat=repeat,
cycle_duration_ms=duration_ms,
entry=dict(raw_entry),
phases=phases,
max_runtime_ms=max_runtime_ms,
)
def to_yaml(self) -> dict[str, Any]:
result = {
"repeat": self.repeat,
"entry": dict(self.entry),
"phases": [phase.to_yaml() for phase in self.phases],
}
if self.cycle_duration_ms is not None:
result["cycle_duration_ms"] = self.cycle_duration_ms
if self.max_runtime_ms is not None:
result["max_runtime_ms"] = self.max_runtime_ms
return result
def public_dict(self) -> dict[str, Any]:
return self.to_yaml()
@dataclass(frozen=True)
class ModeProfile:
id: str
@@ -113,6 +302,7 @@ class ModeProfile:
source: str
common: bool = True
vehicle_ids: tuple[str, ...] = ()
timeline: ModeTimeline | None = None
@classmethod
def from_yaml(cls, path: Path, profile_root: Path | None = None) -> "ModeProfile":
@@ -128,6 +318,9 @@ class ModeProfile:
name,
[str(alias) for alias in document.get("aliases", [])],
)
raw_timeline = document.get("timeline")
if raw_timeline is not None and not isinstance(raw_timeline, dict):
raise ValueError(f"{path} timeline must be an object")
return cls(
id=str(document["id"]),
name=name,
@@ -138,10 +331,15 @@ class ModeProfile:
source=str(path),
common=common,
vehicle_ids=vehicle_ids,
timeline=(
ModeTimeline.from_dict(raw_timeline)
if isinstance(raw_timeline, dict)
else None
),
)
def public_dict(self) -> dict[str, Any]:
return {
result = {
"id": self.id,
"name": self.name,
"aliases": list(self.aliases),
@@ -153,7 +351,11 @@ class ModeProfile:
"common": self.common,
"vehicle_ids": list(self.vehicle_ids),
},
"behavior": "cyclic" if self.timeline is not None else "static",
}
if self.timeline is not None:
result["timeline"] = self.timeline.public_dict()
return result
class ModeProfileRegistry:
@@ -370,7 +572,7 @@ def _mode_profile_document(
"common": profile.common,
"vehicle_ids": list(profile.vehicle_ids),
}
return {
document = {
"id": profile.id,
"name": profile.name,
"aliases": list(profile.aliases),
@@ -379,6 +581,9 @@ def _mode_profile_document(
"scope": scope,
"operations": [operation.to_yaml() for operation in operations],
}
if profile.timeline is not None:
document["timeline"] = profile.timeline.to_yaml()
return document
def _replace_yaml(path: Path, document: dict[str, Any]) -> None:
@@ -27,7 +27,14 @@ SEAT_MOTION_CAPABILITIES = {
SEAT_RELATIVE_PHYSICAL_PARAMETERS = {"distance_mm", "angle_delta_deg"}
SAVE_PRESET_CAPABILITY = "seat.save_posture_preset"
APPLY_PRESET_CAPABILITY = "seat.apply_posture_preset"
DYNAMIC_MODE_CAPABILITY = "seat.start_dynamic_mode"
DYNAMIC_MODE_STOP_CAPABILITY = "seat.stop_dynamic_mode"
DYNAMIC_MODE_MAX_RUNTIME_MS = 600_000
HEX_COLOR = re.compile(r"^#[0-9A-Fa-f]{6}$")
NATIVE_CAN_ONLY_CAPABILITIES = {
"climate.set_control_mode",
"roof.set_shade_motion",
}
class ModelPlanAdapter:
@@ -69,7 +76,7 @@ class ModelPlanAdapter:
)
continue
domain, availability, capability = registered
domain, domain_availability, capability = registered
parameters = {
str(item["name"]): item.get("value")
for item in request.get("parameters", [])
@@ -93,7 +100,26 @@ class ModelPlanAdapter:
)
continue
if availability != "executable":
availability = (
"native_can"
if capability_name in NATIVE_CAN_ONLY_CAPABILITIES
else domain_availability
)
native_only_request = (
domain == "seat"
and capability_name in SEAT_CAPABILITIES
and (
request.get("target_scope") in {"left_rear", "right_rear"}
or parameters.get("level") in {4, 5}
)
) or (
capability_name == "climate.set_outlet_mode"
and (
request.get("target_scope") == "vehicle"
or parameters.get("mode") not in {"face", "feet"}
)
)
if availability != "executable" or native_only_request:
values = [value for value in parameters.values() if value is not None]
deferred.append(
{
@@ -104,6 +130,11 @@ class ModelPlanAdapter:
"requested_value": values[0] if len(values) == 1 else None,
"parameters": parameters,
"reason": request["reason"],
"execution_backend": (
"native_can"
if availability == "native_can" or native_only_request
else availability
),
}
)
continue
@@ -182,6 +213,20 @@ class ModelPlanAdapter:
}
)
continue
if capability_name == DYNAMIC_MODE_STOP_CAPABILITY:
dynamic_action = self._dynamic_mode_stop_action(
index, request, parameters, ambiguities
)
if dynamic_action is not None:
actions.append(dynamic_action)
continue
if capability_name == DYNAMIC_MODE_CAPABILITY:
dynamic_action = self._dynamic_mode_action(
index, request, parameters, ambiguities
)
if dynamic_action is not None:
actions.append(dynamic_action)
continue
action = self._seat_action(
index, request, parameters, ambiguities, context, vehicle_profile
)
@@ -352,6 +397,79 @@ class ModelPlanAdapter:
return None
return {"target": target, "preset_name": preset_name.strip()}
@staticmethod
def _dynamic_mode_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前动态座椅模式仅控制副驾。",
}
)
return None
mode_id = parameters.get("mode_id")
if (
not isinstance(mode_id, (int, float))
or isinstance(mode_id, bool)
or not float(mode_id).is_integer()
or int(mode_id) not in {1, 2}
):
ambiguities.append(
{
"field": f"requests[{index}].parameters.mode_id",
"question": "动态座椅模式编号必须是 1(木马模式)或 2(动态摇篮)。",
}
)
return None
normalized_mode_id = int(mode_id)
return {
"action_type": "dynamic_mode",
"target": target,
"mode_id": normalized_mode_id,
"mode_name": (
"木马模式"
if normalized_mode_id == 1
else "动态摇篮模式"
),
"repeat": "until_stopped_or_timeout",
"max_runtime_ms": DYNAMIC_MODE_MAX_RUNTIME_MS,
}
@staticmethod
def _dynamic_mode_stop_action(
index: int,
request: dict[str, Any],
parameters: dict[str, Any],
ambiguities: list[dict[str, str]],
) -> dict[str, Any] | None:
target = request.get("target_scope")
if target != "passenger":
ambiguities.append(
{
"field": f"requests[{index}].target_scope",
"question": "当前动态座椅模式仅控制副驾。",
}
)
return None
if parameters:
ambiguities.append(
{
"field": f"requests[{index}].parameters",
"question": "停止动态座椅模式不接受额外参数。",
}
)
return None
return {
"action_type": "dynamic_mode_stop",
"target": target,
}
@staticmethod
def _find_preset(
presets: list[dict[str, Any]],
@@ -0,0 +1,348 @@
"""Deterministic parsing for the trusted vehicle CAN command vocabulary.
The parser only emits named capabilities. It never accepts a raw CAN ID or
payload from a user utterance; byte-level compilation stays in NativeCanRuntime.
"""
from __future__ import annotations
import re
from typing import Any
WINDOW_TARGETS = {
"全部": "all",
"所有": "all",
"四个": "all",
"全车": "all",
"前排": "front",
"后排": "rear",
"左前": "front_left",
"前左": "front_left",
"右前": "front_right",
"前右": "front_right",
"左后": "rear_left",
"后左": "rear_left",
"右后": "rear_right",
"后右": "rear_right",
}
OUTLET_TARGETS = {
"主驾左": "driver_left",
"主驾右": "driver_right",
"副驾左": "passenger_left",
"副驾右": "passenger_right",
}
LIGHT_TARGETS = {
"全部": "all",
"所有": "all",
"左前": "front_left",
"右前": "front_right",
"中排左旋钮": "middle_left_rotary",
"中排右旋钮": "middle_right_rotary",
"中排左": "middle_left",
"中排右": "middle_right",
"左后": "rear_left",
"右后": "rear_right",
}
SEAT_TARGETS = {
"driver": "driver",
"passenger": "passenger",
"left-rear": "left_rear",
"right-rear": "right_rear",
}
def _action(
domain: str,
capability: str,
target: str,
parameters: dict[str, Any],
reason: str,
) -> dict[str, Any]:
return {
"domain": domain,
"capability": capability,
"target": target,
"parameters": parameters,
"operation": "set",
"reason": reason,
"execution_backend": "native_can",
}
def _parse_exact_command(text: str) -> list[dict[str, Any]]:
command = " ".join(text.strip().lower().split())
match = re.fullmatch(r"door rear (lock|unlock)", command)
if match:
return [_action("door", "door.set_rear_lock", "rear", {"state": match[1]}, "用户输入了受信任的后排门锁命令。")]
match = re.fullmatch(
r"window (all|front|rear|front-left|front-right|rear-left|rear-right) (open|close|stop)",
command,
)
if match:
return [_action("window", "window.set_motion", match[1].replace("-", "_"), {"motion": match[2]}, "用户输入了受信任的车窗动作命令。")]
match = re.fullmatch(
r"window (front-left|front-right|rear-left|rear-right) position (\d+)",
command,
)
if match and 0 <= int(match[2]) <= 100:
return [_action("window", "window.set_position", match[1].replace("-", "_"), {"position_pct": int(match[2])}, "用户输入了受信任的车窗位置命令。")]
match = re.fullmatch(r"ac (on|off)", command)
if match:
return [_action("climate", "climate.set_power", "vehicle", {"state": match[1]}, "用户输入了受信任的空调开关命令。")]
match = re.fullmatch(r"ac fan ([1-7])", command)
if match:
return [_action("climate", "climate.set_fan_level", "vehicle", {"level": int(match[1])}, "用户输入了受信任的空调风量命令。")]
match = re.fullmatch(
r"ac mode (face|face-feet|feet|feet-defrost|defrost|face-feet-defrost|face-defrost)",
command,
)
if match:
return [_action("climate", "climate.set_outlet_mode", "vehicle", {"mode": match[1].replace("-", "_")}, "用户输入了受信任的空调出风模式命令。")]
match = re.fullmatch(r"ac temp (driver|passenger|rear) (\d+)", command)
if match and 18 <= int(match[2]) <= 32:
return [_action("climate", "climate.set_temperature", match[1], {"temperature_c": int(match[2])}, "用户输入了受信任的空调温度命令。")]
match = re.fullmatch(r"ac control (manual|auto)", command)
if match:
return [_action("climate", "climate.set_control_mode", "vehicle", {"mode": match[1]}, "用户输入了受信任的空调控制模式命令。")]
match = re.fullmatch(
r"outlet (driver-left|driver-right|passenger-left|passenger-right) percent (\d+) (\d+)",
command,
)
if match and all(0 <= int(value) <= 100 for value in match.groups()[1:]):
return [_action("outlet", "outlet.set_position_percent", match[1].replace("-", "_"), {"horizontal_pct": int(match[2]), "vertical_pct": int(match[3])}, "用户输入了受信任的电动风口百分比命令。")]
match = re.fullmatch(
r"outlet (driver-left|driver-right|passenger-left|passenger-right) position (-?\d+) (-?\d+)",
command,
)
if match and all(-128 <= int(value) <= 126 for value in match.groups()[1:]):
return [_action("outlet", "outlet.set_calibrated_position", match[1].replace("-", "_"), {"horizontal_value": int(match[2]), "vertical_value": int(match[3])}, "用户输入了受信任的电动风口标定坐标命令。")]
match = re.fullmatch(
r"seat (driver|passenger|left-rear|right-rear) (heat|vent) (off|[1-5])",
command,
)
if match:
parameters: dict[str, Any] = {"state": "off" if match[3] == "off" else "on"}
if match[3] != "off":
parameters["level"] = int(match[3])
capability = "seat.set_heating" if match[2] == "heat" else "seat.set_ventilation"
return [_action("seat", capability, SEAT_TARGETS[match[1]], parameters, "用户输入了受信任的整车座椅热舒适命令。")]
match = re.fullmatch(r"light (indoor|wall) (on|off)", command)
if match:
capability = "interior_light.set_indoor_power" if match[1] == "indoor" else "interior_light.set_wall_power"
return [_action("interior_light", capability, "vehicle", {"state": match[2]}, "用户输入了受信任的车内灯命令。")]
match = re.fullmatch(
r"light reading (all|front-left|front-right|middle-left|middle-right|middle-left-rotary|middle-right-rotary|rear-left|rear-right) (on|off)",
command,
)
if match:
return [_action("interior_light", "interior_light.set_reading_power", match[1].replace("-", "_"), {"state": match[2]}, "用户输入了受信任的阅读灯命令。")]
match = re.fullmatch(r"light ambient (on|off)", command)
if match:
return [_action("ambient_light", "ambient_light.set_power", "vehicle", {"state": match[1]}, "用户输入了受信任的氛围灯开关命令。")]
match = re.fullmatch(r"light ambient scene (camping|nap|cinema|meditation|romance)", command)
if match:
mode = {"nap": "rest", "romance": "romantic"}.get(match[1], match[1])
return [_action("ambient_light", "ambient_light.set_mode", "vehicle", {"mode": mode}, "用户输入了受信任的氛围灯场景命令。")]
match = re.fullmatch(r"light ambient rgb (\d+) (\d+) (\d+) (\d+)", command)
if match:
red, green, blue, brightness = map(int, match.groups())
if all(1 <= value <= 255 for value in (red, green, blue)) and 0 <= brightness <= 100:
return [_action("ambient_light", "ambient_light.set_illumination", "vehicle", {"color_hex": f"#{red:02X}{green:02X}{blue:02X}", "brightness_pct": brightness}, "用户输入了受信任的氛围灯 RGB 命令。")]
match = re.fullmatch(r"shade (open|close|half|stop)", command)
if match:
return [_action("roof", "roof.set_shade_motion", "vehicle", {"motion": match[1]}, "用户输入了受信任的遮阳帘动作命令。")]
match = re.fullmatch(r"shade position (\d+)", command)
if match and 0 <= int(match[1]) <= 100:
return [_action("roof", "roof.set_shade_position", "vehicle", {"position_pct": int(match[1])}, "用户输入了受信任的遮阳帘位置命令。")]
return []
def _parse_chinese_commands(text: str) -> list[dict[str, Any]]:
compact = re.sub(r"\s+", "", text)
actions: list[dict[str, Any]] = []
if "空调" in compact or re.search(r"(?:AC|A/C)", text, re.IGNORECASE):
if re.search(r"(?:打开|开启|启动)空调", compact):
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "on"}, "用户要求打开空调。"))
elif re.search(r"(?:关闭|关掉|关上)空调", compact):
actions.append(_action("climate", "climate.set_power", "vehicle", {"state": "off"}, "用户要求关闭空调。"))
temperature = re.search(
r"(?:(主驾|副驾|后排)(?:空调)?(?:温度)?|空调(?:温度)?|温度)(?:调到|设为|设置为|到)?(1[89]|2\d|3[0-2])(?:℃|度)?",
compact,
)
if temperature:
target = {"主驾": "driver", "副驾": "passenger", "后排": "rear"}.get(temperature[1], "vehicle")
actions.append(_action("climate", "climate.set_temperature", target, {"temperature_c": int(temperature[2])}, "用户指定了空调温度。"))
fan = re.search(r"(?:风量|风速)(?:调到|设为|设置为|到)?([1-7])(?:档)?", compact)
if fan:
actions.append(_action("climate", "climate.set_fan_level", "vehicle", {"level": int(fan[1])}, "用户指定了空调风量。"))
if re.search(r"自动(?:控制|模式)", compact):
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "auto"}, "用户指定空调自动控制。"))
elif re.search(r"手动(?:控制|模式)", compact):
actions.append(_action("climate", "climate.set_control_mode", "vehicle", {"mode": "manual"}, "用户指定空调手动控制。"))
outlet_modes = [
("吹面吹脚除霜", "face_feet_defrost"),
("吹面除霜", "face_defrost"),
("吹脚除霜", "feet_defrost"),
("吹面吹脚", "face_feet"),
("除霜", "defrost"),
("吹脚", "feet"),
("吹面", "face"),
]
for label, mode in outlet_modes:
if label in compact:
actions.append(_action("climate", "climate.set_outlet_mode", "vehicle", {"mode": mode}, "用户指定了空调出风模式。"))
break
if "车窗" in compact:
target = next((value for label, value in WINDOW_TARGETS.items() if label in compact), "all")
position = re.search(r"(?:位置|开度|调到|设为)(\d{1,3})%", compact)
if position and target not in {"all", "front", "rear"} and int(position[1]) <= 100:
actions.append(_action("window", "window.set_position", target, {"position_pct": int(position[1])}, "用户指定了单个车窗的绝对开度。"))
else:
motion = (
"stop" if re.search(r"停止|停下|暂停", compact)
else "close" if re.search(r"关闭|关上|升起|关窗", compact)
else "open" if re.search(r"打开|开启|降下|开窗", compact)
else None
)
if motion:
actions.append(_action("window", "window.set_motion", target, {"motion": motion}, "用户指定了车窗目标和动作。"))
if re.search(r"后排.*(?:门锁|车门)|(?:门锁|车门).*后排", compact):
state = "unlock" if re.search(r"解锁|开锁", compact) else "lock" if re.search(r"上锁|落锁|锁上", compact) else None
if state:
actions.append(_action("door", "door.set_rear_lock", "rear", {"state": state}, "用户指定了后排门锁状态。"))
if re.search(r"遮阳帘|天幕帘", compact):
position = re.search(r"(?:位置|开度|调到|设为)(\d{1,3})%", compact)
if position and int(position[1]) <= 100:
actions.append(_action("roof", "roof.set_shade_position", "vehicle", {"position_pct": int(position[1])}, "用户指定了遮阳帘开度。"))
else:
motion = (
"stop" if re.search(r"停止|停下|暂停", compact)
else "half" if re.search(r"半开|一半", compact)
else "close" if re.search(r"关闭|关上", compact)
else "open" if re.search(r"打开|开启", compact)
else None
)
if motion:
actions.append(_action("roof", "roof.set_shade_motion", "vehicle", {"motion": motion}, "用户指定了遮阳帘动作。"))
for label, capability in (("室内灯", "interior_light.set_indoor_power"), ("车内灯", "interior_light.set_indoor_power"), ("壁灯", "interior_light.set_wall_power")):
if label in compact:
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
if state:
actions.append(_action("interior_light", capability, "vehicle", {"state": state}, "用户指定了车内灯状态。"))
break
if "阅读灯" in compact:
target = next((value for label, value in LIGHT_TARGETS.items() if label in compact), "all")
state = "off" if re.search(r"关闭|关掉|关上", compact) else "on" if re.search(r"打开|开启", compact) else None
if state:
actions.append(_action("interior_light", "interior_light.set_reading_power", target, {"state": state}, "用户指定了阅读灯区域和状态。"))
outlet_target = next((value for label, value in OUTLET_TARGETS.items() if f"{label}风口" in compact), None)
outlet_position = re.search(r"(?:左右|水平)(\d{1,3})%?.*?(?:上下|垂直)(\d{1,3})%?", compact)
if outlet_target and outlet_position and all(int(value) <= 100 for value in outlet_position.groups()):
actions.append(_action("outlet", "outlet.set_position_percent", outlet_target, {"horizontal_pct": int(outlet_position[1]), "vertical_pct": int(outlet_position[2])}, "用户指定了已标定风口的双轴百分比位置。"))
return actions
def _signature(action: dict[str, Any]) -> tuple[str, str, str, tuple[tuple[str, str], ...]]:
return (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope"))),
tuple(sorted((str(key), repr(value)) for key, value in action.get("parameters", {}).items())),
)
def merge_native_can_intent(parsed: dict[str, Any], utterance: str) -> dict[str, Any]:
"""Add deterministic vehicle-CAN actions without duplicating model requests."""
clauses = [part for part in re.split(r"[,,。;;!!??]+", utterance) if part.strip()]
exact_actions = [action for clause in clauses for action in _parse_exact_command(clause)]
actions = exact_actions or _parse_chinese_commands(utterance)
if not actions:
return parsed
result = dict(parsed)
existing = [
*result.get("actions", []),
*result.get("native_can_actions", []),
*[
item
for item in result.get("deferred_requests", [])
if item.get("execution_backend") == "native_can"
],
]
signatures = {_signature(item) for item in existing}
unique_actions = [item for item in actions if _signature(item) not in signatures]
if not unique_actions:
return result
result["native_can_actions"] = [
*result.get("native_can_actions", []),
*unique_actions,
]
result["deferred_requests"] = [
*result.get("deferred_requests", []),
*[
{
"domain": item["domain"],
"capability": item["capability"],
"target_scope": item["target"],
"desired_change": item["operation"],
"requested_value": next(iter(item["parameters"].values())) if len(item["parameters"]) == 1 else None,
"parameters": dict(item["parameters"]),
"reason": item["reason"],
"execution_backend": "native_can",
}
for item in unique_actions
],
]
result["model_requests"] = [
*result.get("model_requests", []),
*[
{
"domain": item["domain"],
"capability": item["capability"],
"target_scope": item["target"],
"operation": item["operation"],
"parameters": [
{"name": name, "value": value}
for name, value in item["parameters"].items()
],
"reason": item["reason"],
}
for item in unique_actions
],
]
domains = list(result.get("detected_domains", []))
for item in unique_actions:
if item["domain"] not in domains:
domains.append(item["domain"])
result["detected_domains"] = domains
result["ambiguities"] = [
item
for item in result.get("ambiguities", [])
if item.get("field") != "capability"
]
result["decision_summary"] = [
*result.get("decision_summary", []),
*[item["reason"] for item in unique_actions],
]
result["confidence"] = max(float(result.get("confidence", 0)), 0.98)
return result
@@ -0,0 +1,864 @@
"""Trusted compilation and execution support for CAN_DEV vehicle controls."""
from __future__ import annotations
import hashlib
import json
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.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."""
class NativeCanRuntime:
"""Compile named capabilities into byte-exact CAN_DEV frames."""
compiler_version = "1.0.0"
mapping_version = "can-dev-6e7ed04"
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(
{
"door.set_rear_lock",
"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",
"outlet.set_position_percent",
"outlet.set_calibrated_position",
"seat.set_heating",
"seat.set_ventilation",
"interior_light.set_indoor_power",
"interior_light.set_wall_power",
"interior_light.set_reading_power",
"ambient_light.set_power",
"ambient_light.set_mode",
"ambient_light.set_illumination",
"roof.set_shade_motion",
"roof.set_shade_position",
}
)
def supports(self, action: dict[str, Any]) -> bool:
return str(action.get("capability")) in self.supported_capabilities
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:
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", {})),
"timeout_ms": 15_000 if capability_name == "window.set_position" 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,
}
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"]
return {
"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}),
}
@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 = operation["capability"]
target = operation["target"]["zone"]
parameters = operation["parameters"]
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_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 == "door.set_rear_lock":
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, feedback_ids=(0x407,), expected=f"rear_lock:{parameters['state']}")]
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_heating", "seat.set_ventilation"}:
return [self._body_seat_frame(capability, target, 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":
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 24, 3, state)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"indoor_light:{parameters['state']}")]
if capability == "interior_light.set_wall_power":
state = self._enum(parameters, "state", {"on": 1, "off": 2})
payload = bytearray(8)
payload[0] = 0x11
_set_bits(payload, 20, 2, state)
return [self._classic(0x32B, payload)]
if capability == "interior_light.set_reading_power":
return [self._reading_light_frame(target, parameters)]
if capability == "ambient_light.set_power":
value = self._enum(parameters, "state", {"off": 6, "on": 7})
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 27, 5, value)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"ambient_power:{parameters['state']}")]
if capability == "ambient_light.set_mode":
value = self._enum(
parameters,
"mode",
{"camping": 1, "rest": 2, "cinema": 3, "meditation": 4, "romantic": 5, "off": 6, "on": 7},
)
payload = bytearray(8)
payload[0] = 0x0A
_set_bits(payload, 27, 5, value)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected=f"ambient_mode:{value}")]
if capability == "ambient_light.set_illumination":
color = str(parameters.get("color_hex", ""))
if len(color) != 7 or not color.startswith("#"):
raise NativeCanCompileError("color_hex must use #RRGGBB")
try:
red, green, blue = (int(color[index : index + 2], 16) for index in (1, 3, 5))
except ValueError as exc:
raise NativeCanCompileError("color_hex must use #RRGGBB") from exc
if not all(1 <= value <= 255 for value in (red, green, blue)):
raise NativeCanCompileError("CAN_DEV RGB components must be between 1 and 255")
brightness = self._integer(parameters, "brightness_pct", 0, 100)
payload = bytearray((0x2B, red, green, blue, 0, 0, 0, 0))
_set_bits(payload, 32, 7, brightness)
return [self._classic(0x32B, payload, feedback_ids=(0x42E,), expected="ambient_rgb")]
if capability == "roof.set_shade_motion":
motion = self._enum(parameters, "motion", {"open": 1, "close": 2, "half": 3, "stop": 4})
payload = bytearray(8)
_set_bits(payload, 4, 4, motion)
return [self._classic(0x431, payload, feedback_ids=(0x41A,), expected=f"shade_motion:{motion}")]
if capability == "roof.set_shade_position":
position = self._integer(parameters, "position_pct", 0, 100)
payload = bytearray(8)
payload[3] = 0xFF
payload[4] = position
return [self._classic(0x4F5, payload, feedback_ids=(0x41A,), expected=f"shade_position:{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}",
)
def _body_seat_frame(
self, capability: str, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
indexes = {"driver": 0, "passenger": 1, "left_rear": 2, "right_rear": 3}
if target not in indexes:
raise NativeCanCompileError(f"unsupported body seat target {target}")
state = str(parameters.get("state"))
if state not in {"on", "off"}:
raise NativeCanCompileError("state must be on or off")
feature = "heating" if capability.endswith("heating") else "ventilation"
index = indexes[target]
payload = bytearray(8)
if state == "off":
payload[0] = 0x20
starts = (12, 20, 28, 36) if feature == "heating" else (8, 16, 24, 32)
_set_bits(payload, starts[index], 2, 2)
else:
level = self._integer(parameters, "level", 1, 5)
payload[0] = 0x21
starts = (12, 20, 28, 36) if feature == "heating" else (8, 16, 24, 32)
_set_bits(payload, starts[index], 4, level)
return self._classic(0x4C1, payload, feedback_ids=(0x42A,), expected=f"body_seat:{target}:{feature}:{state}")
def _reading_light_frame(
self, target: str, parameters: dict[str, Any]
) -> NativeFrameSpec:
state = self._enum(parameters, "state", {"off": 1, "on": 2})
if target == "all":
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"reading_all:{parameters['state']}")
starts = {
"front_left": 0,
"front_right": 2,
"middle_left": 4,
"middle_right": 6,
"middle_left_rotary": 8,
"middle_right_rotary": 10,
"rear_left": 12,
"rear_right": 14,
}
try:
start = starts[target]
except KeyError as exc:
raise NativeCanCompileError(f"unsupported reading-light target {target}") from exc
payload = bytearray(8)
_set_bits(payload, start, 2, state)
return self._fd(0x1B2, payload, 3, 100, expected=f"reading:{target}:{parameters['state']}")
class NativeCanExecutor:
"""Submit compiled vehicle frames and capture post-command feedback."""
def __init__(
self,
bus_factory: Callable[[], CanBus],
*,
feedback_timeout_ms: int = 1_200,
):
self.bus_factory = bus_factory
self.feedback_timeout_ms = feedback_timeout_ms
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"]]
try:
if command["signals"].get("special_handler") == "window_position":
sent_frames, feedback = self._execute_window_position(bus, command)
else:
sent_frames = self._send_command(bus, command)
feedback = self._wait_feedback(bus, command)
feedback_match = self._feedback_matches(command, feedback)
feedback_required = bool(
str(command["signals"].get("feedback_ids", "")).strip()
)
verified = (
(not feedback_required or feedback is not None)
and feedback_match is not False
)
results.append(
{
"ref": operation["id"],
"domain": function_config["spec"]["domain"],
"status": "succeeded" if verified else "failed",
"message": (
"CAN 盒已提交车端控制帧并观察到匹配反馈。"
if feedback_match is True
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_required": feedback_required,
"channel": 1,
"simulated": False,
},
}
)
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 _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
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 not None and frame.arbitration_id in feedback_ids:
return frame
return None
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:
if frame is None:
return None
expected = str(command["signals"].get("expected_feedback", ""))
if not expected or frame.arbitration_id != 0x404 or len(frame.data) < 6:
return None
parts = expected.split(":")
if parts[0] == "climate_power":
actual = frame.data[1] & 0x03
return actual == (1 if parts[1] == "on" else 2)
if parts[0] == "climate_control":
return ((frame.data[1] >> 2) & 0x03) == int(parts[1])
if parts[0] == "climate_mode":
return (frame.data[2] & 0x0F) == int(parts[1])
if parts[0] == "climate_fan":
return ((frame.data[2] >> 4) & 0x0F) == int(parts[1])
if parts[0] == "climate_temperature":
byte_index = {"driver": 4, "passenger": 5}.get(parts[1])
return None if byte_index is None else frame.data[byte_index] == int(parts[2])
return None
+438 -48
View File
@@ -8,6 +8,7 @@ import re
import sys
import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from datetime import UTC, datetime
from pathlib import Path
@@ -46,10 +47,16 @@ from cognitive_agent.application.mode_profile import ( # noqa: E402
from cognitive_agent.application.model_plan_adapter import ( # noqa: E402
ModelPlanAdapter,
APPLY_PRESET_CAPABILITY,
DYNAMIC_MODE_STOP_CAPABILITY,
SAVE_PRESET_CAPABILITY,
SEAT_CAPABILITIES,
SEAT_MOTION_CAPABILITIES,
)
from cognitive_agent.application.native_can_intent import merge_native_can_intent # noqa: E402
from cognitive_agent.application.native_can_runtime import ( # noqa: E402
NativeCanExecutor,
NativeCanRuntime,
)
from cognitive_agent.application.seat_calibration import position_pct_to_angle # noqa: E402
from cognitive_agent.application.vehicle_profile import ( # noqa: E402
VehicleProfile,
@@ -63,7 +70,9 @@ from cognitive_agent.infrastructure.can.hardware import ( # noqa: E402
HardwareSafetyError,
PHYSICAL_BENCH_COORDINATOR,
SeatHardwareSettings,
VehicleCanSettings,
)
from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig # noqa: E402
from cognitive_agent.infrastructure.can.access import ( # noqa: E402
PHYSICAL_BENCH_ACCESS,
)
@@ -112,6 +121,18 @@ SEAT_AXIS_TO_CAPABILITY = {
SEAT_FUNCTION_TO_CAPABILITY = {
function: capability for capability, function in SEAT_CAPABILITIES.items()
}
DYNAMIC_MODE_STOP_WORDS = ("停止", "关闭", "结束", "取消", "停下", "退出")
DYNAMIC_MODE_WORDS = (
"动态模式",
"动态座椅",
"木马模式",
"木马",
"摇摇马",
"骑木马",
"rocking horse",
"动态摇篮",
"摇篮模式",
)
class SeatDemoService:
@@ -142,6 +163,8 @@ class SeatDemoService:
repository_root / "platforms" / "t2g_seat_bench" / "calibration" / "seat.yaml",
)
self.hardware_settings = SeatHardwareSettings.from_env(repository_root)
self.vehicle_can_settings = VehicleCanSettings.from_env(repository_root)
self.native_can_runtime = NativeCanRuntime(repository_root)
self.hardware_execution = PHYSICAL_BENCH_COORDINATOR
self.hardware_access = PHYSICAL_BENCH_ACCESS
# Shared by the short-lived hardware executor instances. A process must
@@ -192,8 +215,18 @@ class SeatDemoService:
"platform": "reference_vehicle",
"execution_mode": "dry_run",
"execution_modes": ["dry_run", "simulated_hardware", "hardware"],
"hardware_execution_allowed": self.hardware_settings.configured,
"hardware_execution_allowed": (
self.hardware_settings.configured
and self.vehicle_can_settings.configured
),
"hardware": hardware_info,
"vehicle_can": self.vehicle_can_settings.public_info(),
"multi_channel_can": {
"adapter_ownership": "single_process",
"seat_channel": self.hardware_settings.channel,
"vehicle_channel": self.vehicle_can_settings.channel,
"channels_must_be_distinct": True,
},
"seat_position_memory_synchronized": bool(
self._seat_position_memory_state.get("synchronized", False)
),
@@ -298,10 +331,13 @@ class SeatDemoService:
"actuator_state": context["devices"],
}
def _new_hardware_executor(self) -> HardwareSeatExecutor:
def _new_hardware_executor(
self, bus_factory: Any | None = None
) -> HardwareSeatExecutor:
resolved_bus_factory = bus_factory or self.hardware_settings.bus_factory()
return HardwareSeatExecutor(
self.hardware_matrix,
self.hardware_settings.bus_factory(),
resolved_bus_factory,
thermal_timeout_ms=self.hardware_settings.thermal_timeout_ms,
ack_timeout_ms=self.hardware_settings.ack_timeout_ms,
axis_maximum_x100={
@@ -319,7 +355,7 @@ class SeatDemoService:
),
auto_home=self.hardware_settings.auto_home_enabled,
home_timeout_s=self.hardware_settings.home_timeout_s,
firmware_guard=self.hardware_settings.firmware_guard(),
firmware_guard=self.hardware_settings.firmware_guard(resolved_bus_factory),
synchronize_position_before_first_motion=(
self.hardware_settings.position_sync_enabled
),
@@ -414,8 +450,6 @@ class SeatDemoService:
or mode_update is not None
)
hardware_request = execution_mode == "hardware"
if hardware_request and not control_plane_request:
self.hardware_settings.authorize(approval_token)
conversation_id = conversation_id or f"conv-{uuid.uuid4().hex}"
if not CONVERSATION_ID_PATTERN.fullmatch(conversation_id):
raise ValueError("conversation_id must start with conv- and contain only lowercase letters, numbers, or hyphens")
@@ -512,7 +546,40 @@ class SeatDemoService:
context,
vehicle_profile,
)
parsed = self._apply_mode_profiles(parsed, matched_modes, context, vehicle_profile)
if self._is_dynamic_mode_stop_request(utterance):
parsed = self._apply_dynamic_mode_stop_request(parsed, utterance)
else:
parsed = self._apply_mode_profiles(
parsed, matched_modes, context, vehicle_profile
)
parsed = merge_native_can_intent(parsed, utterance)
parsed = self._partition_native_actions(
parsed, promote_supported=hardware_request
)
if hardware_request and not control_plane_request:
if parsed.get("actions"):
self.hardware_settings.authorize(approval_token)
if parsed.get("native_can_actions"):
self.vehicle_can_settings.authorize(approval_token)
if not parsed.get("actions") and not parsed.get("native_can_actions"):
raise HardwareSafetyError(
"hardware mode requires at least one verified seat or vehicle CAN operation"
)
if parsed.get("actions") and parsed.get("native_can_actions"):
if self.hardware_settings.backend != "zlgcan":
raise HardwareSafetyError(
"linked seat/vehicle execution requires the shared zlgcan backend"
)
try:
seat_channel = int(self.hardware_settings.channel, 0)
except ValueError as exc:
raise HardwareSafetyError(
"ZLG seat CAN channel must be an integer"
) from exc
if seat_channel == self.vehicle_can_settings.channel:
raise HardwareSafetyError(
"seat and vehicle CAN must use different adapter channels"
)
self._trace(
traces,
request_id,
@@ -580,14 +647,9 @@ class SeatDemoService:
},
)
if parsed.get("domain_actions") or any(
if parsed.get("domain_actions") or parsed.get("native_can_actions") or any(
domain != "seat" for domain in detected_domains
):
if hardware_request and not parsed.get("actions"):
raise HardwareSafetyError(
"hardware mode requires at least one passenger seat operation; "
"non-seat-only requests stay dry-run"
)
return self._process_orchestrated(
utterance,
request_id,
@@ -1405,6 +1467,63 @@ class SeatDemoService:
)
return merged
@staticmethod
def _is_dynamic_mode_stop_request(utterance: str) -> bool:
compact = re.sub(r"\s+", "", utterance)
if any(negation in compact for negation in ("不要停止", "无需停止", "别停止")):
return False
return any(word in compact for word in DYNAMIC_MODE_STOP_WORDS) and any(
word in compact for word in DYNAMIC_MODE_WORDS
)
def _apply_dynamic_mode_stop_request(
self,
parsed: dict[str, Any],
utterance: str,
) -> dict[str, Any]:
"""Make explicit stop language authoritative over a matched start profile."""
stopped = deepcopy(parsed)
stopped["actions"] = [
action
for action in stopped.get("actions", [])
if action.get("action_type") not in {"dynamic_mode", "dynamic_mode_stop"}
]
stopped["actions"].append(
{"action_type": "dynamic_mode_stop", "target": "passenger"}
)
stopped["model_requests"] = [
request
for request in stopped.get("model_requests", [])
if request.get("capability")
not in {"seat.start_dynamic_mode", DYNAMIC_MODE_STOP_CAPABILITY}
]
stopped["model_requests"].append(
{
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": "passenger",
"operation": "deactivate",
"parameters": [],
"reason": "用户明确要求停止动态座椅循环。",
"source_span": utterance,
}
)
stopped["ambiguities"] = [
item
for item in stopped.get("ambiguities", [])
if item.get("field") != "capability"
]
stopped["detected_domains"] = self._merge_domains(
["seat"], stopped.get("detected_domains", [])
)
stopped["decision_summary"] = [
"识别到停止动态座椅模式指令;不会重新启动同名长期模式。",
*stopped.get("decision_summary", []),
]
stopped["confidence"] = max(float(stopped.get("confidence", 0)), 0.99)
return stopped
@staticmethod
def _contains_seat_preset_request(parsed: dict[str, Any]) -> bool:
capabilities = {
@@ -1454,6 +1573,19 @@ class SeatDemoService:
action.get("target"),
action.get("axis"),
)
if action.get("action_type") == "dynamic_mode_stop":
return (
"seat",
"dynamic_mode_stop",
action.get("target"),
)
if action.get("action_type") == "dynamic_mode":
return (
"seat",
"dynamic_mode",
action.get("target"),
action.get("mode_id"),
)
return (
"seat",
"function",
@@ -2136,6 +2268,24 @@ class SeatDemoService:
"parameters": parameters,
"reason": "用户固化的座椅位置模式动作。",
}
if action.get("action_type") == "dynamic_mode_stop":
return {
"domain": "seat",
"capability": DYNAMIC_MODE_STOP_CAPABILITY,
"target_scope": action["target"],
"operation": "deactivate",
"parameters": {},
"reason": "用户固化的动态座椅停止动作。",
}
if action.get("action_type") == "dynamic_mode":
return {
"domain": "seat",
"capability": "seat.start_dynamic_mode",
"target_scope": action["target"],
"operation": "set",
"parameters": {"mode_id": int(action["mode_id"])},
"reason": "用户固化的固件原生动态座椅模式。",
}
function = action.get("function")
capability = SEAT_FUNCTION_TO_CAPABILITY.get(str(function))
if capability is None:
@@ -2152,6 +2302,71 @@ class SeatDemoService:
"reason": "用户固化的座椅舒适功能模式动作。",
}
def _partition_native_actions(
self, parsed: dict[str, Any], *, promote_supported: bool = False
) -> dict[str, Any]:
"""Promote verified CAN_DEV capabilities and leave unsupported domains virtual."""
result = deepcopy(parsed)
native_actions = list(result.get("native_can_actions", []))
native_signatures = {
(
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
for action in native_actions
}
virtual_actions: list[dict[str, Any]] = []
for action in result.get("domain_actions", []):
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature in native_signatures:
continue
if promote_supported and self.native_can_runtime.supports(action):
promoted = dict(action)
promoted["execution_backend"] = "native_can"
native_actions.append(promoted)
else:
virtual_actions.append(action)
deduplicated: list[dict[str, Any]] = []
signatures: set[tuple[str, str, str, str]] = set()
for action in native_actions:
signature = (
str(action.get("domain")),
str(action.get("capability")),
str(action.get("target", action.get("target_scope", "vehicle"))),
json.dumps(action.get("parameters", {}), sort_keys=True, ensure_ascii=False),
)
if signature not in signatures:
signatures.add(signature)
deduplicated.append(action)
result["domain_actions"] = virtual_actions
result["native_can_actions"] = deduplicated
detected_domains = list(result.get("detected_domains", []))
if result.get("actions") and "seat" not in detected_domains:
detected_domains.insert(0, "seat")
for action in [*virtual_actions, *deduplicated]:
if action["domain"] not in detected_domains:
detected_domains.append(action["domain"])
result["detected_domains"] = detected_domains
result["deferred_requests"] = [
item
for item in result.get("deferred_requests", [])
if not (
item.get("execution_backend") == "native_can"
and self.native_can_runtime.supports(item)
)
]
return result
def _process_orchestrated(
self,
utterance: str,
@@ -2172,10 +2387,17 @@ class SeatDemoService:
vehicle_profile = vehicle_profile or self.vehicle_profiles.default()
generation_started = time.perf_counter()
intent_ir = build_intent_ir(parsed, utterance, request_id, provider_used)
for action in parsed.get("domain_actions", []):
capability = self.domain_runtimes[action["domain"]].capabilities[
action["capability"]
]
intent_actions = [
*parsed.get("domain_actions", []),
*parsed.get("native_can_actions", []),
]
for action in intent_actions:
if action in parsed.get("native_can_actions", []):
capability = self.native_can_runtime.capabilities[action["capability"]]
else:
capability = self.domain_runtimes[action["domain"]].capabilities[
action["capability"]
]
intent_ir["spec"]["requests"].append(
{
"domain": action["domain"],
@@ -2212,6 +2434,20 @@ class SeatDemoService:
provider_used,
)
native_config_keys: set[str] = set()
native_actions_by_domain: dict[str, list[dict[str, Any]]] = {}
for action in parsed.get("native_can_actions", []):
native_actions_by_domain.setdefault(action["domain"], []).append(action)
for domain, actions in native_actions_by_domain.items():
key = f"native_{domain}"
native_config_keys.add(key)
configs[key] = self.native_can_runtime.build_function_config(
domain,
actions,
request_id,
provider_used,
)
result["function_configs"] = configs
result["function_config"] = (
configs.get("seat") or next(iter(configs.values()), None)
@@ -2252,7 +2488,8 @@ class SeatDemoService:
"message": ambiguity["question"],
}
)
validation.append(
if not native_config_keys:
validation.append(
{
"severity": "warning",
"code": "VIRTUAL_EXECUTION_ONLY",
@@ -2269,8 +2506,8 @@ class SeatDemoService:
"code": "NO_EXECUTABLE_OPERATION",
"path": "spec.operations",
"message": "模型没有生成任何可执行领域操作。",
}
)
}
)
if hardware_request:
self._adapt_validation_for_hardware(validation)
result["validation"] = validation
@@ -2310,6 +2547,8 @@ class SeatDemoService:
)
else:
plans[domain] = self.compiler.compile(config)
elif domain in native_config_keys:
plans[domain] = self.native_can_runtime.compile(config)
else:
runtime = self.domain_runtimes[domain]
plans[domain] = runtime.compile(config)
@@ -2388,37 +2627,47 @@ class SeatDemoService:
execute_started = time.perf_counter()
execution_started_at = _now()
reports: dict[str, dict[str, Any]] = {}
for domain, config in configs.items():
if domain == "seat":
if hardware_request:
reports[domain] = self._new_hardware_executor().execute(
plans[domain], config, context["devices"]
)
elif simulated_hardware_request:
reports[domain] = self.simulated_hardware_executor.execute(
plans[domain],
config,
context["devices"],
fault_scenario=simulation_fault,
)
else:
reports[domain] = self.executor.execute(
plans[domain], config, context["devices"]
)
else:
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
reports = self._execute_orchestrated_reports(
configs,
plans,
native_config_keys,
context,
hardware_request=hardware_request,
simulated_hardware_request=simulated_hardware_request,
simulation_fault=simulation_fault,
)
report_results = [
item
for domain_report in reports.values()
for item in domain_report["spec"]["results"]
]
operation_results = [
item
for item in report_results
if item.get("ref") != "seat-controller-firmware"
]
operation_domains = {
str(item.get("domain")) for item in operation_results
}
successful_domains = {
domain
for domain in operation_domains
if all(
item.get("status") == "succeeded"
for item in operation_results
if str(item.get("domain")) == domain
)
}
failed_domains = {
str(item.get("domain"))
for item in operation_results
if item.get("status") == "failed"
}
execution_succeeded = all(
domain_report["spec"]["status"] == "succeeded"
for domain_report in reports.values()
) and all(item.get("status") == "succeeded" for item in report_results)
execution_partially_succeeded = bool(successful_domains and failed_domains)
physical_state = (
reports.get("seat", {}).get("spec", {}).get("physical_state")
if hardware_request
@@ -2477,6 +2726,8 @@ class SeatDemoService:
result["validation"] = validation
if report_issues:
result["status"] = "execution_report_invalid"
elif execution_partially_succeeded:
result["status"] = "partially_succeeded"
elif not execution_succeeded:
result["status"] = "execution_failed"
elif parsed.get("deferred_requests"):
@@ -2496,7 +2747,14 @@ class SeatDemoService:
"副驾座椅已按物理 CAN 回读闭环执行,其他领域已完成虚拟 CAN 执行。"
if hardware_request and execution_succeeded
else (
f"副驾座椅真实执行失败:{failed_execution['message']}"
(
"跨域指令部分执行成功;成功域="
f"{','.join(sorted(successful_domains))},失败域="
f"{','.join(sorted(failed_domains))}:"
f"{failed_execution['message']}"
)
if execution_partially_succeeded and failed_execution
else f"真实 CAN 执行失败:{failed_execution['message']}"
if hardware_request and failed_execution
else (
f"模拟台架已执行 {len(report_results)} 个领域操作,物理 CAN 未打开。"
@@ -2523,19 +2781,40 @@ class SeatDemoService:
"failed_domain": (
failed_execution.get("domain") if failed_execution else None
),
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
"partially_succeeded": execution_partially_succeeded,
},
domain="orchestration",
)
if execution_succeeded and not report_issues:
seat_actions = parsed.get("actions", [])
if hardware_request:
if (execution_succeeded or execution_partially_succeeded) and not report_issues:
seat_actions = (
parsed.get("actions", [])
if execution_succeeded or "seat" in successful_domains
else []
)
if hardware_request and seat_actions and "seat" in reports:
seat_actions = self._hardware_feedback_actions(
seat_actions,
configs["seat"],
reports["seat"],
)
all_actions = [*seat_actions, *parsed.get("domain_actions", [])]
all_actions = [
*seat_actions,
*[
action
for action in parsed.get("domain_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
*[
action
for action in parsed.get("native_can_actions", [])
if execution_succeeded
or str(action.get("domain")) in successful_domains
],
]
updated_context = self.conversations.apply_successful_actions(
conversation_id,
utterance,
@@ -2559,11 +2838,17 @@ class SeatDemoService:
request_id,
"state_update",
"succeeded",
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。",
(
f"跨域会话状态已更新为第 {updated_context['turn_count']} 轮。"
if execution_succeeded
else "仅成功设备已写入会话状态;失败设备保持执行前状态。"
),
time.perf_counter(),
{
"seat_state": updated_context["devices"],
"domain_state": updated_context["domain_devices"],
"successful_domains": sorted(successful_domains),
"failed_domains": sorted(failed_domains),
},
domain="orchestration",
)
@@ -2595,6 +2880,111 @@ class SeatDemoService:
self._persist(result)
return result
def _execute_orchestrated_reports(
self,
configs: dict[str, dict[str, Any]],
plans: dict[str, dict[str, Any]],
native_config_keys: set[str],
context: dict[str, Any],
*,
hardware_request: bool,
simulated_hardware_request: bool,
simulation_fault: str,
) -> dict[str, dict[str, Any]]:
reports: dict[str, dict[str, Any]] = {}
bridge = None
seat_bus_factory = None
native_bus_factory = None
if hardware_request and native_config_keys:
channel_configs = [self.vehicle_can_settings.channel_config()]
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
channel_configs.insert(
0,
ZlgChannelConfig(
channel=seat_channel,
arbitration_bitrate=self.hardware_settings.bitrate,
data_bitrate=self.vehicle_can_settings.data_bitrate,
can_fd_iso=True,
),
)
bridge = self.vehicle_can_settings.bridge(channel_configs)
native_bus_factory = lambda: bridge.channel_bus(
self.vehicle_can_settings.channel
)
if "seat" in configs:
seat_channel = int(self.hardware_settings.channel, 0)
seat_bus_factory = lambda: bridge.channel_bus(seat_channel)
try:
def execute_native_configs() -> dict[str, dict[str, Any]]:
native_reports: dict[str, dict[str, Any]] = {}
for key in sorted(native_config_keys):
if not hardware_request:
native_reports[key] = self.native_can_runtime.execute_virtual(
plans[key], configs[key]
)
continue
executor = NativeCanExecutor(
native_bus_factory,
feedback_timeout_ms=(
self.vehicle_can_settings.feedback_timeout_ms
),
)
native_reports[key] = executor.execute(
self.native_can_runtime,
plans[key],
configs[key],
)
return native_reports
def execute_seat_config() -> dict[str, Any]:
config = configs["seat"]
if hardware_request:
seat_executor = (
self._new_hardware_executor(seat_bus_factory)
if seat_bus_factory is not None
else self._new_hardware_executor()
)
return seat_executor.execute(
plans["seat"], config, context["devices"]
)
if simulated_hardware_request:
return self.simulated_hardware_executor.execute(
plans["seat"],
config,
context["devices"],
fault_scenario=simulation_fault,
)
return self.executor.execute(
plans["seat"], config, context["devices"]
)
if hardware_request and native_config_keys and "seat" in configs:
# Both channel views share one adapter owner; executor threads let
# the two channels start within normal USB scheduling jitter.
with ThreadPoolExecutor(max_workers=2) as pool:
native_future = pool.submit(execute_native_configs)
seat_future = pool.submit(execute_seat_config)
reports.update(native_future.result())
reports["seat"] = seat_future.result()
else:
reports.update(execute_native_configs())
if "seat" in configs:
reports["seat"] = execute_seat_config()
for domain, config in configs.items():
if domain in native_config_keys or domain == "seat":
continue
reports[domain] = self.domain_runtimes[domain].execute(
plans[domain], config
)
finally:
if bridge is not None:
bridge.close()
return reports
@staticmethod
def _build_scene_config(
request_id: str,