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
@@ -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