1636 lines
72 KiB
Python
1636 lines
72 KiB
Python
"""Seat Controller self-repair application service.
|
||
|
||
The original HIL demo mixed HTTP hosting, business state, firmware tooling and
|
||
ZCANPRO process control in one script. This module keeps the proven state
|
||
machine and diagnostic rules while exposing them through the repository's
|
||
existing server. Every machine-specific path is supplied through environment
|
||
configuration or discovered standard installations; importing the module never
|
||
launches hardware tooling.
|
||
"""
|
||
|
||
from __future__ import unicode_literals
|
||
|
||
import json
|
||
import os
|
||
from pathlib import Path
|
||
import hashlib
|
||
import subprocess
|
||
import threading
|
||
import time
|
||
|
||
from .healing_model import (
|
||
PHASE_FAILED, PHASE_PASSED, SCENARIO_BY_ID, SelfHealingEngine,
|
||
public_scenarios,
|
||
run_self_test as run_healing_self_test,
|
||
)
|
||
from .signal_diagnostic import (
|
||
SignalDiagnosticModel, load_config,
|
||
run_self_test as run_signal_self_test,
|
||
)
|
||
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,
|
||
)
|
||
|
||
|
||
REPOSITORY_ROOT = Path(__file__).resolve().parents[4]
|
||
|
||
|
||
def _env_path(name, default):
|
||
value = os.getenv(name, "").strip()
|
||
return Path(value).expanduser() if value else Path(default)
|
||
|
||
|
||
def _env_or_installed_path(name, candidates, fallback):
|
||
value = os.getenv(name, "").strip()
|
||
if value:
|
||
return Path(value).expanduser()
|
||
for candidate in candidates:
|
||
path = Path(candidate)
|
||
if path.exists():
|
||
return path
|
||
return Path(fallback)
|
||
|
||
|
||
def _env_flag(name, default=False):
|
||
value = os.getenv(name)
|
||
if value is None:
|
||
return bool(default)
|
||
return value.strip().lower() in {"1", "true", "yes", "on"}
|
||
|
||
|
||
RUNTIME_DIR = _env_path(
|
||
"SELF_REPAIR_RUNTIME_DIR",
|
||
REPOSITORY_ROOT / "artifacts" / "tmp" / "self-repair" / "runtime",
|
||
)
|
||
PENDING_DIR = RUNTIME_DIR / "requests" / "pending"
|
||
STATUS_PATH = _env_path(
|
||
"SELF_REPAIR_BRIDGE_STATUS_PATH", RUNTIME_DIR / "bridge_status.json")
|
||
BRIDGE_SCRIPT_PATH = _env_path(
|
||
"SELF_REPAIR_BRIDGE_SCRIPT_PATH",
|
||
REPOSITORY_ROOT / "tools" / "zcan" / "seat_self_repair_bridge.py",
|
||
)
|
||
EMERGENCY_PATH = RUNTIME_DIR / "emergency_stop.json"
|
||
CONFIG_PATH = _env_path(
|
||
"SELF_REPAIR_DIAGNOSTIC_CONFIG",
|
||
REPOSITORY_ROOT / "config" / "self-repair" / "signal_diagnostic_config.json",
|
||
)
|
||
LOG_ROOT = _env_path(
|
||
"SELF_REPAIR_LOG_DIR",
|
||
REPOSITORY_ROOT / "artifacts" / "self-repair" / "software",
|
||
)
|
||
HARDWARE_LOG_ROOT = _env_path(
|
||
"SELF_REPAIR_HARDWARE_LOG_DIR",
|
||
REPOSITORY_ROOT / "artifacts" / "self-repair" / "hardware",
|
||
)
|
||
PROJECT_DIR = _env_path(
|
||
"SELF_REPAIR_FIRMWARE_ROOT",
|
||
REPOSITORY_ROOT / "firmware" / "t2g" / "Seat_Controller_demo")
|
||
IAR_PROJECT_PATH = PROJECT_DIR / "IAR" / "Seat_Controller_demo.ewp"
|
||
FIRMWARE_OUT_PATH = PROJECT_DIR / "IAR" / "rev_d" / "Exe" / \
|
||
"Seat_Controller_demo.out"
|
||
FIRMWARE_HEX_PATH = PROJECT_DIR / "IAR" / "rev_d" / "Exe" / \
|
||
"Seat_Controller_demo.hex"
|
||
IAR_INSTALL_ROOTS = (
|
||
Path(r"D:\Program Files\IAR Systems\Embedded Workbench 9.2"),
|
||
Path(r"C:\Program Files\IAR Systems\Embedded Workbench 9.2"),
|
||
Path(r"C:\Program Files\IAR Systems\Embedded Workbench 9.4"),
|
||
)
|
||
IAR_BUILD_PATH = _env_or_installed_path(
|
||
"SELF_REPAIR_IAR_BUILD_PATH",
|
||
tuple(root / "common" / "bin" / "iarbuild.exe"
|
||
for root in IAR_INSTALL_ROOTS),
|
||
REPOSITORY_ROOT / "__unconfigured_iar_build__")
|
||
CSPY_BAT_PATH = _env_or_installed_path(
|
||
"SELF_REPAIR_CSPY_BAT_PATH",
|
||
tuple(root / "common" / "bin" / "CSpyBat.exe"
|
||
for root in IAR_INSTALL_ROOTS),
|
||
REPOSITORY_ROOT / "__unconfigured_cspy__")
|
||
CSPY_GENERAL_PATH = _env_path(
|
||
"SELF_REPAIR_CSPY_GENERAL_PATH",
|
||
PROJECT_DIR / "IAR" / "settings" / "Seat_Controller_demo.rev_d.general.xcl",
|
||
)
|
||
CSPY_DRIVER_PATH = _env_path(
|
||
"SELF_REPAIR_CSPY_DRIVER_PATH",
|
||
PROJECT_DIR / "IAR" / "settings" / "Seat_Controller_demo.rev_d.driver.xcl",
|
||
)
|
||
FIRMWARE_LOG_ROOT = _env_path(
|
||
"SELF_REPAIR_FIRMWARE_LOG_DIR",
|
||
REPOSITORY_ROOT / "artifacts" / "self-repair" / "firmware",
|
||
)
|
||
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-10-legrest-runtime-v36"
|
||
ZCANPRO_ROOTS = (
|
||
Path(r"C:\Program Files (x86)\ZCANPRO"),
|
||
Path(r"C:\Program Files\ZCANPRO"),
|
||
)
|
||
ZCANPRO_EXE = _env_or_installed_path(
|
||
"SELF_REPAIR_ZCANPRO_EXE",
|
||
tuple(root / "ZCANPRO.exe" for root in ZCANPRO_ROOTS),
|
||
REPOSITORY_ROOT / "__unconfigured_zcanpro__")
|
||
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__")
|
||
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):
|
||
try:
|
||
with path.open("r", encoding="utf-8") as stream:
|
||
return json.load(stream)
|
||
except (OSError, ValueError, TypeError):
|
||
return None
|
||
|
||
|
||
def _atomic_json(path, value):
|
||
path.parent.mkdir(parents=True, exist_ok=True)
|
||
temporary = path.with_name(
|
||
"%s.%s.%s.%s.tmp" %
|
||
(path.name, os.getpid(), threading.get_ident(), time.time_ns()))
|
||
try:
|
||
with temporary.open("w", encoding="utf-8") as stream:
|
||
json.dump(value, stream, ensure_ascii=False, indent=2,
|
||
sort_keys=True)
|
||
stream.flush()
|
||
os.fsync(stream.fileno())
|
||
for attempt in range(8):
|
||
try:
|
||
os.replace(str(temporary), str(path))
|
||
return
|
||
except PermissionError:
|
||
if attempt >= 7:
|
||
raise
|
||
# Windows may briefly deny replacement while the ZCAN bridge
|
||
# or an HTTP polling thread is closing the previous file.
|
||
time.sleep(0.01 * (attempt + 1))
|
||
finally:
|
||
try:
|
||
temporary.unlink()
|
||
except OSError:
|
||
pass
|
||
|
||
|
||
class ConsoleState(object):
|
||
def __init__(self, bridge_manager):
|
||
self.lock = threading.RLock()
|
||
self.bridge_manager = bridge_manager
|
||
self.engine = SelfHealingEngine()
|
||
self.signal_model = SignalDiagnosticModel(load_config(CONFIG_PATH))
|
||
self.latest_report_path = None
|
||
self.batch = self._new_batch_state()
|
||
self.batch_thread = None
|
||
self.hardware_active = False
|
||
self.hardware_run_id = None
|
||
self.hardware_started_epoch = None
|
||
self.hardware_events = []
|
||
self.hardware_signatures = {}
|
||
self.hardware_result = self.signal_model.analyze(None)
|
||
self.hardware_report_path = None
|
||
self.hardware_self_test = None
|
||
self.hardware_request_id = None
|
||
self.hardware_prepare = {
|
||
"state": "IDLE",
|
||
"detail": "等待启动;诊断前将自动检查并按需烧录Seat_Controller_demo固件",
|
||
"automatic_flash": False,
|
||
"reason_code": None,
|
||
}
|
||
self.bench_request_id = None
|
||
self.bench_reset = {
|
||
"phase": "IDLE", "request_id": None,
|
||
"started_epoch": None, "finished_epoch": None,
|
||
"summary": "实物用例启动前须点击重置并完成四轴归零",
|
||
"details": None, "axes": [], "zero_count": 0,
|
||
"all_zero": False,
|
||
}
|
||
self.firmware_operation = {
|
||
"state": "IDLE", "message": "尚未执行固件烧录",
|
||
"hex_path": str(FIRMWARE_HEX_PATH), "probe_checked_epoch": 0.0,
|
||
}
|
||
self.cspy_process = None
|
||
|
||
@staticmethod
|
||
def _run_hidden(command, timeout_s):
|
||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||
return subprocess.run(
|
||
[str(item) for item in command], cwd=str(PROJECT_DIR),
|
||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||
text=True, encoding="utf-8", errors="replace",
|
||
timeout=timeout_s, creationflags=creationflags, check=False)
|
||
|
||
@classmethod
|
||
def _programmer_status(cls):
|
||
script = (
|
||
"$items=Get-PnpDevice -PresentOnly -ErrorAction SilentlyContinue | "
|
||
"Where-Object {$_.FriendlyName -match "
|
||
"'CMSIS-DAP|DAPLink|fireDAP|KitProg|MiniProg|J-Link' "
|
||
"-or $_.InstanceId -match "
|
||
"'CMSIS|CYPRESS|INFINEON|SEGGER|VID_1FC9&PID_5601'}; "
|
||
"$items | ForEach-Object {"
|
||
"$bus=(Get-PnpDeviceProperty -InstanceId $_.InstanceId "
|
||
"-KeyName DEVPKEY_Device_BusReportedDeviceDesc "
|
||
"-ErrorAction SilentlyContinue).Data; "
|
||
"$name=if($bus){$bus}else{$_.FriendlyName}; "
|
||
"if($name -match "
|
||
"'CMSIS-DAP|DAPLink|fireDAP CMSIS-DAP|KitProg|MiniProg|J-Link'){"
|
||
"$_.Status + '|' + $name + '|' + $_.InstanceId}}")
|
||
try:
|
||
result = cls._run_hidden(
|
||
["powershell", "-NoProfile", "-Command", script], 10)
|
||
devices = [line.strip() for line in result.stdout.splitlines()
|
||
if line.strip()]
|
||
except (OSError, subprocess.SubprocessError):
|
||
devices = []
|
||
return {"detected": bool(devices), "devices": devices,
|
||
"required": "CMSIS-DAP"}
|
||
|
||
def _firmware_snapshot(self):
|
||
now = time.time()
|
||
if now - float(self.firmware_operation.get(
|
||
"probe_checked_epoch", 0.0)) > 5.0:
|
||
self.firmware_operation["programmer"] = \
|
||
self._programmer_status()
|
||
self.firmware_operation["probe_checked_epoch"] = now
|
||
value = dict(self.firmware_operation)
|
||
if FIRMWARE_HEX_PATH.exists():
|
||
value["hex_sha256"] = hashlib.sha256(
|
||
FIRMWARE_HEX_PATH.read_bytes()).hexdigest()
|
||
value["hex_mtime"] = FIRMWARE_HEX_PATH.stat().st_mtime
|
||
return value
|
||
|
||
@staticmethod
|
||
def _classify_firmware_issue(bridge, hex_exists, source_newer,
|
||
probe_detected):
|
||
"""Classify only failures that can reasonably be fixed by flashing.
|
||
|
||
CAN traffic alone proves the adapter and physical pair are alive; it
|
||
does not prove that the connected ECU runs Seat_Controller_demo. A
|
||
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 CAN runtime.
|
||
"""
|
||
bridge = bridge or {}
|
||
if not bridge.get("online"):
|
||
return {
|
||
"ready": False, "auto_flash": False,
|
||
"reason_code": "BRIDGE_OFFLINE",
|
||
"detail": "CAN Runtime未在线;该问题不能通过烧录修复",
|
||
}
|
||
if not hex_exists:
|
||
reason = "FIRMWARE_IMAGE_MISSING"
|
||
detail = "未找到Seat_Controller_demo固件镜像,需要自动构建并烧录"
|
||
elif source_newer:
|
||
reason = "FIRMWARE_SOURCE_NEWER"
|
||
detail = "检测到固件源码晚于当前镜像,需要自动重新构建并烧录"
|
||
elif not bridge.get("protocol_online"):
|
||
reason = ("CAN_TRAFFIC_PROTOCOL_MISSING" if
|
||
bridge.get("bus_traffic") else
|
||
"SEAT_PROTOCOL_MISSING")
|
||
detail = (
|
||
"CAN链路有流量但缺少0x301~0x305,判定为未烧录/固件不匹配类问题" if
|
||
bridge.get("bus_traffic") else
|
||
"桥接在线但座椅协议无报文,判定为未烧录/固件未运行类问题")
|
||
elif not bridge.get("firmware_revision_ok"):
|
||
reason = "FIRMWARE_REVISION_MISMATCH"
|
||
detail = (
|
||
"Seat_Controller_demo固件revision=%s,期望revision=%s,"
|
||
"需要自动重新构建并烧录" %
|
||
(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,
|
||
"reason_code": "FIRMWARE_QUALIFIED",
|
||
"detail": "Seat_Controller_demo固件协议在线,无需重复烧录",
|
||
}
|
||
return {
|
||
"ready": False,
|
||
"auto_flash": bool(probe_detected),
|
||
"reason_code": reason,
|
||
"detail": (detail if probe_detected else
|
||
detail + ";未检测到CMSIS-DAP,无法自动烧录"),
|
||
}
|
||
|
||
@staticmethod
|
||
def _firmware_source_newer_than_image():
|
||
if not FIRMWARE_HEX_PATH.is_file():
|
||
return False, None
|
||
image_mtime = FIRMWARE_HEX_PATH.stat().st_mtime
|
||
newest_path = None
|
||
newest_mtime = 0.0
|
||
for root in FIRMWARE_SOURCE_ROOTS:
|
||
if not root.is_dir():
|
||
continue
|
||
for path in root.rglob("*"):
|
||
if (not path.is_file() or
|
||
path.suffix.lower() not in FIRMWARE_SOURCE_SUFFIXES):
|
||
continue
|
||
try:
|
||
modified = path.stat().st_mtime
|
||
except OSError:
|
||
continue
|
||
if modified > newest_mtime:
|
||
newest_path, newest_mtime = path, modified
|
||
return newest_mtime > image_mtime, newest_path
|
||
|
||
def _firmware_qualification(self, status=None, now=None):
|
||
now = time.time() if now is None else float(now)
|
||
status = _read_json(STATUS_PATH) if status is None else status
|
||
bridge = self._bridge_summary(status, now)
|
||
source_newer, newest_source = \
|
||
self._firmware_source_newer_than_image()
|
||
programmer = self._programmer_status()
|
||
value = self._classify_firmware_issue(
|
||
bridge, FIRMWARE_HEX_PATH.is_file(), source_newer,
|
||
programmer.get("detected", False))
|
||
value.update({
|
||
"checked_epoch": now,
|
||
"programmer": programmer,
|
||
"source_newer": source_newer,
|
||
"newest_source": (None if newest_source is None else
|
||
str(newest_source)),
|
||
"hex_path": str(FIRMWARE_HEX_PATH),
|
||
"bridge": bridge,
|
||
})
|
||
return value
|
||
|
||
def _wait_bridge(self, require_protocol=False, timeout_s=10.0):
|
||
deadline = time.time() + float(timeout_s)
|
||
last_status = None
|
||
last_bridge = None
|
||
while time.time() < deadline:
|
||
last_status = _read_json(STATUS_PATH)
|
||
last_bridge = self._bridge_summary(last_status, time.time())
|
||
if (last_bridge.get("online") and
|
||
(not require_protocol or
|
||
(last_bridge.get("protocol_online") and
|
||
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 {}
|
||
|
||
def _prepare_firmware_for_hardware_diagnostic(self):
|
||
with self.lock:
|
||
self.hardware_prepare = {
|
||
"state": "CHECKING_FIRMWARE",
|
||
"detail": "正在检查CAN桥接、Seat_Controller_demo协议和固件镜像",
|
||
"automatic_flash": False,
|
||
"reason_code": None,
|
||
"started_epoch": time.time(),
|
||
}
|
||
self.bridge_manager.ensure_async()
|
||
status, bridge = self._wait_bridge(require_protocol=False,
|
||
timeout_s=10.0)
|
||
qualification = self._firmware_qualification(status, time.time())
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"reason_code": qualification.get("reason_code"),
|
||
"detail": qualification.get("detail"),
|
||
"qualification": qualification,
|
||
})
|
||
if qualification.get("ready"):
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "READY",
|
||
"finished_epoch": time.time(),
|
||
})
|
||
return status, bridge
|
||
if not qualification.get("auto_flash"):
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "FAILED",
|
||
"finished_epoch": time.time(),
|
||
})
|
||
raise ValueError(qualification.get("detail"))
|
||
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "AUTO_FLASHING",
|
||
"automatic_flash": True,
|
||
"detail": qualification.get("detail") +
|
||
";正在自动构建并烧录",
|
||
})
|
||
firmware = self.build_and_flash_firmware(
|
||
confirmed_bench_clear=True, automatic=True,
|
||
trigger_reason=qualification.get("reason_code"))
|
||
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") or
|
||
not bridge.get("firmware_build_id_ok")):
|
||
detail = ("自动烧录已执行,但仍未收到0x301~0x305;"
|
||
"硬件诊断未启动,请检查ECU供电或烧录日志")
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "FAILED", "detail": detail,
|
||
"firmware": firmware,
|
||
"finished_epoch": time.time(),
|
||
})
|
||
raise ValueError(detail)
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "READY",
|
||
"detail": "自动烧录完成,0x301~0x305已上线,开始21针检测",
|
||
"firmware": firmware,
|
||
"finished_epoch": time.time(),
|
||
})
|
||
return status, bridge
|
||
|
||
@staticmethod
|
||
def _new_batch_state(execution_mode="shadow"):
|
||
return {
|
||
"active": False, "total": len(public_scenarios()),
|
||
"execution_mode": execution_mode,
|
||
"completed": 0, "passed": 0, "failed": 0,
|
||
"current_case_id": None, "started_epoch": None,
|
||
"finished_epoch": None, "results": [],
|
||
}
|
||
|
||
@staticmethod
|
||
def _bridge_summary(status, now):
|
||
heartbeat = None if not isinstance(status, dict) else status.get(
|
||
"heartbeat_epoch")
|
||
age = None
|
||
if heartbeat is not None:
|
||
try:
|
||
age = max(0.0, now - float(heartbeat))
|
||
except (TypeError, ValueError):
|
||
age = None
|
||
version_ok = (isinstance(status, dict) and
|
||
status.get("bridge_version") ==
|
||
REQUIRED_BRIDGE_VERSION)
|
||
online = age is not None and age < 3.0 and version_ok
|
||
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 = []
|
||
for frame_id, epoch in frame_epochs.items():
|
||
if ":" in str(frame_id):
|
||
continue
|
||
try:
|
||
if max(0.0, now - float(epoch)) <= 1.5:
|
||
recent_frame_ids.append(str(frame_id))
|
||
except (TypeError, ValueError):
|
||
continue
|
||
recent_frame_ids.sort()
|
||
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),
|
||
"state": "ONLINE" if online else "OFFLINE",
|
||
"bridge_state": None if not isinstance(status, dict) else status.get(
|
||
"bridge_state"),
|
||
"bridge_version": None if not isinstance(status, dict) else status.get(
|
||
"bridge_version"),
|
||
"bridge_version_ok": version_ok,
|
||
"current_request_id": None if not isinstance(status, dict) else status.get(
|
||
"current_request_id"),
|
||
"position_valid_mask": int(system.get("position_valid_mask", 0)),
|
||
"firmware_revision": firmware_revision,
|
||
"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 (
|
||
status.get("bus") or {}),
|
||
"bus_traffic": bool(recent_frame_ids),
|
||
"protocol_online": protocol_online,
|
||
"recent_frame_ids": recent_frame_ids,
|
||
"expected_frame_ids": [
|
||
"0x301", "0x302", "0x303", "0x304", "0x305", "0x306", "0x307"
|
||
],
|
||
"frame_epochs": frame_epochs,
|
||
}
|
||
|
||
@staticmethod
|
||
def _bench_twin_snapshot(status, now):
|
||
"""Convert raw 0x301~0x305 observations into a UI-safe live twin."""
|
||
if not isinstance(status, dict):
|
||
return {"online": False, "source": "NO_BENCH_DATA", "axes": [],
|
||
"thermal": {"fan_running": False, "heater_level": 0}}
|
||
frame_epochs = status.get("frame_epochs") or {}
|
||
system = status.get("system") or {}
|
||
motors = status.get("motors") or []
|
||
diagnostics = status.get("diagnostics") or []
|
||
currents = status.get("currents") or []
|
||
axes = []
|
||
labels = ("水平滑轨", "坐盆升降", "靠背角度", "腿托角度")
|
||
for motor in range(4):
|
||
state = motors[motor] if motor < len(motors) and \
|
||
isinstance(motors[motor], dict) else {}
|
||
diagnostic = diagnostics[motor] if motor < len(diagnostics) and \
|
||
isinstance(diagnostics[motor], dict) else {}
|
||
current = currents[motor] if motor < len(currents) and \
|
||
isinstance(currents[motor], dict) else {}
|
||
raw_position = state.get("position")
|
||
position = raw_position
|
||
hall = diagnostic.get("hall")
|
||
max_hall = diagnostic.get("max_hall")
|
||
# The production protocol deliberately publishes Position=0xFFFF
|
||
# while PositionValid is false. The mechanism still has live
|
||
# Hall feedback, so use Hall/MaxHall for the visual twin instead
|
||
# of making the model jump to 100% during fault stop and homing.
|
||
if (position is None or not state.get("position_valid", False) or
|
||
int(position) > 10000) and hall is not None and max_hall:
|
||
position = int(round(10000.0 * float(hall) /
|
||
float(max_hall)))
|
||
epoch = frame_epochs.get("0x302:M%d" % motor)
|
||
age = None
|
||
try:
|
||
age = max(0.0, now - float(epoch))
|
||
except (TypeError, ValueError):
|
||
pass
|
||
axes.append({
|
||
"motor": motor, "label": labels[motor],
|
||
"position": position,
|
||
"raw_position": raw_position,
|
||
"position_pct": None if position is None else round(
|
||
max(0.0, min(100.0, float(position) / 100.0)), 1),
|
||
"position_valid": bool(state.get("position_valid", False)),
|
||
"active": bool(state.get("active", False)),
|
||
"direction": int(state.get("direction", 0) or 0),
|
||
"hall": diagnostic.get("hall"),
|
||
"max_hall": diagnostic.get("max_hall"),
|
||
"speed_pps_x100": diagnostic.get("speed_pps_x100"),
|
||
"current_raw": current.get("raw"),
|
||
"fault": bool(state.get("stall") or
|
||
state.get("driver_fault") or
|
||
diagnostic.get("fault_flags")),
|
||
"fresh": age is not None and age <= 1.0,
|
||
"age_s": None if age is None else round(age, 2),
|
||
})
|
||
thermal = status.get("thermal_current") or {}
|
||
thermal_feedback = status.get("thermal") or []
|
||
thermal_epoch = frame_epochs.get("0x305")
|
||
try:
|
||
thermal_age = max(0.0, now - float(thermal_epoch))
|
||
except (TypeError, ValueError):
|
||
thermal_age = None
|
||
protocol_epoch = frame_epochs.get("0x301")
|
||
try:
|
||
online = max(0.0, now - float(protocol_epoch)) <= 0.8
|
||
except (TypeError, ValueError):
|
||
online = False
|
||
progress = status.get("self_healing_progress") or {}
|
||
progress_telemetry = progress.get("telemetry") or {}
|
||
return {
|
||
"online": online,
|
||
"source": "REAL_BENCH_CAN_0x301_0x305",
|
||
"generated_epoch": now,
|
||
"axes": axes,
|
||
"system": {
|
||
"active_mask": int(system.get("active_mask", 0) or 0),
|
||
"faulted_mask": int(system.get("faulted_mask", 0) or 0),
|
||
"position_valid_mask": int(
|
||
system.get("position_valid_mask", 0) or 0),
|
||
},
|
||
"thermal": {
|
||
"fan_running": bool(thermal.get("fan_running", False)) and
|
||
thermal_age is not None and thermal_age <= 1.0,
|
||
"heater_level": (int(thermal.get("heater_level", 0) or 0)
|
||
if thermal_age is not None and
|
||
thermal_age <= 1.0 else 0),
|
||
"fan_raw": thermal.get("fan_raw"),
|
||
"heater_raw": thermal.get("heater_raw"),
|
||
"fresh": thermal_age is not None and thermal_age <= 1.0,
|
||
"age_s": (None if thermal_age is None else
|
||
round(thermal_age, 2)),
|
||
"feedback_byte0": (thermal_feedback[0]
|
||
if thermal_feedback else None),
|
||
},
|
||
"task": {
|
||
"stage": progress_telemetry.get("operation_stage"),
|
||
"name": progress_telemetry.get("normal_task"),
|
||
"reaction": progress_telemetry.get("bench_reaction"),
|
||
"target_positions": progress_telemetry.get(
|
||
"task_target_positions"),
|
||
"restored": bool(progress_telemetry.get("task_restored")),
|
||
},
|
||
}
|
||
|
||
def _save_completed_report(self, healing, hardware, bridge):
|
||
if healing["phase"] not in (PHASE_PASSED, PHASE_FAILED):
|
||
return
|
||
if self.engine.report_saved:
|
||
return
|
||
run_id = healing.get("run_id") or "RUN-UNKNOWN"
|
||
report_dir = LOG_ROOT / run_id
|
||
report_path = report_dir / "self_healing_report.json"
|
||
report = {
|
||
"schema": "seat-self-healing-report-v1",
|
||
"generated_epoch": time.time(),
|
||
"healing": healing,
|
||
"bridge": bridge,
|
||
"hardware_diagnosis": hardware,
|
||
}
|
||
_atomic_json(report_path, report)
|
||
if healing["phase"] == PHASE_PASSED:
|
||
_atomic_json(report_dir / "diagnostic_snapshot_success.json", report)
|
||
self.engine.report_saved = True
|
||
self.latest_report_path = report_path
|
||
|
||
def _record_hardware_transitions(self, result, now):
|
||
for group in result.get("groups", []):
|
||
signature = (group.get("state"), group.get("summary"))
|
||
previous = self.hardware_signatures.get(group.get("key"))
|
||
self.hardware_signatures[group.get("key")] = signature
|
||
if previous == signature:
|
||
continue
|
||
self.hardware_events.append({
|
||
"time": time.strftime("%H:%M:%S", time.localtime(now)),
|
||
"epoch": now, "key": group.get("key"),
|
||
"name": group.get("name"), "state": group.get("state"),
|
||
"summary": group.get("summary"),
|
||
"evidence": list(group.get("evidence") or []),
|
||
})
|
||
self.hardware_events = self.hardware_events[-200:]
|
||
|
||
def _save_hardware_report(self, status, now):
|
||
self.hardware_active = False
|
||
if not self.hardware_run_id or self.hardware_report_path is not None:
|
||
return
|
||
active_test = (self.hardware_result or {}).get("active_test") or {}
|
||
test_phase = active_test.get("phase")
|
||
if test_phase in ("PASSED", "FAILED"):
|
||
self.hardware_prepare.update({
|
||
"state": "DIAGNOSTIC_%s" % test_phase,
|
||
"detail": active_test.get("detail") or
|
||
active_test.get("title") or
|
||
"21针主动检测已结束",
|
||
"finished_epoch": now,
|
||
})
|
||
report = {
|
||
"schema": "seat-hardware-diagnostic-report-v1",
|
||
"run_id": self.hardware_run_id,
|
||
"generated_epoch": now,
|
||
"events": list(self.hardware_events),
|
||
"diagnosis": self.hardware_result,
|
||
"startup_rule_check": self.hardware_self_test,
|
||
"active_pin_test": active_test,
|
||
"bridge": self._bridge_summary(status, now),
|
||
}
|
||
path = HARDWARE_LOG_ROOT / self.hardware_run_id / \
|
||
"hardware_diagnostic_report.json"
|
||
_atomic_json(path, report)
|
||
self.hardware_report_path = path
|
||
|
||
def _hardware_snapshot(self, status, now):
|
||
# Keep the read-only link summary current even before a diagnostic
|
||
# session starts. Event recording and report generation remain gated
|
||
# by ``hardware_active``.
|
||
self.hardware_result = self.signal_model.analyze(status, now=now)
|
||
active_test = {} if not isinstance(status, dict) else (
|
||
status.get("hardware_diagnostic") or {})
|
||
if (self.hardware_request_id and
|
||
active_test.get("request_id") == self.hardware_request_id):
|
||
pin_results = active_test.get("pin_results") or {}
|
||
group_pins = {
|
||
"can": ("JC1-1", "JC1-21"),
|
||
"m0": ("JA1-4", "JA1-5", "JC1-9"),
|
||
"m1": ("JB1-6", "JB1-8", "JC1-15"),
|
||
"m2": ("JB1-12", "JB1-15", "JC1-17"),
|
||
"m3": ("JB1-1", "JB1-3", "JC1-12"),
|
||
"fan": ("JA1-13",), "heater": ("JB1-18",),
|
||
"common": ("JA1-1", "JA1-10", "JA1-11",
|
||
"JC1-6", "JC1-18"),
|
||
}
|
||
ranks = {"OK": 0, "UNKNOWN": 1, "IDLE": 1,
|
||
"UNMONITORED": 1, "WARN": 2, "FAULT": 3}
|
||
for group in self.hardware_result.get("groups", []):
|
||
ids = group_pins.get(group.get("key"))
|
||
values = [pin_results.get(pin_id) for pin_id in (ids or ())]
|
||
values = [item for item in values if item]
|
||
if not values:
|
||
continue
|
||
group["state"] = max(
|
||
(item.get("state", "UNKNOWN") for item in values),
|
||
key=lambda state: ranks.get(state, 1))
|
||
group["summary"] = "主动检测:%s" % ";".join(
|
||
"%s %s" % (pin_id,
|
||
(pin_results.get(pin_id) or {}).get(
|
||
"state", "UNKNOWN"))
|
||
for pin_id in ids)
|
||
group["evidence"] = [
|
||
item.get("evidence") for item in values
|
||
if item.get("evidence")]
|
||
for connector, pins in self.hardware_result.get(
|
||
"connectors", {}).items():
|
||
for pin in pins:
|
||
pin_id = "%s-%s" % (connector, pin.get("number"))
|
||
evidence = pin_results.get(pin_id)
|
||
if not evidence:
|
||
continue
|
||
pin["state"] = evidence.get("state", pin.get("state"))
|
||
pin["active_evidence"] = evidence.get("evidence")
|
||
pin["evidence_kind"] = evidence.get("evidence_kind")
|
||
enabled = [pin for pins in self.hardware_result.get(
|
||
"connectors", {}).values() for pin in pins
|
||
if pin.get("enabled", True)]
|
||
pending = ("UNKNOWN", "IDLE", "UNMONITORED")
|
||
self.hardware_result["connector_ok_count"] = sum(
|
||
1 for pin in enabled if pin.get("state") == "OK")
|
||
self.hardware_result["connector_warn_count"] = sum(
|
||
1 for pin in enabled if pin.get("state") == "WARN")
|
||
self.hardware_result["connector_fault_count"] = sum(
|
||
1 for pin in enabled if pin.get("state") == "FAULT")
|
||
self.hardware_result["connector_pending_count"] = sum(
|
||
1 for pin in enabled if pin.get("state") in pending)
|
||
if self.hardware_result["connector_fault_count"]:
|
||
self.hardware_result["overall_state"] = "FAULT"
|
||
elif self.hardware_result["connector_warn_count"]:
|
||
self.hardware_result["overall_state"] = "WARN"
|
||
elif self.hardware_result["connector_pending_count"] == 0:
|
||
self.hardware_result["overall_state"] = "OK"
|
||
self.hardware_result["active_test"] = active_test
|
||
if self.hardware_active:
|
||
self._record_hardware_transitions(self.hardware_result, now)
|
||
if (active_test.get("request_id") == self.hardware_request_id and
|
||
active_test.get("phase") in ("PASSED", "FAILED")):
|
||
self._save_hardware_report(status, now)
|
||
return self.hardware_result
|
||
|
||
def _sync_bench_healing(self, status, now):
|
||
if self.engine.execution_mode != "bench" or not self.bench_request_id:
|
||
return
|
||
progress = {} if not isinstance(status, dict) else (
|
||
status.get("self_healing_progress") or {})
|
||
if progress.get("request_id") != self.bench_request_id:
|
||
return
|
||
self.engine.apply_external(
|
||
progress.get("phase"), telemetry=progress.get("telemetry"),
|
||
diagnosed_code=progress.get("diagnosed_code"),
|
||
title=progress.get("title"), detail=progress.get("detail"),
|
||
now=progress.get("epoch", now))
|
||
|
||
@staticmethod
|
||
def _bench_zero_snapshot(status):
|
||
status = status if isinstance(status, dict) else {}
|
||
motors = status.get("motors") or []
|
||
by_motor = {
|
||
int(value.get("motor", -1)): value for value in motors
|
||
if isinstance(value, dict)
|
||
}
|
||
axes = []
|
||
for motor in range(4):
|
||
value = by_motor.get(motor) or {}
|
||
position = int(value.get("position", -1))
|
||
at_zero = bool(
|
||
value.get("position_valid") and value.get("near") and
|
||
0 <= position <= 100 and not value.get("active"))
|
||
axes.append({
|
||
"motor": motor, "position": position,
|
||
"position_valid": bool(value.get("position_valid")),
|
||
"near": bool(value.get("near")),
|
||
"active": bool(value.get("active")),
|
||
"at_zero": at_zero,
|
||
})
|
||
system = status.get("system") or {}
|
||
all_zero = bool(
|
||
all(axis["at_zero"] for axis in axes) and
|
||
int(system.get("position_valid_mask", 0)) == 0x0F and
|
||
int(system.get("active_mask", 0)) == 0 and
|
||
int(system.get("faulted_mask", 0)) == 0)
|
||
return {
|
||
"axes": axes,
|
||
"zero_count": sum(1 for axis in axes if axis["at_zero"]),
|
||
"all_zero": all_zero,
|
||
}
|
||
|
||
def _sync_bench_reset(self, status, now):
|
||
zero = self._bench_zero_snapshot(status)
|
||
self.bench_reset.update(zero)
|
||
request_id = self.bench_reset.get("request_id")
|
||
if not request_id or self.bench_reset.get("phase") not in (
|
||
"QUEUED", "HOMING"):
|
||
return
|
||
current_request_id = None if not isinstance(status, dict) else \
|
||
status.get("current_request_id")
|
||
if current_request_id == request_id:
|
||
self.bench_reset["phase"] = "HOMING"
|
||
self.bench_reset["summary"] = \
|
||
"四轴依次归零中,当前%d/4轴已在零位" % zero["zero_count"]
|
||
return
|
||
last_result = {} if not isinstance(status, dict) else \
|
||
(status.get("last_result") or {})
|
||
if last_result.get("request_id") != request_id:
|
||
if now - float(self.bench_reset.get("started_epoch") or now) > 600.0:
|
||
self.bench_reset.update({
|
||
"phase": "FAILED", "finished_epoch": now,
|
||
"summary": "四轴归零等待超时,台架保持停止",
|
||
})
|
||
return
|
||
passed = last_result.get("result") == "PASS" and zero["all_zero"]
|
||
self.bench_reset.update({
|
||
"phase": "PASSED" if passed else "FAILED",
|
||
"finished_epoch": now,
|
||
"summary": ("四轴已归零,可启动一项实物故障用例" if passed else
|
||
(last_result.get("summary") or
|
||
"四轴归零反馈不完整,禁止故障注入")),
|
||
"details": last_result.get("details"),
|
||
})
|
||
|
||
def snapshot(self):
|
||
with self.lock:
|
||
now = time.time()
|
||
status = _read_json(STATUS_PATH)
|
||
self._sync_bench_reset(status, now)
|
||
self._sync_bench_healing(status, now)
|
||
hardware = self._hardware_snapshot(status, now)
|
||
bridge = self._bridge_summary(status, now)
|
||
healing = self.engine.snapshot(now)
|
||
self._save_completed_report(healing, hardware, bridge)
|
||
return {
|
||
"schema": "seat-self-healing-console-state-v1",
|
||
"generated_epoch": now,
|
||
"project": {
|
||
"name": "Seat_Controller_demo",
|
||
"controller": "T2G Seat Controller",
|
||
"ai_engine": "Seat-Heal Evidence Engine v1",
|
||
"execution_target": "影子故障模型 + 受控实物CAN写入闭环",
|
||
},
|
||
"scenarios": public_scenarios(),
|
||
"bridge": bridge,
|
||
"bench_twin": self._bench_twin_snapshot(status, now),
|
||
"bridge_manager": self.bridge_manager.snapshot(),
|
||
"healing": healing,
|
||
"bench_reset": dict(self.bench_reset),
|
||
"hardware": hardware,
|
||
"batch": dict(self.batch),
|
||
"hardware_session": {
|
||
"active": self.hardware_active,
|
||
"run_id": self.hardware_run_id,
|
||
"started_epoch": self.hardware_started_epoch,
|
||
"events": list(self.hardware_events),
|
||
"report_path": (None if self.hardware_report_path is None
|
||
else str(self.hardware_report_path)),
|
||
"self_test": self.hardware_self_test,
|
||
"request_id": self.hardware_request_id,
|
||
"progress": (self.hardware_result or {}).get(
|
||
"active_test"),
|
||
"prepare": dict(self.hardware_prepare),
|
||
},
|
||
"model": {
|
||
"available": True,
|
||
"type": "PROCEDURAL_SEAT",
|
||
"name": "Cognitive_Agent程序化座椅孪生",
|
||
},
|
||
"latest_report_path": (None if self.latest_report_path is None
|
||
else str(self.latest_report_path)),
|
||
"firmware": self._firmware_snapshot(),
|
||
}
|
||
|
||
def start(self, case_id, execution_mode="shadow", allow_batch=False):
|
||
with self.lock:
|
||
if execution_mode == "bench":
|
||
if (self.hardware_active or
|
||
(self.batch.get("active") and not allow_batch)):
|
||
raise ValueError("硬件诊断或批量验证正在运行,不能启动实物故障用例")
|
||
scenario = SCENARIO_BY_ID.get(case_id) or {}
|
||
if not (scenario.get("bench") or {}).get("supported", False):
|
||
raise ValueError(
|
||
"该用例没有安全的实物注入手段,请使用影子仿真")
|
||
self.bridge_manager.ensure_async()
|
||
now = time.time()
|
||
status, bridge = self._wait_bridge(
|
||
require_protocol=True, timeout_s=12.0)
|
||
now = time.time()
|
||
if not bridge.get("online"):
|
||
raise ValueError("CAN Runtime未在线,不能向实物ECU写入故障")
|
||
if not bridge.get("protocol_online"):
|
||
raise ValueError("未识别到本工程座椅ECU协议,不能执行台架故障注入")
|
||
self._sync_bench_reset(status, now)
|
||
zero = self._bench_zero_snapshot(status)
|
||
if (self.bench_reset.get("phase") != "PASSED" or
|
||
not zero.get("all_zero")):
|
||
raise ValueError(
|
||
"请先点击‘重置并四轴归零’,确认四轴均为零位后再启动该用例")
|
||
request_id = "SELF_HEAL_BENCH_%d" % int(now * 1000)
|
||
request = {
|
||
"schema": "seat-function-request-v1",
|
||
"request_id": request_id,
|
||
"created_epoch": now,
|
||
"armed_until_epoch": now + 60.0,
|
||
"source": "seat_self_healing_console",
|
||
"action": "execute_self_healing_case",
|
||
"payload": {"case_id": case_id},
|
||
}
|
||
self.latest_report_path = None
|
||
self.bench_request_id = request_id
|
||
value = self.engine.start(
|
||
case_id, now=now, execution_mode="bench")
|
||
self.engine.telemetry["bench_request_id"] = request_id
|
||
_atomic_json(PENDING_DIR / (request_id + ".json"), request)
|
||
self.bench_reset.update({
|
||
"phase": "CONSUMED",
|
||
"summary": "本次归零许可已用于%s;下一个用例前须重新归零" %
|
||
case_id,
|
||
})
|
||
return value
|
||
self.latest_report_path = None
|
||
self.bench_request_id = None
|
||
return self.engine.start(case_id, execution_mode="shadow")
|
||
|
||
def reset(self, execution_mode="shadow", allow_batch=False,
|
||
force_all_axes=True, home_nonzero_axes=False):
|
||
with self.lock:
|
||
if (self.engine.execution_mode == "bench" and
|
||
self.engine.phase not in ("IDLE", PHASE_PASSED,
|
||
PHASE_FAILED)):
|
||
raise ValueError("实物台架执行中不能重置,请使用STOP ALL")
|
||
if execution_mode not in ("shadow", "bench"):
|
||
raise ValueError("未知重置方式")
|
||
self.engine.reset()
|
||
self.bench_request_id = None
|
||
if execution_mode != "bench":
|
||
self.bench_reset.update({
|
||
"phase": "IDLE", "request_id": None,
|
||
"started_epoch": None, "finished_epoch": None,
|
||
"summary": "影子验证已重置",
|
||
"details": None,
|
||
})
|
||
return {"healing": self.engine.snapshot(),
|
||
"bench_reset": dict(self.bench_reset)}
|
||
if (self.hardware_active or
|
||
(self.batch.get("active") and not allow_batch)):
|
||
raise ValueError("硬件诊断或批量验证正在运行,不能执行四轴归零")
|
||
self.bridge_manager.ensure_async()
|
||
now = time.time()
|
||
status, bridge = self._wait_bridge(
|
||
require_protocol=True, timeout_s=12.0)
|
||
now = time.time()
|
||
if not bridge.get("online") or not bridge.get("protocol_online"):
|
||
raise ValueError("CAN桥接或座椅ECU协议未在线,不能执行四轴归零")
|
||
if (status or {}).get("current_request_id"):
|
||
raise ValueError("台架已有任务正在执行,不能开始四轴归零")
|
||
request_id = "RESET_HOME_ALL_%d" % int(now * 1000)
|
||
request = {
|
||
"schema": "seat-function-request-v1",
|
||
"request_id": request_id,
|
||
"created_epoch": now,
|
||
"armed_until_epoch": now + 600.0,
|
||
"source": "seat_self_healing_console",
|
||
"action": "initialize_known_ranges",
|
||
"payload": {
|
||
"force_all_axes": bool(force_all_axes),
|
||
"home_nonzero_axes": bool(home_nonzero_axes),
|
||
},
|
||
}
|
||
zero = self._bench_zero_snapshot(status)
|
||
self.bench_reset = {
|
||
"phase": "QUEUED", "request_id": request_id,
|
||
"started_epoch": now, "finished_epoch": None,
|
||
"summary": "已停止全部输出,等待四轴依次归零",
|
||
"details": None, **zero,
|
||
}
|
||
_atomic_json(PENDING_DIR / (request_id + ".json"), request)
|
||
return {"healing": self.engine.snapshot(),
|
||
"bench_reset": dict(self.bench_reset)}
|
||
|
||
def _wait_bench_reset(self, timeout_s=620.0):
|
||
"""Wait for the bridge-owned four-axis homing request to finish."""
|
||
deadline = time.time() + float(timeout_s)
|
||
last = None
|
||
while time.time() < deadline:
|
||
last = self.snapshot()
|
||
reset = last.get("bench_reset") or {}
|
||
if reset.get("phase") in ("PASSED", "FAILED"):
|
||
return reset
|
||
time.sleep(0.2)
|
||
with self.lock:
|
||
self.bench_reset.update({
|
||
"phase": "FAILED",
|
||
"finished_epoch": time.time(),
|
||
"summary": "四轴归零等待超时,批量验证已停止",
|
||
})
|
||
return dict(self.bench_reset)
|
||
|
||
@staticmethod
|
||
def _wait_bench_bridge_idle(timeout_s=8.0):
|
||
"""Wait until the bridge clears its just-published request result."""
|
||
deadline = time.time() + float(timeout_s)
|
||
while time.time() < deadline:
|
||
status = _read_json(STATUS_PATH) or {}
|
||
if not status.get("current_request_id"):
|
||
return True
|
||
time.sleep(0.05)
|
||
return False
|
||
|
||
def _batch_worker(self, execution_mode):
|
||
try:
|
||
for scenario in public_scenarios():
|
||
with self.lock:
|
||
self.batch["current_case_id"] = scenario["id"]
|
||
self.latest_report_path = None
|
||
if execution_mode == "bench":
|
||
if not self._wait_bench_bridge_idle():
|
||
raise ValueError(
|
||
"%s前台架桥接任务未在安全窗口内结束" % scenario["id"])
|
||
reset = {}
|
||
for reset_attempt in range(2):
|
||
self.reset(
|
||
"bench", allow_batch=True,
|
||
force_all_axes=False,
|
||
home_nonzero_axes=True)
|
||
reset = self._wait_bench_reset()
|
||
if (reset.get("phase") == "PASSED" and
|
||
reset.get("all_zero")):
|
||
break
|
||
if reset_attempt == 0:
|
||
time.sleep(1.0)
|
||
if (reset.get("phase") != "PASSED" or
|
||
not reset.get("all_zero")):
|
||
raise ValueError(
|
||
"%s前四轴自动归零失败:%s" %
|
||
(scenario["id"], reset.get("summary", "未知错误")))
|
||
self.start(
|
||
scenario["id"], execution_mode, allow_batch=True)
|
||
while True:
|
||
value = self.snapshot()
|
||
phase = value["healing"]["phase"]
|
||
if phase in (PHASE_PASSED, PHASE_FAILED):
|
||
break
|
||
time.sleep(0.12)
|
||
with self.lock:
|
||
passed = phase == PHASE_PASSED
|
||
result = {
|
||
"case_id": scenario["id"],
|
||
"title": scenario["short_title"],
|
||
"result": "PASS" if passed else "FAIL",
|
||
"run_id": value["healing"].get("run_id"),
|
||
"duration_s": value["healing"].get("elapsed_s"),
|
||
"diagnosed_code": value["healing"].get("diagnosed_code"),
|
||
"execution_mode": execution_mode,
|
||
"report_path": (None if self.latest_report_path is None else
|
||
str(self.latest_report_path)),
|
||
"snapshot_path": (
|
||
None if self.latest_report_path is None or not passed else
|
||
str(self.latest_report_path.parent /
|
||
"diagnostic_snapshot_success.json")),
|
||
}
|
||
self.batch["results"].append(result)
|
||
self.batch["completed"] += 1
|
||
self.batch["passed" if passed else "failed"] += 1
|
||
with self.lock:
|
||
self.batch["active"] = False
|
||
self.batch["current_case_id"] = None
|
||
self.batch["finished_epoch"] = time.time()
|
||
except Exception as error:
|
||
with self.lock:
|
||
self.batch["active"] = False
|
||
self.batch["failed"] += 1
|
||
self.batch["current_case_id"] = None
|
||
self.batch["finished_epoch"] = time.time()
|
||
self.batch["results"].append({
|
||
"case_id": "BATCH", "title": "批量验证执行器",
|
||
"result": "FAIL", "reason": str(error),
|
||
})
|
||
|
||
def start_batch(self, execution_mode="shadow"):
|
||
with self.lock:
|
||
if self.batch.get("active"):
|
||
raise ValueError("六项自动验证正在运行")
|
||
if execution_mode not in ("shadow", "bench"):
|
||
raise ValueError("未知批量验证方式")
|
||
if execution_mode == "bench":
|
||
if self.hardware_active:
|
||
raise ValueError("硬件诊断正在运行,不能启动实物批量验证")
|
||
self.bridge_manager.ensure_async()
|
||
self.batch = self._new_batch_state(execution_mode)
|
||
self.batch["active"] = True
|
||
self.batch["started_epoch"] = time.time()
|
||
self.batch_thread = threading.Thread(
|
||
target=self._batch_worker, name="seat-self-healing-batch",
|
||
args=(execution_mode,), daemon=True)
|
||
self.batch_thread.start()
|
||
return dict(self.batch)
|
||
|
||
def start_hardware(self):
|
||
try:
|
||
status, bridge = self._prepare_firmware_for_hardware_diagnostic()
|
||
self_test = self.run_hardware_self_test()
|
||
if self_test.get("result") != "PASS":
|
||
raise ValueError("内置诊断规则校验失败,未启动实物诊断:%s" %
|
||
self_test.get("reason", "未知错误"))
|
||
with self.lock:
|
||
now = time.time()
|
||
status = _read_json(STATUS_PATH) or status
|
||
bridge = self._bridge_summary(status, now)
|
||
if not bridge.get("online"):
|
||
raise ValueError(
|
||
"固件资格检查后CAN Runtime离线,未启动21针主动检测")
|
||
if not bridge.get("protocol_online"):
|
||
raise ValueError(
|
||
"固件资格检查后仍未识别0x301~0x305,未启动21针主动检测")
|
||
self.signal_model = SignalDiagnosticModel(
|
||
load_config(CONFIG_PATH))
|
||
self.hardware_active = True
|
||
self.hardware_started_epoch = now
|
||
self.hardware_run_id = \
|
||
"HW-RUN-%s" % time.strftime("%Y%m%d-%H%M%S")
|
||
self.hardware_request_id = \
|
||
"HARDWARE_PIN_%d" % int(now * 1000)
|
||
self.hardware_events = []
|
||
self.hardware_signatures = {}
|
||
self.hardware_report_path = None
|
||
self.hardware_result = self.signal_model.analyze(
|
||
status, now=now)
|
||
self._record_hardware_transitions(self.hardware_result, now)
|
||
request = {
|
||
"schema": "seat-function-request-v1",
|
||
"request_id": self.hardware_request_id,
|
||
"created_epoch": now,
|
||
"armed_until_epoch": now + 180.0,
|
||
"source": "seat_self_healing_console",
|
||
"action": "execute_hardware_diagnostic",
|
||
"payload": {
|
||
"enabled_pin_count": 21,
|
||
"repeat_per_direction": 2,
|
||
"bounded_output": True,
|
||
},
|
||
}
|
||
_atomic_json(
|
||
PENDING_DIR / (self.hardware_request_id + ".json"),
|
||
request)
|
||
self.hardware_prepare.update({
|
||
"state": "DIAGNOSTIC_RUNNING",
|
||
"detail": ("固件资格检查通过;21针主动检测已启动" if
|
||
not self.hardware_prepare.get(
|
||
"automatic_flash") else
|
||
"自动烧录与协议校验通过;21针主动检测已启动"),
|
||
})
|
||
return self.hardware_session()
|
||
except Exception as error:
|
||
with self.lock:
|
||
self.hardware_prepare.update({
|
||
"state": "FAILED", "detail": str(error),
|
||
"finished_epoch": time.time(),
|
||
})
|
||
if isinstance(error, ValueError):
|
||
raise
|
||
raise ValueError("硬件诊断启动前的自动固件处理失败:%s" % error)
|
||
|
||
def hardware_session(self):
|
||
return {
|
||
"active": self.hardware_active, "run_id": self.hardware_run_id,
|
||
"started_epoch": self.hardware_started_epoch,
|
||
"events": list(self.hardware_events),
|
||
"report_path": (None if self.hardware_report_path is None else
|
||
str(self.hardware_report_path)),
|
||
"self_test": self.hardware_self_test,
|
||
"request_id": self.hardware_request_id,
|
||
"progress": (self.hardware_result or {}).get("active_test"),
|
||
"prepare": dict(self.hardware_prepare),
|
||
}
|
||
|
||
def stop_hardware(self):
|
||
with self.lock:
|
||
now = time.time()
|
||
status = _read_json(STATUS_PATH)
|
||
self._save_hardware_report(status, now)
|
||
return self.hardware_session()
|
||
|
||
def run_hardware_self_test(self):
|
||
started = time.time()
|
||
try:
|
||
run_signal_self_test()
|
||
result = "PASS"
|
||
reason = ("启动前内部校验通过:JA1/JB1/JC1共72针映射、"
|
||
"21个启用针脚及检测阈值一致")
|
||
except Exception as error:
|
||
result = "FAIL"
|
||
reason = str(error)
|
||
value = {
|
||
"result": result, "reason": reason,
|
||
"duration_ms": round((time.time() - started) * 1000.0, 2),
|
||
"time": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||
}
|
||
with self.lock:
|
||
self.hardware_self_test = value
|
||
path = HARDWARE_LOG_ROOT / "self_test_latest.json"
|
||
_atomic_json(path, value)
|
||
return value
|
||
|
||
def emergency_stop(self):
|
||
request_id = "SELF_HEAL_STOP_%d" % int(time.time() * 1000)
|
||
value = {
|
||
"schema": "seat-function-emergency-v1",
|
||
"request_id": request_id,
|
||
"created_epoch": time.time(),
|
||
"armed_until_epoch": time.time() + 5.0,
|
||
"stop_all": True,
|
||
"thermal_off": True,
|
||
"source": "seat_self_healing_console",
|
||
}
|
||
_atomic_json(EMERGENCY_PATH, value)
|
||
return value
|
||
|
||
def build_and_flash_firmware(self, confirmed_bench_clear=False,
|
||
automatic=False, trigger_reason=None):
|
||
if not confirmed_bench_clear:
|
||
raise ValueError("请先确认台架运动区域无人、座椅机构无遮挡且急停可用")
|
||
with self.lock:
|
||
if self.batch.get("active") or self.hardware_active:
|
||
raise ValueError("有台架验证正在运行,不能烧录固件")
|
||
if self.firmware_operation.get("state") in ("BUILDING", "FLASHING"):
|
||
raise ValueError("固件构建/烧录已在进行")
|
||
if (self.cspy_process is not None and
|
||
self.cspy_process.poll() is None):
|
||
raise ValueError("台架测试固件已由C-SPY运行,无需重复烧录")
|
||
if not IAR_BUILD_PATH.exists() or not CSPY_BAT_PATH.exists():
|
||
raise ValueError("未找到IAR构建或C-SPY命令行工具")
|
||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||
run_dir = FIRMWARE_LOG_ROOT / stamp
|
||
run_dir.mkdir(parents=True, exist_ok=True)
|
||
self.firmware_operation = {
|
||
"state": "BUILDING", "message": "正在构建台架测试固件",
|
||
"started_epoch": time.time(), "log_dir": str(run_dir),
|
||
"hex_path": str(FIRMWARE_HEX_PATH),
|
||
"probe_checked_epoch": 0.0,
|
||
"automatic": bool(automatic),
|
||
"trigger_reason": trigger_reason,
|
||
}
|
||
|
||
build = self._run_hidden(
|
||
[IAR_BUILD_PATH, IAR_PROJECT_PATH, "-build", "rev_d",
|
||
"-log", "errors"], 180)
|
||
build_log = run_dir / "iar_build.log"
|
||
build_log.write_text(build.stdout, encoding="utf-8")
|
||
if build.returncode != 0 or not FIRMWARE_OUT_PATH.exists():
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"state": "BUILD_FAILED", "message": "IAR构建失败",
|
||
"build_returncode": build.returncode,
|
||
"build_log": str(build_log),
|
||
})
|
||
raise ValueError("IAR构建失败,详见%s" % build_log)
|
||
|
||
programmer = self._programmer_status()
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"programmer": programmer,
|
||
"probe_checked_epoch": time.time(),
|
||
"build_returncode": build.returncode,
|
||
"build_log": str(build_log),
|
||
"hex_sha256": hashlib.sha256(
|
||
FIRMWARE_HEX_PATH.read_bytes()).hexdigest(),
|
||
})
|
||
if not programmer.get("detected"):
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"state": "BUILT_NO_PROBE",
|
||
"message": "固件已构建;未检测到CMSIS-DAP,未执行烧录",
|
||
})
|
||
raise ValueError(
|
||
"测试固件已成功构建,但Windows未枚举到CMSIS-DAP下载器;"
|
||
"连接下载器后可再次点击烧录")
|
||
|
||
self.emergency_stop()
|
||
time.sleep(0.5)
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"state": "FLASHING", "message": "正在通过CMSIS-DAP烧录并校验",
|
||
})
|
||
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||
flash = None
|
||
flash_log = None
|
||
flash_logs = []
|
||
flash_returncode = None
|
||
flash_error = None
|
||
protocol_online = False
|
||
max_flash_attempts = 3
|
||
for attempt in range(1, max_flash_attempts + 1):
|
||
flash_error = None
|
||
flash_log = run_dir / ("cspy_flash.log" if attempt == 1 else
|
||
"cspy_flash_attempt_%d.log" % attempt)
|
||
flash_logs.append(str(flash_log))
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"state": "FLASHING",
|
||
"message": "正在通过CMSIS-DAP烧录并校验(%d/%d)" %
|
||
(attempt, max_flash_attempts),
|
||
"flash_attempt": attempt,
|
||
"flash_attempts_max": max_flash_attempts,
|
||
"flash_logs": list(flash_logs),
|
||
})
|
||
try:
|
||
# Leave the flashed ECU running without an attached debugger.
|
||
# Otherwise an on-chip watchdog reset can be intercepted before
|
||
# the restarted firmware publishes recovery evidence. A macro
|
||
# parameter marks this as an authorized clean detach; unexpected
|
||
# debugger loss still forces the ECU safe.
|
||
with flash_log.open("w", encoding="utf-8") as stream:
|
||
flash = subprocess.Popen([
|
||
str(CSPY_BAT_PATH), "--silent", "--timeout", "3000",
|
||
"--leave_target_running", "--macro_param",
|
||
"seat_hil_leave_target_running=1",
|
||
"-f", str(CSPY_GENERAL_PATH), "--backend", "-f",
|
||
str(CSPY_DRIVER_PATH)], cwd=str(PROJECT_DIR),
|
||
stdout=stream, stderr=subprocess.STDOUT,
|
||
creationflags=creationflags)
|
||
self.cspy_process = flash
|
||
except OSError as error:
|
||
flash_error = str(error)
|
||
flash_returncode = None
|
||
else:
|
||
self.bridge_manager.ensure_async()
|
||
deadline = time.time() + 20.0
|
||
while time.time() < deadline:
|
||
flash_returncode = flash.poll()
|
||
if flash_returncode not in (None, 0):
|
||
break
|
||
bridge = self._bridge_summary(
|
||
_read_json(STATUS_PATH), time.time())
|
||
if (bridge.get("protocol_online") and
|
||
flash_returncode == 0):
|
||
protocol_online = True
|
||
break
|
||
time.sleep(0.25)
|
||
flash_returncode = flash.poll()
|
||
if flash_returncode is not None:
|
||
self.cspy_process = None
|
||
if protocol_online:
|
||
break
|
||
# Retry only a genuine probe/CPU connection failure. A zero
|
||
# return code means the image was downloaded successfully; if CAN
|
||
# is still silent, another download cannot repair the bus path.
|
||
# Likewise, do not launch a second debugger while the first one
|
||
# still owns the probe.
|
||
if flash_error is None and flash_returncode in (None, 0):
|
||
break
|
||
if attempt < max_flash_attempts:
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"message": ("CMSIS-DAP目标握手失败,%0.1f秒后自动重试" %
|
||
float(attempt)),
|
||
"flash_returncode": flash_returncode,
|
||
"flash_error": flash_error,
|
||
})
|
||
time.sleep(float(attempt))
|
||
|
||
if flash_returncode not in (None, 0) and not protocol_online:
|
||
with self.lock:
|
||
self.firmware_operation.update({
|
||
"state": "FLASH_FAILED",
|
||
"message": "C-SPY多次无法连接目标CPU",
|
||
"flash_returncode": flash_returncode,
|
||
"flash_error": flash_error,
|
||
"flash_log": str(flash_log),
|
||
"flash_logs": list(flash_logs),
|
||
"finished_epoch": time.time(),
|
||
})
|
||
raise ValueError(
|
||
"CMSIS-DAP已枚举,但%d次均无法连接目标CPU;"
|
||
"请检查座椅ECU 12V供电和SWD接线,日志:%s" %
|
||
(max_flash_attempts, flash_log))
|
||
|
||
with self.lock:
|
||
if flash_returncode is not None:
|
||
self.cspy_process = None
|
||
self.firmware_operation.update({
|
||
"state": "READY" if protocol_online else "FLASHED_WAITING_CAN",
|
||
"message": ("测试固件烧录成功且0x301~0x305已在线" if
|
||
protocol_online else
|
||
"C-SPY仍在运行,但20秒内未收到座椅协议"),
|
||
"flash_returncode": flash_returncode,
|
||
"flash_log": str(flash_log),
|
||
"flash_logs": list(flash_logs),
|
||
"cspy_pid": (flash.pid if flash is not None and
|
||
flash_returncode is None else None),
|
||
"target_mode": ("STANDALONE" if flash_returncode == 0 else
|
||
"CSPY_ATTACHED"),
|
||
"finished_epoch": time.time(),
|
||
"protocol_online": protocol_online,
|
||
})
|
||
return dict(self.firmware_operation)
|
||
|
||
def save_snapshot(self):
|
||
value = self.snapshot()
|
||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||
path = LOG_ROOT / ("snapshot_%s.json" % stamp)
|
||
_atomic_json(path, value)
|
||
self.latest_report_path = path
|
||
return path
|
||
|
||
def open_log_dir(self):
|
||
LOG_ROOT.mkdir(parents=True, exist_ok=True)
|
||
try:
|
||
os.startfile(str(LOG_ROOT))
|
||
return True
|
||
except (AttributeError, OSError):
|
||
return False
|
||
|
||
|
||
class SelfRepairService(object):
|
||
"""Repository-facing facade for the migrated self-repair runtime."""
|
||
|
||
def __init__(self):
|
||
self.bridge_manager = BRIDGE_MANAGER
|
||
self.state = ConsoleState(self.bridge_manager)
|
||
|
||
@staticmethod
|
||
def _configuration():
|
||
firmware_root_configured = PROJECT_DIR.is_dir()
|
||
return {
|
||
"runtime_dir": str(RUNTIME_DIR),
|
||
"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),
|
||
"cspy_bat_path": str(CSPY_BAT_PATH),
|
||
"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
|
||
and IAR_BUILD_PATH.is_file()
|
||
and CSPY_BAT_PATH.is_file()
|
||
),
|
||
"auto_start_bridge": _env_flag("SELF_REPAIR_AUTO_START_BRIDGE", False),
|
||
}
|
||
|
||
def start(self):
|
||
self.bridge_manager.start()
|
||
|
||
def close(self):
|
||
self.bridge_manager.stop()
|
||
|
||
def health(self):
|
||
configuration = self._configuration()
|
||
return {
|
||
"ok": configuration["diagnostic_config_available"],
|
||
"service": "seat-self-repair",
|
||
"configuration": configuration,
|
||
"bridge_manager": self.bridge_manager.snapshot(),
|
||
}
|
||
|
||
def snapshot(self):
|
||
value = self.state.snapshot()
|
||
value["integration"] = self._configuration()
|
||
return value
|
||
|
||
def start_healing(self, payload):
|
||
return self.state.start(
|
||
payload.get("case_id"), payload.get("execution_mode", "shadow"))
|
||
|
||
def start_batch(self, payload):
|
||
return self.state.start_batch(payload.get("execution_mode", "shadow"))
|
||
|
||
def reset_healing(self, payload):
|
||
return self.state.reset(
|
||
payload.get("execution_mode", "shadow"),
|
||
force_all_axes=bool(payload.get("force_all_axes", True)),
|
||
home_nonzero_axes=bool(payload.get("home_nonzero_axes", False)))
|
||
|
||
def start_hardware(self):
|
||
return self.state.start_hardware()
|
||
|
||
def stop_hardware(self):
|
||
return self.state.stop_hardware()
|
||
|
||
def run_hardware_self_test(self):
|
||
return self.state.run_hardware_self_test()
|
||
|
||
def emergency_stop(self):
|
||
return self.state.emergency_stop()
|
||
|
||
def build_and_flash_firmware(self, payload):
|
||
return self.state.build_and_flash_firmware(
|
||
bool(payload.get("confirmed_bench_clear", False)))
|
||
|
||
def save_snapshot(self):
|
||
return self.state.save_snapshot()
|
||
|
||
def open_log_dir(self):
|
||
return self.state.open_log_dir()
|
||
|
||
@staticmethod
|
||
def run_self_test():
|
||
run_healing_self_test()
|
||
run_signal_self_test()
|
||
run_bridge_manager_self_test(
|
||
STATUS_PATH,
|
||
BRIDGE_SCRIPT_PATH,
|
||
zcanpro_exe=ZCANPRO_EXE,
|
||
script_config_path=ZCANPRO_SCRIPT_CONFIG,
|
||
)
|
||
return True
|