优化前端,优化前端指令,优化自定义控制
This commit is contained in:
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
@@ -61,11 +62,13 @@ DRIVER_SEAT_CAPABILITIES = frozenset(
|
||||
"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", "door.set_rear_lock"}
|
||||
{"door.set_vehicle_lock"}
|
||||
)
|
||||
|
||||
|
||||
@@ -76,18 +79,14 @@ def _unsupported_ch1_door_lock_message(capability: str) -> str:
|
||||
"current CH1; the 0x496 smoke request caused no 0x407 vehicle-lock or "
|
||||
"four-door transition, so CH1 door-lock support is observational only"
|
||||
)
|
||||
return (
|
||||
"door.set_rear_lock is unsupported for physical execution on the current "
|
||||
"CH1; 0x32B is second-row-only and is not a verified whole-vehicle lock "
|
||||
"control, 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-v4"
|
||||
mapping_version = "can-dev-verified-body-can-v5"
|
||||
|
||||
def __init__(self, repository_root: Path):
|
||||
self.root = repository_root
|
||||
@@ -122,6 +121,8 @@ class NativeCanRuntime:
|
||||
"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,
|
||||
@@ -185,7 +186,12 @@ class NativeCanRuntime:
|
||||
"timeout_ms": (
|
||||
20_000
|
||||
if capability_name
|
||||
in {"window.set_position", "roof.set_shade_position"}
|
||||
in {
|
||||
"window.set_position",
|
||||
"roof.set_shade_position",
|
||||
"seat.set_driver_position_native",
|
||||
"seat.recall_driver_memory_native",
|
||||
}
|
||||
else 4_000
|
||||
),
|
||||
"failure_policy": "abort",
|
||||
@@ -232,6 +238,7 @@ class NativeCanRuntime:
|
||||
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
|
||||
@@ -337,8 +344,103 @@ class NativeCanRuntime:
|
||||
"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":
|
||||
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],
|
||||
@@ -380,6 +482,23 @@ class NativeCanRuntime:
|
||||
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)
|
||||
@@ -412,6 +531,21 @@ class NativeCanRuntime:
|
||||
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 = {
|
||||
@@ -478,6 +612,12 @@ class NativeCanRuntime:
|
||||
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)]
|
||||
@@ -810,6 +950,78 @@ class NativeCanRuntime:
|
||||
},
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -876,9 +1088,11 @@ class NativeCanExecutor:
|
||||
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,
|
||||
@@ -896,7 +1110,13 @@ class NativeCanExecutor:
|
||||
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()
|
||||
@@ -915,6 +1135,10 @@ class NativeCanExecutor:
|
||||
sent_frames, feedback = self._execute_driver_seat_axis(
|
||||
bus, command
|
||||
)
|
||||
elif special_handler == "driver_seat_memory_recall":
|
||||
sent_frames, feedback = self._execute_driver_seat_memory(
|
||||
bus, command
|
||||
)
|
||||
elif special_handler == "shade_preset":
|
||||
sent_frames, feedback = self._execute_shade_preset(
|
||||
bus, command
|
||||
@@ -927,8 +1151,10 @@ class NativeCanExecutor:
|
||||
sent_frames = self._send_command(bus, command)
|
||||
feedback = self._wait_feedback(bus, command)
|
||||
feedback_match = self._feedback_matches(command, feedback)
|
||||
feedback_required = True
|
||||
feedback_required = self.feedback_required
|
||||
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
|
||||
@@ -936,13 +1162,24 @@ class NativeCanExecutor:
|
||||
if sent_frames
|
||||
else None
|
||||
)
|
||||
mqtt_messages = list(
|
||||
getattr(bus, "published_messages", ())[
|
||||
publication_start:
|
||||
]
|
||||
)
|
||||
results.append(
|
||||
{
|
||||
"ref": operation["id"],
|
||||
"domain": function_config["spec"]["domain"],
|
||||
"status": "succeeded" if verified else "failed",
|
||||
"status": "succeeded" if succeeded else "failed",
|
||||
"message": (
|
||||
"CAN 盒已提交车端控制帧并观察到匹配反馈。"
|
||||
"MQTT 白名单控制帧已发布,并收到匹配的车辆状态反馈。"
|
||||
if not feedback_required and feedback_match is True
|
||||
else "MQTT 白名单控制帧已发布;已收到状态帧,但目标值尚未确认。"
|
||||
if not feedback_required and feedback is not None
|
||||
else "MQTT 白名单控制帧已发布;车辆状态反馈尚未确认。"
|
||||
if not feedback_required
|
||||
else "CAN 盒已提交车端控制帧并观察到匹配反馈。"
|
||||
if feedback_match is True
|
||||
else "CAN 盒已提交车端控制帧,但反馈字段与目标值不一致。"
|
||||
if feedback_match is False
|
||||
@@ -960,8 +1197,24 @@ class NativeCanExecutor:
|
||||
"feedback_match": feedback_match,
|
||||
"feedback_state": self._feedback_state(feedback),
|
||||
"feedback_required": feedback_required,
|
||||
"delivery_status": (
|
||||
"published"
|
||||
if not feedback_required and delivered
|
||||
else "submitted"
|
||||
),
|
||||
"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 {}
|
||||
),
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -1062,6 +1315,62 @@ class NativeCanExecutor:
|
||||
raise RuntimeError("; ".join(details))
|
||||
return sent, self._wait_feedback(bus, command)
|
||||
|
||||
def _execute_driver_seat_memory(
|
||||
self, bus: CanBus, command: dict[str, Any]
|
||||
) -> 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"])
|
||||
)
|
||||
return sent, feedback
|
||||
|
||||
def _execute_shade_preset(
|
||||
self, bus: CanBus, command: dict[str, Any]
|
||||
) -> tuple[list[CanFrame], CanFrame | None]:
|
||||
|
||||
Reference in New Issue
Block a user