feat: refine vehicle console and self-repair workflow
This commit is contained in:
@@ -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"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user