feat: complete dual-seat whole-vehicle orchestration

This commit is contained in:
vee
2026-08-12 15:12:40 +08:00
parent 32379a1573
commit 9ce64b3a01
62 changed files with 9741 additions and 3950 deletions
@@ -0,0 +1,596 @@
"""Headless multi-channel CAN runtime for orchestration and self-repair.
One process owns the ZLG USB device and exposes channel-scoped bus views. The
seat protocol engine runs on CH0 in a normal Python thread, while vehicle CAN
executors can use CH1 through the same owner. This removes the ZCANPRO window
and extension-script runtime from the normal deployment path.
"""
from __future__ import annotations
import importlib.util
import json
import logging
import os
from pathlib import Path
import threading
import time
from typing import Any, Callable
from cognitive_agent.infrastructure.can.gateway import (
CanFrame,
ZlgCanBridgeProcess,
ZlgChannelConfig,
)
LOGGER = logging.getLogger(__name__)
class DirectSeatCanTransport:
"""Adapt a shared CH0 bus view to the transport-neutral seat bridge."""
def __init__(
self,
owner: ZlgCanBridgeProcess,
channel: int,
*,
device_type: int,
device_index: int,
stop_event: threading.Event | None = None,
log_callback: Callable[[str], None] | None = None,
) -> None:
self.channel = int(channel)
self._bus = owner.channel_bus(self.channel)
self._stop_event = stop_event
self._log_callback = log_callback
self.bus = {
"backend": "standalone_zlg",
"busID": f"zlg:{device_type}:{device_index}:{self.channel}",
"devType": int(device_type),
"devIndex": int(device_index),
"chnIndex": self.channel,
"frame_format": self._bus.config.frame_format,
"arbitration_bitrate": self._bus.config.arbitration_bitrate,
}
def log(self, value: str) -> None:
text = str(value)
LOGGER.info("seat CAN runtime: %s", text)
if self._log_callback is not None:
self._log_callback(text)
def send(self, can_id: int, data: list[int]) -> None:
self._bus.send(
int(can_id),
bytes(int(item) & 0xFF for item in data),
is_fd=False,
bitrate_switch=False,
)
@staticmethod
def _public_frame(frame: CanFrame) -> dict[str, Any]:
return {
"can_id": int(frame.arbitration_id),
"data": list(frame.data),
"timestamp_us": (
int(float(frame.timestamp) * 1_000_000)
if frame.timestamp is not None
else 0
),
"is_canfd": int(frame.is_fd),
"canfd_brs": int(frame.bitrate_switch),
"channel": frame.channel,
}
def receive(self) -> list[dict[str, Any]]:
if self._stop_event is not None and self._stop_event.is_set():
# Raising here unwinds any long-running bridge workflow through
# its existing exception-safe STOP_ALL/thermal-off path before
# the shared device owner is closed.
raise RuntimeError("standalone CAN runtime is stopping")
# Non-blocking draining is important: CH1 commands share the one driver
# process and must never wait behind a long CH0 receive call.
frames: list[dict[str, Any]] = []
for _ in range(64):
frame = self._bus.receive(0, include_fd=False)
if frame is None:
break
frames.append(self._public_frame(frame))
return frames
class StandaloneCanRuntimeManager:
"""Supervise one headless ZLG owner and the CH0 seat protocol engine."""
def __init__(
self,
status_path: Path,
seat_bridge_script: Path,
*,
required_version: str,
python_executable: Path,
zlg_bridge_script: Path,
dll_path: Path,
device_type: int,
device_index: int,
channels: list[ZlgChannelConfig],
seat_channel: int,
firmware_programmer: Any | None = None,
firmware_auto_flash: bool = False,
expected_firmware_revision: int | None = None,
expected_firmware_build_id: int | None = None,
position_sync_enabled: bool = False,
firmware_boot_wait_s: float = 1.5,
protocol_probe_timeout_s: float = 3.5,
protocol_fresh_s: float = 1.5,
heartbeat_stale_s: float = 3.0,
startup_timeout_s: float = 12.0,
poll_s: float = 0.25,
auto_start: bool = False,
process_factory: Callable[..., ZlgCanBridgeProcess] = ZlgCanBridgeProcess,
) -> None:
if not channels:
raise ValueError("standalone CAN runtime requires at least one channel")
channel_ids = {int(item.channel) for item in channels}
if int(seat_channel) not in channel_ids:
raise ValueError("seat channel must be included in runtime channels")
self.status_path = Path(status_path)
self.script_path = Path(seat_bridge_script).resolve()
self.required_version = str(required_version)
self.python_executable = Path(python_executable)
self.zlg_bridge_script = Path(zlg_bridge_script).resolve()
self.dll_path = Path(dll_path)
self.device_type = int(device_type)
self.device_index = int(device_index)
self.channels = list(channels)
self.seat_channel = int(seat_channel)
self.firmware_programmer = firmware_programmer
self.firmware_auto_flash = bool(firmware_auto_flash)
self.expected_firmware_revision = expected_firmware_revision
self.expected_firmware_build_id = expected_firmware_build_id
self.position_sync_enabled = bool(position_sync_enabled)
self.firmware_boot_wait_s = float(firmware_boot_wait_s)
self.protocol_probe_timeout_s = float(protocol_probe_timeout_s)
self.protocol_fresh_s = float(protocol_fresh_s)
self.heartbeat_stale_s = float(heartbeat_stale_s)
self.startup_timeout_s = float(startup_timeout_s)
self.poll_s = float(poll_s)
self.process_factory = process_factory
self.lock = threading.RLock()
self.firmware_lock = threading.Lock()
self.stop_event = threading.Event()
self.wake_event = threading.Event()
self.runtime_stop_event = threading.Event()
self.owner_ready = threading.Event()
self.thread: threading.Thread | None = None
self.runtime_thread: threading.Thread | None = None
self.owner: ZlgCanBridgeProcess | None = None
self.state = "IDLE"
self.message = "等待前端服务启动"
self.attempts = 0
self.auto_started = False
self.last_attempt_epoch: float | None = None
self.last_online_epoch: float | None = None
self.retry_requested = bool(auto_start)
self.last_runtime_error: str | None = None
self.firmware_recovery_active = False
self.firmware_recovery_count = 0
self.last_firmware_programming: dict[str, Any] | None = None
self._position_sync_required = self.position_sync_enabled
@property
def configured(self) -> bool:
return all(
path.is_file()
for path in (
self.python_executable,
self.zlg_bridge_script,
self.dll_path,
self.script_path,
)
)
def start(self) -> None:
with self.lock:
if self.thread and self.thread.is_alive():
return
self.stop_event.clear()
self.thread = threading.Thread(
target=self._monitor,
name="standalone-cabin-can-monitor",
daemon=True,
)
self.thread.start()
def ensure_async(self) -> None:
self.start()
with self.lock:
if self.owner_ready.is_set():
return
self.retry_requested = True
self.wake_event.set()
def stop(self) -> None:
self.stop_event.set()
self.runtime_stop_event.set()
self.wake_event.set()
runtime_thread = self.runtime_thread
if runtime_thread and runtime_thread is not threading.current_thread():
runtime_thread.join(timeout=10.0)
with self.lock:
owner = self.owner
# Normal shutdown is owned by the protocol thread. Only force-close
# the driver process when that thread did not unwind within the safety
# window (for example, a wedged vendor call).
if owner is not None and runtime_thread and runtime_thread.is_alive():
owner.close()
runtime_thread.join(timeout=2.0)
monitor = self.thread
if monitor and monitor is not threading.current_thread():
monitor.join(timeout=1.0)
def snapshot(self) -> dict[str, Any]:
age = self._heartbeat_age()
with self.lock:
return {
"state": self.state,
"message": self.message,
"backend": "standalone_zlg",
"attempts": self.attempts,
"auto_started": self.auto_started,
"last_attempt_epoch": self.last_attempt_epoch,
"last_online_epoch": self.last_online_epoch,
"heartbeat_age_s": None if age is None else round(age, 2),
"retry_armed": self.retry_requested,
"required_version": self.required_version,
"script_path": str(self.script_path),
"python_executable": str(self.python_executable),
"dll_path": str(self.dll_path),
"device_type": self.device_type,
"device_index": self.device_index,
"seat_channel": self.seat_channel,
"channels": [item.public() for item in self.channels],
"owner_ready": self.owner_ready.is_set(),
"last_error": self.last_runtime_error,
"firmware_auto_flash": self.firmware_auto_flash,
"expected_firmware_revision": self.expected_firmware_revision,
"expected_firmware_build_id": self.expected_firmware_build_id,
"firmware_recovery_active": self.firmware_recovery_active,
"firmware_recovery_count": self.firmware_recovery_count,
"last_firmware_programming": self.last_firmware_programming,
"position_sync_enabled": self.position_sync_enabled,
"position_sync_required": self._position_sync_required,
# Compatibility fields consumed by the existing status UI.
"zcan_exe": None,
"script_config_path": None,
"zcan_window_found": False,
}
def channel_bus(self, channel: int, timeout_s: float | None = None):
channel = int(channel)
if channel not in {item.channel for item in self.channels}:
raise RuntimeError(f"CAN channel {channel} is not configured")
self.ensure_async()
deadline = time.time() + float(timeout_s or self.startup_timeout_s)
while time.time() < deadline:
if self.owner_ready.wait(timeout=0.05):
with self.lock:
owner = self.owner
if owner is not None:
return owner.channel_bus(channel)
with self.lock:
if self.state == "ERROR" and not self.retry_requested:
break
raise RuntimeError(
"独立CAN Runtime未就绪:%s" % (self.last_runtime_error or self.message)
)
def channel_bus_factory(self, channel: int):
return lambda: self.channel_bus(channel)
def seat_position_sync_required(self) -> bool:
with self.lock:
return self.position_sync_enabled and self._position_sync_required
def mark_seat_position_synchronized(self) -> None:
with self.lock:
self._position_sync_required = False
def _read_status(self) -> dict[str, Any] | None:
try:
value = json.loads(self.status_path.read_text(encoding="utf-8"))
except (OSError, ValueError, TypeError):
return None
return value if isinstance(value, dict) else None
def _protocol_status(
self,
) -> tuple[bool, str, dict[str, Any] | None]:
status = self._read_status()
if status is None:
return False, "CAN Runtime尚未发布状态", None
now = time.time()
try:
heartbeat_age = now - float(status.get("heartbeat_epoch", 0) or 0)
except (TypeError, ValueError):
heartbeat_age = float("inf")
if heartbeat_age < 0 or heartbeat_age > self.heartbeat_stale_s:
return False, "CAN Runtime心跳已过期", status
frame_epochs = status.get("frame_epochs") or {}
required_frames = (
"0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307"
)
stale_frames = []
for frame_id in required_frames:
try:
age = now - float(frame_epochs.get(frame_id, 0) or 0)
except (TypeError, ValueError):
age = float("inf")
if age < 0 or age > self.protocol_fresh_s:
stale_frames.append(frame_id)
if stale_frames:
return (
False,
"座椅ECU报文缺失或过期:" + ",".join(stale_frames),
status,
)
system = status.get("system") or {}
revision = system.get("firmware_revision")
if (
self.expected_firmware_revision is not None
and revision != self.expected_firmware_revision
):
return (
False,
"座椅固件revision不匹配:%s != %s"
% (revision, self.expected_firmware_revision),
status,
)
identity = status.get("firmware_identity") or {}
build_id = identity.get("firmware_build_id")
if (
self.expected_firmware_build_id is not None
and (
build_id != self.expected_firmware_build_id
or not identity.get("qualified")
)
):
return (
False,
"座椅固件build不匹配:%s != %s"
% (build_id, self.expected_firmware_build_id),
status,
)
return True, "座椅ECU协议与仓库固件身份已确认", status
def _stop_runtime_owner(self) -> None:
self.runtime_stop_event.set()
runtime_thread = self.runtime_thread
if runtime_thread and runtime_thread is not threading.current_thread():
runtime_thread.join(timeout=10.0)
with self.lock:
owner = self.owner
if owner is not None:
owner.close()
if runtime_thread and runtime_thread.is_alive():
runtime_thread.join(timeout=2.0)
self.owner_ready.clear()
def ensure_seat_protocol(self) -> dict[str, Any]:
"""Return only fresh, repository-qualified seat protocol feedback.
Standalone motor scripts intentionally replace the ECU image. When
automatic programming is enabled, release the USB CAN owner, restore
the repository image through CMSIS-DAP, and reopen the shared runtime
before accepting an orchestration request.
"""
self.ensure_async()
deadline = time.time() + self.protocol_probe_timeout_s
reason = "座椅ECU协议尚未就绪"
while time.time() < deadline:
ready, reason, status = self._protocol_status()
if ready and status is not None:
return status
time.sleep(0.1)
if not self.firmware_auto_flash or self.firmware_programmer is None:
raise RuntimeError(reason + ";自动恢复仓库固件未启用")
with self.firmware_lock:
ready, reason, status = self._protocol_status()
if ready and status is not None:
return status
if status and status.get("current_request_id"):
raise RuntimeError(
"CAN Runtime仍有活动任务,不能恢复固件:%s"
% status.get("current_request_id")
)
with self.lock:
self.firmware_recovery_active = True
self.state = "FIRMWARE_RECOVERY"
self.message = reason + ";正在自动恢复仓库固件"
try:
self._stop_runtime_owner()
programming = dict(self.firmware_programmer.program())
with self.lock:
self.firmware_recovery_count += 1
self.last_firmware_programming = programming
if self.position_sync_enabled:
self._position_sync_required = True
time.sleep(self.firmware_boot_wait_s)
if self.stop_event.is_set():
raise RuntimeError("CAN Runtime已停止,固件恢复后不再重启")
self._attempt_start()
verify_deadline = time.time() + self.startup_timeout_s
while time.time() < verify_deadline:
ready, reason, status = self._protocol_status()
if ready and status is not None:
return status
time.sleep(0.1)
raise RuntimeError("仓库固件已烧录但协议校验失败:" + reason)
except Exception as exc:
self._set_state(
"ERROR",
"座椅仓库固件自动恢复失败:%s" % exc,
last_runtime_error=str(exc),
)
raise
finally:
with self.lock:
self.firmware_recovery_active = False
def _set_state(self, state: str, message: str, **updates: Any) -> None:
with self.lock:
self.state = state
self.message = message
for key, value in updates.items():
setattr(self, key, value)
def _heartbeat_age(self) -> float | None:
try:
value = self._read_status() or {}
heartbeat = float(value.get("heartbeat_epoch", 0) or 0)
if heartbeat <= 0:
return None
return max(0.0, time.time() - heartbeat)
except (OSError, ValueError, TypeError):
return None
def _heartbeat_online(self) -> bool:
age = self._heartbeat_age()
if age is None or age > self.heartbeat_stale_s:
return False
try:
value = self._read_status() or {}
return value.get("bridge_version") == self.required_version
except (OSError, ValueError, TypeError):
return False
def _take_retry_request(self) -> bool:
with self.lock:
if not self.retry_requested:
return False
self.retry_requested = False
return True
def _monitor(self) -> None:
while not self.stop_event.is_set():
if self.firmware_recovery_active:
self.wake_event.wait(self.poll_s)
self.wake_event.clear()
continue
runtime_alive = bool(
self.runtime_thread and self.runtime_thread.is_alive()
)
if runtime_alive and self._heartbeat_online():
self._set_state(
"ONLINE",
"双通道CAN Runtime在线",
last_online_epoch=time.time(),
last_runtime_error=None,
)
elif runtime_alive:
self._set_state("STARTING", "CAN Runtime已启动,等待桥接心跳")
elif self._take_retry_request():
self._attempt_start()
elif self.state not in {"ERROR", "STOPPED"}:
self._set_state("WAITING", "CAN Runtime未启动,等待硬件请求")
self.wake_event.wait(self.poll_s)
self.wake_event.clear()
def _validate_paths(self) -> None:
missing = [
str(path)
for path in (
self.python_executable,
self.zlg_bridge_script,
self.dll_path,
self.script_path,
)
if not path.is_file()
]
if missing:
raise RuntimeError("CAN Runtime依赖不存在:" + ", ".join(missing))
def _attempt_start(self) -> None:
self._set_state("STARTING", "正在启动独立双通道CAN Runtime")
with self.lock:
self.attempts += 1
self.last_attempt_epoch = time.time()
try:
self._validate_paths()
owner = self.process_factory(
python_executable=self.python_executable,
bridge_script=self.zlg_bridge_script,
dll_path=self.dll_path,
device_type=self.device_type,
device_index=self.device_index,
channels=self.channels,
)
self.runtime_stop_event.clear()
with self.lock:
self.owner = owner
self.last_runtime_error = None
self.runtime_thread = threading.Thread(
target=self._run_protocol_engine,
args=(owner,),
name="standalone-seat-protocol-runtime",
daemon=True,
)
self.runtime_thread.start()
if self.runtime_thread.is_alive():
self.owner_ready.set()
self.auto_started = True
except Exception as exc: # noqa: BLE001 - normalize vendor/runtime errors
self.owner_ready.clear()
self._set_state(
"ERROR",
"独立CAN Runtime启动失败:%s" % exc,
last_runtime_error=str(exc),
)
def _load_bridge_module(self):
module_name = "_cognitive_agent_standalone_seat_bridge"
spec = importlib.util.spec_from_file_location(module_name, self.script_path)
if spec is None or spec.loader is None:
raise RuntimeError("无法加载座椅协议桥接模块")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if getattr(module, "BRIDGE_VERSION", None) != self.required_version:
raise RuntimeError(
"桥接版本不匹配:%s" % getattr(module, "BRIDGE_VERSION", None)
)
return module
def _run_protocol_engine(self, owner: ZlgCanBridgeProcess) -> None:
try:
module = self._load_bridge_module()
transport = DirectSeatCanTransport(
owner,
self.seat_channel,
device_type=self.device_type,
device_index=self.device_index,
stop_event=self.runtime_stop_event,
)
module.run_bridge(transport=transport, stop_event=self.runtime_stop_event)
except Exception as exc: # noqa: BLE001 - keep manager observable
LOGGER.exception("standalone seat protocol runtime failed")
self._set_state(
"ERROR",
"独立座椅协议Runtime异常:%s" % exc,
last_runtime_error=str(exc),
)
finally:
self.owner_ready.clear()
owner.close()
with self.lock:
if self.owner is owner:
self.owner = None
if self.stop_event.is_set():
self.state = "STOPPED"
self.message = "CAN Runtime已停止"
@@ -30,7 +30,14 @@ from .signal_diagnostic import (
from .zcan_bridge import (
ZcanBridgeManager, run_self_test as run_bridge_manager_self_test,
)
from .can_runtime import StandaloneCanRuntimeManager
from cognitive_agent.infrastructure.can.gateway import ZlgChannelConfig
from cognitive_agent.infrastructure.can.hardware import (
SeatHardwareSettings,
VehicleCanSettings,
)
from cognitive_agent.infrastructure.firmware.t2g_seat import (
EXPECTED_FIRMWARE_BUILD_ID,
EXPECTED_FIRMWARE_REVISION,
)
@@ -123,7 +130,7 @@ FIRMWARE_LOG_ROOT = _env_path(
FIRMWARE_SOURCE_ROOTS = tuple(
PROJECT_DIR / name for name in ("App", "Bsp", "Config", "Driver"))
FIRMWARE_SOURCE_SUFFIXES = frozenset((".c", ".h", ".s", ".asm"))
REQUIRED_BRIDGE_VERSION = "2026-08-09-real-self-heal-v32"
REQUIRED_BRIDGE_VERSION = "2026-08-10-legrest-runtime-v36"
ZCANPRO_ROOTS = (
Path(r"C:\Program Files (x86)\ZCANPRO"),
Path(r"C:\Program Files\ZCANPRO"),
@@ -136,13 +143,76 @@ ZCANPRO_SCRIPT_CONFIG = _env_or_installed_path(
"SELF_REPAIR_ZCANPRO_SCRIPT_CONFIG",
tuple(root / "config" / "extend_script.ini" for root in ZCANPRO_ROOTS),
REPOSITORY_ROOT / "__unconfigured_zcanpro_script_config__")
BRIDGE_MANAGER = ZcanBridgeManager(
STATUS_PATH, BRIDGE_SCRIPT_PATH,
required_version=REQUIRED_BRIDGE_VERSION,
zcanpro_exe=ZCANPRO_EXE,
script_config_path=ZCANPRO_SCRIPT_CONFIG,
auto_start=_env_flag("SELF_REPAIR_AUTO_START_BRIDGE", False),
)
CAN_RUNTIME_BACKEND = os.getenv(
"COGNITIVE_AGENT_CAN_RUNTIME_BACKEND", "standalone-zlg"
).strip().lower()
def _build_bridge_manager():
auto_start = _env_flag("SELF_REPAIR_AUTO_START_BRIDGE", False)
if CAN_RUNTIME_BACKEND == "zcanpro":
return ZcanBridgeManager(
STATUS_PATH, BRIDGE_SCRIPT_PATH,
required_version=REQUIRED_BRIDGE_VERSION,
zcanpro_exe=ZCANPRO_EXE,
script_config_path=ZCANPRO_SCRIPT_CONFIG,
auto_start=auto_start,
)
if CAN_RUNTIME_BACKEND != "standalone-zlg":
raise ValueError(
"COGNITIVE_AGENT_CAN_RUNTIME_BACKEND must be standalone-zlg or zcanpro"
)
seat = SeatHardwareSettings.from_env(REPOSITORY_ROOT)
vehicle = VehicleCanSettings.from_env(REPOSITORY_ROOT)
seat_channel = int(seat.channel, 0)
channels = [
ZlgChannelConfig(
channel=seat_channel,
frame_format="classic_can",
arbitration_bitrate=seat.bitrate,
data_bitrate=vehicle.data_bitrate,
can_fd_iso=True,
termination_enabled=seat.termination_enabled,
)
]
if vehicle.enabled:
if vehicle.channel == seat_channel:
raise ValueError("seat CH0 and vehicle CH1 must use different channels")
if (
vehicle.device_type != seat.device_type
or vehicle.device_index != seat.device_index
or vehicle.zlg_python != seat.zlg_python
or vehicle.zlg_dll != seat.zlg_dll
):
raise ValueError(
"seat and vehicle channels must share one configured ZLG device runtime"
)
channels.append(vehicle.channel_config())
return StandaloneCanRuntimeManager(
STATUS_PATH,
BRIDGE_SCRIPT_PATH,
required_version=REQUIRED_BRIDGE_VERSION,
python_executable=seat.zlg_python,
zlg_bridge_script=seat.bridge_script,
dll_path=seat.zlg_dll,
device_type=seat.device_type,
device_index=seat.device_index,
channels=channels,
seat_channel=seat_channel,
firmware_programmer=(
seat._programmer() if seat.auto_flash_enabled else None
),
firmware_auto_flash=seat.auto_flash_enabled,
expected_firmware_revision=EXPECTED_FIRMWARE_REVISION,
expected_firmware_build_id=EXPECTED_FIRMWARE_BUILD_ID,
position_sync_enabled=seat.position_sync_enabled,
firmware_boot_wait_s=seat.firmware_boot_wait_ms / 1000.0,
auto_start=auto_start,
)
BRIDGE_MANAGER = _build_bridge_manager()
def _read_json(path):
@@ -278,14 +348,14 @@ class ConsoleState(object):
live bridge without fresh 0x301~0x305, a missing local image, or local
firmware sources newer than that image are treated as recoverable
firmware-qualification problems. A missing bridge is not: flashing
cannot repair a closed adapter or a stopped ZCANPRO script.
cannot repair a closed adapter or a stopped CAN runtime.
"""
bridge = bridge or {}
if not bridge.get("online"):
return {
"ready": False, "auto_flash": False,
"reason_code": "BRIDGE_OFFLINE",
"detail": "ZCANPRO桥接未在线;该问题不能通过烧录修复",
"detail": "CAN Runtime未在线;该问题不能通过烧录修复",
}
if not hex_exists:
reason = "FIRMWARE_IMAGE_MISSING"
@@ -308,6 +378,13 @@ class ConsoleState(object):
"需要自动重新构建并烧录" %
(bridge.get("firmware_revision", "未知"),
EXPECTED_FIRMWARE_REVISION))
elif not bridge.get("firmware_build_id_ok"):
reason = "FIRMWARE_BUILD_MISMATCH"
detail = (
"Seat_Controller_demo firmware build=%s, expected build=%s; "
"the repository image must be restored" %
(bridge.get("firmware_build_id", "unknown"),
EXPECTED_FIRMWARE_BUILD_ID))
else:
return {
"ready": True, "auto_flash": False,
@@ -375,7 +452,8 @@ class ConsoleState(object):
if (last_bridge.get("online") and
(not require_protocol or
(last_bridge.get("protocol_online") and
last_bridge.get("firmware_revision_ok")))):
last_bridge.get("firmware_revision_ok") and
last_bridge.get("firmware_build_id_ok")))):
return last_status, last_bridge
time.sleep(0.25)
return last_status, last_bridge or {}
@@ -427,7 +505,8 @@ class ConsoleState(object):
status, bridge = self._wait_bridge(require_protocol=True,
timeout_s=12.0)
if (not bridge.get("protocol_online") or
not bridge.get("firmware_revision_ok")):
not bridge.get("firmware_revision_ok") or
not bridge.get("firmware_build_id_ok")):
detail = ("自动烧录已执行,但仍未收到0x301~0x305;"
"硬件诊断未启动,请检查ECU供电或烧录日志")
with self.lock:
@@ -473,6 +552,9 @@ class ConsoleState(object):
system = {} if not isinstance(status, dict) else (
status.get("system") or {})
firmware_revision = system.get("firmware_revision")
firmware_identity = {} if not isinstance(status, dict) else (
status.get("firmware_identity") or {})
firmware_build_id = firmware_identity.get("firmware_build_id")
frame_epochs = {} if not isinstance(status, dict) else (
status.get("frame_epochs") or {})
recent_frame_ids = []
@@ -485,12 +567,12 @@ class ConsoleState(object):
except (TypeError, ValueError):
continue
recent_frame_ids.sort()
protocol_epoch = frame_epochs.get("0x301")
try:
protocol_online = (online and protocol_epoch is not None and
max(0.0, now - float(protocol_epoch)) <= 0.8)
except (TypeError, ValueError):
protocol_online = False
protocol_required = {
"0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307"
}
protocol_online = online and protocol_required.issubset(
set(recent_frame_ids)
)
return {
"online": online,
"age_s": None if age is None else round(age, 2),
@@ -507,6 +589,12 @@ class ConsoleState(object):
"expected_firmware_revision": EXPECTED_FIRMWARE_REVISION,
"firmware_revision_ok":
firmware_revision == EXPECTED_FIRMWARE_REVISION,
"firmware_build_id": firmware_build_id,
"expected_firmware_build_id": EXPECTED_FIRMWARE_BUILD_ID,
"firmware_build_id_ok": (
firmware_build_id == EXPECTED_FIRMWARE_BUILD_ID
and bool(firmware_identity.get("qualified"))
),
"active_mask": int(system.get("active_mask", 0)),
"faulted_mask": int(system.get("faulted_mask", 0)),
"bus": {} if not isinstance(status, dict) else (
@@ -514,7 +602,9 @@ class ConsoleState(object):
"bus_traffic": bool(recent_frame_ids),
"protocol_online": protocol_online,
"recent_frame_ids": recent_frame_ids,
"expected_frame_ids": ["0x301", "0x302", "0x303", "0x304", "0x305"],
"expected_frame_ids": [
"0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307"
],
"frame_epochs": frame_epochs,
}
@@ -911,7 +1001,7 @@ class ConsoleState(object):
require_protocol=True, timeout_s=12.0)
now = time.time()
if not bridge.get("online"):
raise ValueError("ZCANPRO桥接未在线,不能向实物ECU写入故障")
raise ValueError("CAN Runtime未在线,不能向实物ECU写入故障")
if not bridge.get("protocol_online"):
raise ValueError("未识别到本工程座椅ECU协议,不能执行台架故障注入")
self._sync_bench_reset(status, now)
@@ -1133,7 +1223,7 @@ class ConsoleState(object):
bridge = self._bridge_summary(status, now)
if not bridge.get("online"):
raise ValueError(
"固件资格检查后ZCANPRO桥接离线,未启动21针主动检测")
"固件资格检查后CAN Runtime离线,未启动21针主动检测")
if not bridge.get("protocol_online"):
raise ValueError(
"固件资格检查后仍未识别0x301~0x305,未启动21针主动检测")
@@ -1451,6 +1541,7 @@ class SelfRepairService(object):
"diagnostic_config": str(CONFIG_PATH),
"bridge_status_path": str(STATUS_PATH),
"bridge_script_path": str(BRIDGE_SCRIPT_PATH),
"can_runtime_backend": CAN_RUNTIME_BACKEND,
"zcanpro_exe": str(ZCANPRO_EXE),
"firmware_root": str(PROJECT_DIR),
"iar_build_path": str(IAR_BUILD_PATH),
@@ -1458,6 +1549,15 @@ class SelfRepairService(object):
"bridge_script_available": BRIDGE_SCRIPT_PATH.is_file(),
"diagnostic_config_available": CONFIG_PATH.is_file(),
"zcanpro_configured": ZCANPRO_EXE.is_file(),
"standalone_can_runtime_configured": bool(
CAN_RUNTIME_BACKEND == "standalone-zlg"
and getattr(BRIDGE_MANAGER, "configured", False)
),
"can_runtime_configured": bool(
getattr(BRIDGE_MANAGER, "configured", False)
if CAN_RUNTIME_BACKEND == "standalone-zlg"
else ZCANPRO_EXE.is_file()
),
"firmware_root_configured": firmware_root_configured,
"firmware_tools_configured": (
firmware_root_configured
@@ -386,7 +386,7 @@ class SignalDiagnosticModel(object):
now = time.time() if now is None else float(now)
if not isinstance(status, dict):
result = self._result(self._offline_groups(
STATE_UNKNOWN, "未发现ZCANPRO桥接状态",
STATE_UNKNOWN, "未发现CAN Runtime状态",
["bridge_status.json不存在或无法读取"]), now)
result["link"] = {
"bridge": STATE_UNKNOWN, "adapter": STATE_UNKNOWN,
@@ -400,7 +400,7 @@ class SignalDiagnosticModel(object):
bridge_age = now - heartbeat
if heartbeat <= 0 or bridge_age > self.config["bridge_stale_s"]:
result = self._result(self._offline_groups(
STATE_UNKNOWN, "ZCANPRO桥接离线,不能判断线束通断",
STATE_UNKNOWN, "CAN Runtime离线,不能判断线束通断",
["桥接心跳年龄 %.1f s" % max(0.0, bridge_age)]), now)
result["link"] = {
"bridge": STATE_UNKNOWN, "adapter": STATE_UNKNOWN,
@@ -418,7 +418,7 @@ class SignalDiagnosticModel(object):
"0x301已中断 %.1f s" % can_age
if recent_frames:
evidence = [
"ZCANPRO桥接心跳正常,CAN设备与通道已打开",
"CAN Runtime心跳正常,CAN设备与通道已打开",
"总线实时帧: %s" % ", ".join(recent_ids),
"%s;未识别本工程座椅ECU协议0x301~0x305" % age_text,
]
@@ -429,7 +429,7 @@ class SignalDiagnosticModel(object):
can_pin_state=STATE_OK,
can_actions=[
"确认当前开发板固件的CAN报文ID是否仍为0x301~0x305",
"确认ZCANPRO监听的是座椅ECU所在网络/通道及正确波特率",
"确认CAN Runtime监听的是座椅ECU所在网络/通道及正确波特率",
])
result = self._result(groups, now)
result["link"] = {
@@ -447,7 +447,7 @@ class SignalDiagnosticModel(object):
self.config["bus_activity_stale_s"]],
can_pin_state=STATE_UNKNOWN,
can_actions=[
"确认ZCANPRO通道、波特率和终端配置",
"确认CAN Runtime通道、波特率和终端配置",
"确认座椅ECU已上电并处于当前CAN网络;尚不能据此判定CANH/CANL损坏",
])
result = self._result(groups, now)