feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
"""Execute normal seat command plans through the shared ZCANPRO bridge.
|
||||
"""Execute normal seat command plans through the shared CAN runtime.
|
||||
|
||||
ZLG's Windows driver gives one process exclusive ownership of a USBCAN device.
|
||||
Self-repair already keeps that ownership inside ZCANPRO, so opening the same
|
||||
adapter again from the web server makes both user and admin orchestration fail.
|
||||
This executor keeps one owner and exchanges bounded, armed JSON requests with
|
||||
the bridge instead.
|
||||
Self-repair and orchestration therefore exchange bounded, armed requests with
|
||||
one transport-neutral runtime instead of opening the adapter independently.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -80,6 +78,10 @@ class ZcanBridgeSeatExecutor:
|
||||
return value
|
||||
|
||||
def _wait_online(self, *, require_protocol: bool) -> dict[str, Any]:
|
||||
if require_protocol and hasattr(
|
||||
self.bridge_manager, "ensure_seat_protocol"
|
||||
):
|
||||
return self.bridge_manager.ensure_seat_protocol()
|
||||
self.bridge_manager.ensure_async()
|
||||
deadline = time.time() + self.startup_timeout_s
|
||||
while time.time() < deadline:
|
||||
@@ -88,7 +90,7 @@ class ZcanBridgeSeatExecutor:
|
||||
return value
|
||||
time.sleep(0.1)
|
||||
qualifier = "及座椅ECU协议" if require_protocol else ""
|
||||
raise RuntimeError(f"ZCANPRO桥接{qualifier}未在线")
|
||||
raise RuntimeError(f"共享CAN Runtime{qualifier}未在线")
|
||||
|
||||
def _payload(
|
||||
self,
|
||||
@@ -119,25 +121,62 @@ class ZcanBridgeSeatExecutor:
|
||||
status = self._wait_online(require_protocol=True)
|
||||
if status.get("current_request_id"):
|
||||
raise RuntimeError(
|
||||
"ZCANPRO桥接正在执行其他台架任务:%s" %
|
||||
"共享CAN Runtime正在执行其他台架任务:%s" %
|
||||
status.get("current_request_id")
|
||||
)
|
||||
|
||||
if (
|
||||
hasattr(self.bridge_manager, "seat_position_sync_required")
|
||||
and self.bridge_manager.seat_position_sync_required()
|
||||
):
|
||||
sync_id = "SEAT_SYNC_%d_%s" % (
|
||||
int(time.time() * 1000), uuid.uuid4().hex[:6]
|
||||
)
|
||||
sync_result = self._submit_request(
|
||||
sync_id,
|
||||
action="initialize_known_ranges",
|
||||
payload={"force_all_axes": True, "home_nonzero_axes": False},
|
||||
timeout_s=self.request_timeout_s,
|
||||
)
|
||||
if sync_result.get("result") != "PASS":
|
||||
detail = sync_result.get("details") or {}
|
||||
raise RuntimeError(
|
||||
detail.get("summary")
|
||||
or sync_result.get("summary")
|
||||
or "四轴位置同步失败,未执行座椅请求"
|
||||
)
|
||||
self.bridge_manager.mark_seat_position_synchronized()
|
||||
|
||||
request_id = "SEAT_ORCH_%d_%s" % (
|
||||
int(time.time() * 1000), uuid.uuid4().hex[:6]
|
||||
)
|
||||
return self._submit_request(
|
||||
request_id,
|
||||
action="execute_command_plan",
|
||||
payload=payload,
|
||||
timeout_s=self.request_timeout_s,
|
||||
)
|
||||
|
||||
def _submit_request(
|
||||
self,
|
||||
request_id: str,
|
||||
*,
|
||||
action: str,
|
||||
payload: dict[str, Any],
|
||||
timeout_s: float,
|
||||
) -> dict[str, Any]:
|
||||
request = {
|
||||
"schema": "seat-function-request-v1",
|
||||
"request_id": request_id,
|
||||
"created_epoch": time.time(),
|
||||
"armed_until_epoch": time.time() + self.request_timeout_s + 30.0,
|
||||
"armed_until_epoch": time.time() + timeout_s + 30.0,
|
||||
"source": "cabin_cognitive_orchestrator",
|
||||
"action": "execute_command_plan",
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
}
|
||||
_atomic_json(self.pending_dir / f"{request_id}.json", request)
|
||||
|
||||
deadline = time.time() + self.request_timeout_s
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
status = self._online_status(require_protocol=False)
|
||||
if status is None:
|
||||
@@ -147,7 +186,49 @@ class ZcanBridgeSeatExecutor:
|
||||
if result.get("request_id") == request_id:
|
||||
return result
|
||||
time.sleep(0.1)
|
||||
raise RuntimeError("ZCANPRO桥接执行座椅请求超时,已停止等待")
|
||||
raise RuntimeError("共享CAN Runtime执行座椅请求超时,已停止等待")
|
||||
|
||||
@staticmethod
|
||||
def _physical_state_from_detail(detail: dict[str, Any]) -> dict[str, Any] | None:
|
||||
motors = detail.get("motors") or []
|
||||
axes = ("longitudinal", "cushion_tilt", "backrest_recline", "legrest_height")
|
||||
positions: dict[str, Any] = {}
|
||||
missing: list[str] = []
|
||||
for motor, axis in enumerate(axes):
|
||||
item = motors[motor] if motor < len(motors) else None
|
||||
if not isinstance(item, dict):
|
||||
missing.append(axis)
|
||||
continue
|
||||
positions[axis] = {
|
||||
"position_pct": round(int(item.get("position", 0)) / 100),
|
||||
"position_valid": bool(item.get("position_valid")),
|
||||
"motion_state": "moving" if item.get("active") else "stopped",
|
||||
}
|
||||
thermal = detail.get("thermal") or []
|
||||
if not positions and not thermal:
|
||||
return None
|
||||
thermal_byte = int(thermal[0]) if thermal else 0
|
||||
ventilation_raw = thermal_byte & 0x07
|
||||
heating_raw = (thermal_byte >> 3) & 0x07
|
||||
return {
|
||||
"position": positions,
|
||||
"ventilation": {
|
||||
"state": "off" if ventilation_raw <= 1 else "on",
|
||||
"level": max(0, ventilation_raw - 1),
|
||||
},
|
||||
"heating": {
|
||||
"state": "off" if heating_raw <= 1 else "on",
|
||||
"level": max(0, heating_raw - 1),
|
||||
},
|
||||
"captured_at": _now(),
|
||||
"source": "physical_can",
|
||||
"channel": 0,
|
||||
"target": {
|
||||
"device": "seat", "zone": "front_right", "instance": "passenger"
|
||||
},
|
||||
"complete": not missing and bool(thermal),
|
||||
"missing": missing + ([] if thermal else ["thermal"]),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _report(
|
||||
@@ -159,10 +240,15 @@ class ZcanBridgeSeatExecutor:
|
||||
request_id = function_config["metadata"]["request_id"]
|
||||
passed = error is None and (bridge_result or {}).get("result") == "PASS"
|
||||
detail = (bridge_result or {}).get("details") or {}
|
||||
physical_state = (
|
||||
ZcanBridgeSeatExecutor._physical_state_from_detail(detail)
|
||||
if passed
|
||||
else None
|
||||
)
|
||||
message = (
|
||||
detail.get("summary")
|
||||
or (bridge_result or {}).get("summary")
|
||||
or (str(error) if error is not None else "ZCANPRO桥接未返回通过结果")
|
||||
or (str(error) if error is not None else "共享CAN Runtime未返回通过结果")
|
||||
)
|
||||
results = [
|
||||
{
|
||||
@@ -171,9 +257,14 @@ class ZcanBridgeSeatExecutor:
|
||||
"status": "succeeded" if passed else "failed",
|
||||
"message": message,
|
||||
"telemetry": {
|
||||
"transport": "zcanpro_shared_bridge",
|
||||
"transport": "shared_can_runtime",
|
||||
"channel": 0,
|
||||
"hardware_feedback": passed,
|
||||
"simulated": False,
|
||||
"feedback_required": True,
|
||||
"feedback_observed": physical_state is not None,
|
||||
"feedback_match": physical_state is not None,
|
||||
"physical_state": physical_state,
|
||||
"bridge_result": bridge_result,
|
||||
"error_type": type(error).__name__ if error is not None else None,
|
||||
"stop_all_attempted": bool(detail.get("stop_all_attempted")),
|
||||
@@ -181,23 +272,26 @@ class ZcanBridgeSeatExecutor:
|
||||
}
|
||||
for operation in function_config["spec"]["operations"]
|
||||
]
|
||||
spec = {
|
||||
"mode": "hil",
|
||||
"status": "succeeded" if passed else "failed",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
}
|
||||
if physical_state is not None:
|
||||
spec["physical_state"] = physical_state
|
||||
return {
|
||||
"apiVersion": "cognitive-agent/v1alpha1",
|
||||
"kind": "ExecutionReport",
|
||||
"metadata": {
|
||||
"id": f"report-{request_id}",
|
||||
"name": "T2G seat ZCANPRO bridge execution report",
|
||||
"name": "T2G seat shared CAN runtime execution report",
|
||||
"version": "1.0.0",
|
||||
"request_id": request_id,
|
||||
"labels": {"simulation": "false", "feedback": "physical_can"},
|
||||
},
|
||||
"spec": {
|
||||
"mode": "hil",
|
||||
"status": "succeeded" if passed else "failed",
|
||||
"started_at": started_at,
|
||||
"finished_at": _now(),
|
||||
"results": results,
|
||||
},
|
||||
"spec": spec,
|
||||
}
|
||||
|
||||
def execute(
|
||||
|
||||
Reference in New Issue
Block a user