feat: refine vehicle console and self-repair workflow

This commit is contained in:
vee
2026-08-13 17:38:04 +08:00
parent fd3c219c39
commit 0ec43052f3
20 changed files with 3925 additions and 552 deletions
@@ -260,6 +260,7 @@ class ConsoleState(object):
self.latest_report_path = None
self.batch = self._new_batch_state()
self.batch_thread = None
self.runtime = self._new_runtime_state()
self.hardware_active = False
self.hardware_run_id = None
self.hardware_started_epoch = None
@@ -535,6 +536,202 @@ class ConsoleState(object):
"finished_epoch": None, "results": [],
}
@staticmethod
def _new_runtime_state(execution_mode="shadow"):
return {
"active": False,
"execution_mode": execution_mode,
"started_epoch": None,
"stopped_epoch": None,
"healthy_started_epoch": None,
"healthy_accumulated_s": 0.0,
"motion_profile": "四电机协同自由运动",
"cycle_count": 0,
"injected_count": 0,
"repaired_count": 0,
"last_healing_run_id": None,
"events": [],
}
@staticmethod
def _runtime_event(kind, label, title, detail, now):
return {
"kind": kind,
"label": label,
"title": title,
"detail": detail,
"epoch": now,
"time": time.strftime("%H:%M:%S", time.localtime(now)) +
(".%03d" % int((now * 1000) % 1000)),
}
def _runtime_snapshot(self, healing, now):
runtime = dict(self.runtime)
elapsed = 0.0
if runtime.get("started_epoch") is not None:
end = now if runtime.get("active") else (
runtime.get("stopped_epoch") or now)
elapsed = max(0.0, end - float(runtime["started_epoch"]))
healthy = float(runtime.get("healthy_accumulated_s") or 0.0)
if runtime.get("active") and runtime.get("healthy_started_epoch") is not None:
healthy += max(
0.0, now - float(runtime["healthy_started_epoch"]))
run_id = healing.get("run_id")
phase = healing.get("phase")
if run_id and run_id != runtime.get("last_healing_run_id"):
self.runtime["last_healing_run_id"] = run_id
self.runtime["injected_count"] += 1
self.runtime["healthy_accumulated_s"] = healthy
self.runtime["healthy_started_epoch"] = None
runtime.update(self.runtime)
if phase == PHASE_PASSED and run_id and \
run_id not in self.runtime.get("repaired_run_ids", []):
repaired = list(self.runtime.get("repaired_run_ids", []))
repaired.append(run_id)
self.runtime["repaired_run_ids"] = repaired[-100:]
self.runtime["repaired_count"] += 1
if self.runtime.get("active"):
self.runtime["healthy_started_epoch"] = now
if self.runtime.get("execution_mode") == "bench":
request_id = "SELF_REPAIR_RUNTIME_RESUME_%d" % int(
now * 1000)
_atomic_json(
PENDING_DIR / (request_id + ".json"),
self._dynamic_runtime_request(request_id, True))
runtime.update(self.runtime)
if phase == PHASE_FAILED and run_id and \
run_id not in self.runtime.get("failed_run_ids", []):
failed = list(self.runtime.get("failed_run_ids", []))
failed.append(run_id)
self.runtime["failed_run_ids"] = failed[-100:]
self.runtime.update({
"active": False,
"stopped_epoch": now,
"healthy_started_epoch": None,
})
self.runtime["events"].append(self._runtime_event(
"fault", "安全停止", "自修复未通过,连续运行已停止",
"座椅保持安全停止,请检查故障证据后重新启动。", now))
runtime.update(self.runtime)
healthy = float(self.runtime.get("healthy_accumulated_s") or 0.0)
if self.runtime.get("active") and self.runtime.get(
"healthy_started_epoch") is not None:
healthy += max(
0.0, now - float(self.runtime["healthy_started_epoch"]))
runtime["elapsed_s"] = round(elapsed, 3)
runtime["healthy_elapsed_s"] = round(healthy, 3)
runtime["cycle_count"] = int(elapsed // 20)
runtime.pop("healthy_started_epoch", None)
runtime.pop("healthy_accumulated_s", None)
runtime.pop("repaired_run_ids", None)
runtime.pop("failed_run_ids", None)
return runtime
@staticmethod
def _dynamic_runtime_request(request_id, start):
opcode = 0x0E if start else 0x0F
mode_id = 1 if start else 0
capability = ("seat.start_dynamic_mode" if start else
"seat.stop_dynamic_mode")
operation_id = "self_repair_runtime_dynamic"
return {
"schema": "seat-function-request-v1",
"request_id": request_id,
"created_epoch": time.time(),
"armed_until_epoch": time.time() + 60.0,
"source": "seat_self_healing_console",
"action": "execute_command_plan",
"payload": {
"operations": [{
"id": operation_id,
"domain": "seat",
"capability": capability,
"parameters": ({"mode_id": mode_id} if start else {}),
}],
"commands": [{
"id": "runtime_dynamic_start" if start else
"runtime_dynamic_stop",
"operation_ref": operation_id,
"frame_id": 0x300,
"message": "T2G_Seat_Move_Command_300",
"data": [opcode, 0x10, 0, 0, mode_id, 0, 0, 0],
"signals": {
"Opcode": opcode,
"Motor_Selector": 0,
"Direction": 0,
"Protocol_Version": 1,
"Sequence": 0,
"Param_A": mode_id,
"Param_B": 0,
},
"completion": {
"value": mode_id,
"accepted_states": [1, 2],
"timeout_ms": 8000,
},
}],
},
}
def set_runtime(self, active, execution_mode="shadow"):
if execution_mode not in ("shadow", "bench"):
raise ValueError("未知运行方式")
with self.lock:
now = time.time()
if bool(self.runtime.get("active")) == bool(active):
return self._runtime_snapshot(self.engine.snapshot(now), now)
if active and (self.hardware_active or self.batch.get("active")):
raise ValueError("硬件诊断或批量验证正在运行,不能启动座椅连续运行")
if active and self.engine.phase not in ("IDLE", PHASE_PASSED,
PHASE_FAILED):
raise ValueError("自修复正在处理故障,请等待完成后再启动连续运行")
if execution_mode == "bench":
self.bridge_manager.ensure_async()
_, bridge = self._wait_bridge(
require_protocol=True, timeout_s=12.0)
if not bridge.get("online") or not bridge.get("protocol_online"):
raise ValueError("CAN桥接或座椅ECU协议未在线,不能控制实物座椅")
request_id = "SELF_REPAIR_RUNTIME_%s_%d" % (
"START" if active else "STOP", int(now * 1000))
_atomic_json(
PENDING_DIR / (request_id + ".json"),
self._dynamic_runtime_request(request_id, bool(active)))
if active:
self.engine.reset()
self.runtime = self._new_runtime_state(execution_mode)
self.runtime.update({
"active": True,
"started_epoch": now,
"healthy_started_epoch": now,
})
self.runtime["events"].append(self._runtime_event(
"system", "运行", "座椅自由运动任务已启动",
"四个电机进入协同自由运动,AI开始持续监测。", now))
else:
healthy_started = self.runtime.get("healthy_started_epoch")
if healthy_started is not None:
self.runtime["healthy_accumulated_s"] += max(
0.0, now - float(healthy_started))
self.runtime.update({
"active": False,
"stopped_epoch": now,
"healthy_started_epoch": None,
})
self.runtime["events"].append(self._runtime_event(
"system", "运行", "座椅自由运动任务已停止",
"管理员停止了连续运行,故障注入入口已关闭。", now))
return self._runtime_snapshot(self.engine.snapshot(now), now)
def reset_runtime_statistics(self):
with self.lock:
if self.runtime.get("active"):
raise ValueError("请先停止座椅运行,再重置本次统计")
self.engine.reset()
mode = self.runtime.get("execution_mode", "shadow")
self.runtime = self._new_runtime_state(mode)
return self._runtime_snapshot(self.engine.snapshot(), time.time())
@staticmethod
def _bridge_summary(status, now):
heartbeat = None if not isinstance(status, dict) else status.get(
@@ -944,6 +1141,7 @@ class ConsoleState(object):
hardware = self._hardware_snapshot(status, now)
bridge = self._bridge_summary(status, now)
healing = self.engine.snapshot(now)
runtime = self._runtime_snapshot(healing, now)
self._save_completed_report(healing, hardware, bridge)
return {
"schema": "seat-self-healing-console-state-v1",
@@ -959,6 +1157,7 @@ class ConsoleState(object):
"bench_twin": self._bench_twin_snapshot(status, now),
"bridge_manager": self.bridge_manager.snapshot(),
"healing": healing,
"runtime": runtime,
"bench_reset": dict(self.bench_reset),
"hardware": hardware,
"batch": dict(self.batch),
@@ -987,6 +1186,8 @@ class ConsoleState(object):
def start(self, case_id, execution_mode="shadow", allow_batch=False):
with self.lock:
if not self.runtime.get("active") and not allow_batch:
raise ValueError("请先启动座椅连续运行,再注入故障")
if execution_mode == "bench":
if (self.hardware_active or
(self.batch.get("active") and not allow_batch)):
@@ -1006,10 +1207,15 @@ class ConsoleState(object):
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")):
system = (status or {}).get("system") or {}
runtime_ready = (
self.runtime.get("active") and
int(system.get("position_valid_mask", 0) or 0) == 0x0F and
int(system.get("faulted_mask", 0) or 0) == 0)
if ((self.bench_reset.get("phase") != "PASSED" or
not zero.get("all_zero")) and not runtime_ready):
raise ValueError(
"请先点击‘重置并四轴归零’,确认四轴均为零位后再启动该用例")
"连续运行状态无可信四轴位置,请先停止运行并完成四轴归零")
request_id = "SELF_HEAL_BENCH_%d" % int(now * 1000)
request = {
"schema": "seat-function-request-v1",
@@ -1026,11 +1232,12 @@ class ConsoleState(object):
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,
})
if not runtime_ready:
self.bench_reset.update({
"phase": "CONSUMED",
"summary": "本次归零许可已用于%s;下一个用例前须重新归零" %
case_id,
})
return value
self.latest_report_path = None
self.bench_request_id = None
@@ -1317,6 +1524,21 @@ class ConsoleState(object):
return value
def emergency_stop(self):
with self.lock:
if self.runtime.get("active"):
now = time.time()
healthy_started = self.runtime.get("healthy_started_epoch")
if healthy_started is not None:
self.runtime["healthy_accumulated_s"] += max(
0.0, now - float(healthy_started))
self.runtime.update({
"active": False,
"stopped_epoch": now,
"healthy_started_epoch": None,
})
self.runtime["events"].append(self._runtime_event(
"fault", "停止", "安全链路触发紧急停止",
"STOP ALL 与热管理关闭请求已提交。", now))
request_id = "SELF_HEAL_STOP_%d" % int(time.time() * 1000)
value = {
"schema": "seat-function-emergency-v1",
@@ -1591,6 +1813,14 @@ class SelfRepairService(object):
return self.state.start(
payload.get("case_id"), payload.get("execution_mode", "shadow"))
def set_runtime(self, payload):
return self.state.set_runtime(
bool(payload.get("active")),
payload.get("execution_mode", "shadow"))
def reset_runtime_statistics(self):
return self.state.reset_runtime_statistics()
def start_batch(self, payload):
return self.state.start_batch(payload.get("execution_mode", "shadow"))