feat: complete dual-seat whole-vehicle orchestration
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
"""ZCANPRO bridge for the web-console self-repair hardware workflow.
|
||||
"""Transport-neutral bridge for the seat/cabin hardware workflow.
|
||||
|
||||
Run this file in ZCANPRO's extension-script window and leave it running. The
|
||||
web console writes armed JSON requests into the repository self-repair runtime;
|
||||
this bridge is the only process that imports and calls ``zcanpro``.
|
||||
The protocol engine can still run in ZCANPRO for compatibility, but the normal
|
||||
application now injects a direct ZLG channel transport. Both modes consume the
|
||||
same armed requests and publish the same status/report schema.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
@@ -16,7 +16,10 @@ import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
import zcanpro
|
||||
try:
|
||||
import zcanpro
|
||||
except ImportError: # normal Python uses an injected direct-CAN transport
|
||||
zcanpro = None
|
||||
|
||||
# ZCANPRO executes extension scripts without adding the script directory or
|
||||
# the firmware HIL helpers to ``sys.path``. A fresh ZCANPRO process therefore
|
||||
@@ -44,16 +47,24 @@ parse_303 = _hil_common.parse_303
|
||||
parse_304 = _hil_common.parse_304
|
||||
parse_305 = _hil_common.parse_305
|
||||
|
||||
RUNTIME_DIR = os.path.join(
|
||||
REPOSITORY_ROOT, "artifacts", "tmp", "self-repair", "runtime")
|
||||
RUNTIME_DIR = os.environ.get(
|
||||
"SELF_REPAIR_RUNTIME_DIR",
|
||||
os.path.join(REPOSITORY_ROOT, "artifacts", "tmp", "self-repair", "runtime"),
|
||||
)
|
||||
PENDING_DIR = os.path.join(RUNTIME_DIR, "requests", "pending")
|
||||
PROCESSING_DIR = os.path.join(RUNTIME_DIR, "requests", "processing")
|
||||
COMPLETED_DIR = os.path.join(RUNTIME_DIR, "requests", "completed")
|
||||
STATUS_PATH = os.path.join(RUNTIME_DIR, "bridge_status.json")
|
||||
STATUS_PATH = os.environ.get(
|
||||
"SELF_REPAIR_BRIDGE_STATUS_PATH", os.path.join(RUNTIME_DIR, "bridge_status.json"))
|
||||
EMERGENCY_PATH = os.path.join(RUNTIME_DIR, "emergency_stop.json")
|
||||
LOG_ROOT = os.path.join(
|
||||
REPOSITORY_ROOT, "artifacts", "self-repair", "hardware")
|
||||
LOG_ROOT = os.environ.get(
|
||||
"SELF_REPAIR_HARDWARE_LOG_DIR",
|
||||
os.path.join(REPOSITORY_ROOT, "artifacts", "self-repair", "hardware"),
|
||||
)
|
||||
CONFIRMED_MAX_HALL = (480, 374, 917, 647)
|
||||
EXPECTED_FIRMWARE_REVISION = 15
|
||||
EXPECTED_FIRMWARE_BUILD_ID = 16
|
||||
FIRMWARE_ID_MAGIC = [0x53, 0x4D, 0x56, 0x38] # "SMV8"
|
||||
POSITION_TIMEOUT_MS = 30000
|
||||
NORMAL_TASK_MIN_OBSERVE_S = 5.0
|
||||
|
||||
@@ -162,7 +173,7 @@ HOME_LEG_TIMEOUT_S = 120
|
||||
SIGNAL_WINDOW_S = 2.5
|
||||
HEATER_CURRENT_ACTIVE_DELTA_RAW = 500
|
||||
FAN_CURRENT_ACTIVE_DELTA_RAW = 40
|
||||
BRIDGE_VERSION = "2026-08-09-real-self-heal-v32"
|
||||
BRIDGE_VERSION = "2026-08-10-legrest-runtime-v36"
|
||||
HARDWARE_MOTOR_PINS = {
|
||||
0: ("JA1-4", "JA1-5", "JC1-9"),
|
||||
1: ("JB1-6", "JB1-8", "JC1-15"),
|
||||
@@ -178,6 +189,36 @@ HARDWARE_ENABLED_PINS = (
|
||||
stop_task = False
|
||||
|
||||
|
||||
class ZcanProTransport(object):
|
||||
"""Compatibility adapter for ZCANPRO's injected extension API."""
|
||||
|
||||
def __init__(self):
|
||||
if zcanpro is None:
|
||||
raise RuntimeError("zcanpro is unavailable; inject a CAN transport")
|
||||
buses = zcanpro.get_buses()
|
||||
if not buses:
|
||||
raise RuntimeError("ZCANPRO中没有已启动的CAN通道")
|
||||
self.bus = buses[0]
|
||||
self.bus_id = self.bus["busID"]
|
||||
|
||||
def log(self, value):
|
||||
zcanpro.write_log(str(value))
|
||||
|
||||
def send(self, can_id, data):
|
||||
frame = {
|
||||
"can_id": int(can_id),
|
||||
"is_canfd": 0,
|
||||
"canfd_brs": 0,
|
||||
"data": [int(item) & 0xFF for item in data],
|
||||
}
|
||||
if not zcanpro.transmit(self.bus_id, [frame]):
|
||||
raise RuntimeError("ZCANPRO transmit returned false")
|
||||
|
||||
def receive(self):
|
||||
ok, frames = zcanpro.receive(self.bus_id)
|
||||
return list(frames or []) if ok else []
|
||||
|
||||
|
||||
def z_notify(event_type, obj):
|
||||
del obj
|
||||
global stop_task
|
||||
@@ -301,15 +342,14 @@ class RunLogger(object):
|
||||
|
||||
|
||||
class SeatFunctionBridge(object):
|
||||
def __init__(self):
|
||||
def __init__(self, transport=None, stop_event=None):
|
||||
for path in (RUNTIME_DIR, PENDING_DIR, PROCESSING_DIR, COMPLETED_DIR,
|
||||
LOG_ROOT):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
buses = zcanpro.get_buses()
|
||||
if not buses:
|
||||
raise RuntimeError("ZCANPRO中没有已启动的CAN通道")
|
||||
self.bus = buses[0]
|
||||
self.bus_id = self.bus["busID"]
|
||||
self.transport = transport or ZcanProTransport()
|
||||
self.stop_event = stop_event
|
||||
self.bus = dict(self.transport.bus)
|
||||
self.bus_id = self.bus.get("busID")
|
||||
self.sequence = int(time.time() * 1000.0) & 0xFFFF
|
||||
self.latest_system = None
|
||||
self.latest_motors = [None, None, None, None]
|
||||
@@ -318,6 +358,7 @@ class SeatFunctionBridge(object):
|
||||
self.latest_thermal_current = None
|
||||
self.latest_thermal = None
|
||||
self.latest_dynamic = None
|
||||
self.latest_firmware_identity = None
|
||||
self.frame_epochs = {}
|
||||
self.motor_current_history = [[], [], [], []]
|
||||
self.motor_hall_history = [[], [], [], []]
|
||||
@@ -336,7 +377,7 @@ class SeatFunctionBridge(object):
|
||||
|
||||
def message(self, value):
|
||||
text = str(value)
|
||||
zcanpro.write_log(text)
|
||||
self.transport.log(text)
|
||||
stamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||
self.messages.append("[%s] %s" % (stamp, text))
|
||||
self.messages = self.messages[-40:]
|
||||
@@ -422,6 +463,7 @@ class SeatFunctionBridge(object):
|
||||
"thermal_current": self.latest_thermal_current,
|
||||
"thermal": self.latest_thermal,
|
||||
"dynamic": self.latest_dynamic,
|
||||
"firmware_identity": self.latest_firmware_identity,
|
||||
"frame_epochs": self.frame_epochs,
|
||||
"signal_window": self.signal_window(),
|
||||
"last_result": self.last_result,
|
||||
@@ -436,15 +478,15 @@ class SeatFunctionBridge(object):
|
||||
values = [int(item) & 0xFF for item in data]
|
||||
if self.logger is not None:
|
||||
self.logger.frame("TX", can_id, values, note=note)
|
||||
frame = {"can_id": int(can_id), "is_canfd": 0, "canfd_brs": 0,
|
||||
"data": values}
|
||||
if not zcanpro.transmit(self.bus_id, [frame]):
|
||||
try:
|
||||
self.transport.send(can_id, values)
|
||||
except Exception:
|
||||
raise RuntimeError("发送失败 0x%03X %s" %
|
||||
(can_id, hex_data(values)))
|
||||
|
||||
def receive(self):
|
||||
ok, frames = zcanpro.receive(self.bus_id)
|
||||
if not ok or not frames:
|
||||
frames = self.transport.receive()
|
||||
if not frames:
|
||||
return []
|
||||
output = []
|
||||
for frame in frames:
|
||||
@@ -501,6 +543,22 @@ class SeatFunctionBridge(object):
|
||||
"cycle_index": data[4] | (data[5] << 8),
|
||||
"phase_elapsed_ms": data[6] | (data[7] << 8),
|
||||
}
|
||||
elif can_id == 0x307 and len(data) >= 8:
|
||||
self.latest_firmware_identity = {
|
||||
"magic": "".join(chr(item) for item in data[:4]),
|
||||
"magic_valid": data[:4] == FIRMWARE_ID_MAGIC,
|
||||
"firmware_revision": data[4],
|
||||
"firmware_build_id": data[5],
|
||||
"motor_count": data[6],
|
||||
"protocol_version": data[7],
|
||||
"qualified": (
|
||||
data[:4] == FIRMWARE_ID_MAGIC
|
||||
and data[4] == EXPECTED_FIRMWARE_REVISION
|
||||
and data[5] == EXPECTED_FIRMWARE_BUILD_ID
|
||||
and data[6] == 4
|
||||
and data[7] == 1
|
||||
),
|
||||
}
|
||||
elif can_id == 0x4FA and len(data) >= 1:
|
||||
self.latest_thermal = data
|
||||
output.append((can_id, data))
|
||||
@@ -949,7 +1007,7 @@ class SeatFunctionBridge(object):
|
||||
motor_results = []
|
||||
self._set_pin_result(
|
||||
pin_results, ("JC1-1", "JC1-21"), "OK",
|
||||
"已接收本工程0x301~0x305且STOP_ALL得到ACK",
|
||||
"已接收本工程0x301~0x307且STOP_ALL得到ACK",
|
||||
"direct")
|
||||
self._publish_hardware_diagnostic(
|
||||
request_id, "BASELINE", "建立安全基线",
|
||||
@@ -1266,7 +1324,12 @@ class SeatFunctionBridge(object):
|
||||
(opcode, motor),
|
||||
}
|
||||
|
||||
if opcode in (0x01, 0x03) and self.latest_system is not None and \
|
||||
# Absolute positioning needs a trusted Hall origin. A bounded
|
||||
# timed command is deliberately allowed while PositionValid=0 so
|
||||
# an operator can move an axis away from a hard stop and recover
|
||||
# it with HOME_KNOWN_RANGE; forcing home before that recovery move
|
||||
# can otherwise leave the axis permanently uncalibratable.
|
||||
if opcode == 0x01 and self.latest_system is not None and \
|
||||
not (self.latest_system["position_valid_mask"] & (1 << motor)):
|
||||
if not payload.get("allow_auto_home", False):
|
||||
return "BLOCKED", {
|
||||
@@ -1400,7 +1463,7 @@ class SeatFunctionBridge(object):
|
||||
|
||||
self.capture(0.3)
|
||||
return "PASS", {
|
||||
"summary": "ZCANPRO共享桥接已完成%d条座椅命令并验证实物反馈" %
|
||||
"summary": "共享CAN Runtime已完成%d条座椅命令并验证实物反馈" %
|
||||
len(commands),
|
||||
"results": results,
|
||||
"system": self.latest_system,
|
||||
@@ -1628,28 +1691,6 @@ class SeatFunctionBridge(object):
|
||||
if initial_diag is not None:
|
||||
observed_halls.append(int(initial_diag["hall"]))
|
||||
current_peak = initial_current
|
||||
endpoint_departure = None
|
||||
if motor == 3 and (initial_status is None or
|
||||
not initial_status.get("position_valid") or
|
||||
initial_status.get("near") or
|
||||
int(initial_status.get("position", 0)) <= 300):
|
||||
departure_ok, endpoint_departure = \
|
||||
self._depart_legrest_endpoint_for_home()
|
||||
if not departure_ok:
|
||||
self.stop_all_sync()
|
||||
homed.append({
|
||||
"motor": motor,
|
||||
"result": "FAIL",
|
||||
"endpoint_departure": endpoint_departure,
|
||||
"initial_status": initial_status,
|
||||
"initial_diagnostic": initial_diag,
|
||||
"initial_current": initial_current,
|
||||
})
|
||||
return "FAIL", {
|
||||
"summary": endpoint_departure.get(
|
||||
"summary", "M3端点脱离失败"),
|
||||
"axes": homed,
|
||||
}
|
||||
home_started = time.monotonic()
|
||||
sequence = self.next_sequence()
|
||||
state, response = self.send_command(
|
||||
@@ -1739,7 +1780,6 @@ class SeatFunctionBridge(object):
|
||||
"initial_status": initial_status,
|
||||
"initial_diagnostic": initial_diag,
|
||||
"initial_current": initial_current,
|
||||
"endpoint_departure": endpoint_departure,
|
||||
"peak_current": current_peak,
|
||||
"home_elapsed_s": round(time.monotonic() - home_started, 3),
|
||||
"observed_hall_values": distinct_halls,
|
||||
@@ -2753,7 +2793,8 @@ class SeatFunctionBridge(object):
|
||||
|
||||
def run(self):
|
||||
self.publish(force=True)
|
||||
while not stop_task:
|
||||
while not stop_task and not (
|
||||
self.stop_event is not None and self.stop_event.is_set()):
|
||||
self.check_emergency()
|
||||
self.receive()
|
||||
pending = sorted(
|
||||
@@ -2766,15 +2807,20 @@ class SeatFunctionBridge(object):
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
def z_main():
|
||||
def run_bridge(transport=None, stop_event=None):
|
||||
global stop_task
|
||||
stop_task = False
|
||||
bridge = None
|
||||
try:
|
||||
bridge = SeatFunctionBridge()
|
||||
bridge = SeatFunctionBridge(transport=transport, stop_event=stop_event)
|
||||
bridge.run()
|
||||
except Exception as error:
|
||||
zcanpro.write_log("SEAT_FUNCTION_BRIDGE_ERROR %r" % error)
|
||||
if transport is not None:
|
||||
transport.log("SEAT_FUNCTION_BRIDGE_ERROR %r" % error)
|
||||
elif zcanpro is not None:
|
||||
zcanpro.write_log("SEAT_FUNCTION_BRIDGE_ERROR %r" % error)
|
||||
else:
|
||||
raise
|
||||
finally:
|
||||
if bridge is not None:
|
||||
bridge.bridge_state = "STOPPED"
|
||||
@@ -2789,3 +2835,7 @@ def z_main():
|
||||
except Exception as error:
|
||||
bridge.message("退出安全关闭失败:%r" % error)
|
||||
bridge.publish(force=True)
|
||||
|
||||
|
||||
def z_main():
|
||||
run_bridge(ZcanProTransport())
|
||||
|
||||
Reference in New Issue
Block a user