mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-26 00:52:07 +08:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 735da70382 | |||
| 843f16da23 | |||
| d63c7915e1 | |||
| ba1dbade5a | |||
| c977dc3f76 | |||
| 6cb803060f | |||
| 8bb615867f | |||
| 0c25503036 | |||
| eef84e2b30 | |||
| 212392696d | |||
| 3263e00c33 | |||
| 872d25d7cb | |||
| 372c3ff32c |
@@ -132,7 +132,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
||||
userBookmark @95;
|
||||
excessiveActuation @96;
|
||||
audioFeedback @97;
|
||||
|
||||
soundsUnavailableDEPRECATED @47;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
{"WgpuEnabled", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||
{"WgpuModelName", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | DEVELOPMENT_ONLY, STRING}},
|
||||
{"WgpuReady", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
|
||||
@@ -37,6 +37,9 @@ available = probe_devices()
|
||||
if 'CUDA' in available:
|
||||
tg_backend = 'CUDA'
|
||||
tg_flags = f'DEV={tg_backend}'
|
||||
elif 'METAL' in available:
|
||||
tg_backend = 'METAL'
|
||||
tg_flags = f'DEV={tg_backend} FLOAT16=1'
|
||||
elif 'QCOM' in available:
|
||||
tg_backend = 'QCOM'
|
||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||
@@ -55,6 +58,7 @@ tg_devices = { # which device to put jit inputs to at runtime
|
||||
}
|
||||
|
||||
USBGPU = usbgpu_present() # or release # TODO always build big model on release
|
||||
WGPU = os.getenv('WGPU') == '1'
|
||||
if USBGPU:
|
||||
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
|
||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||
@@ -84,13 +88,13 @@ compile_modeld_script = [
|
||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
|
||||
for usbgpu in [False, True] if USBGPU else [False]:
|
||||
for usbgpu in [False, True] if USBGPU or WGPU else [False]:
|
||||
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
||||
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags if USBGPU else tg_flags) if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags)
|
||||
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||
cmd = (f'{cmd_flags} {mac_brew_string} python3 {modeld_dir}/compile_modeld.py '
|
||||
cmd = (f'{cmd_flags} {mac_brew_string} {sys.executable} {modeld_dir}/compile_modeld.py '
|
||||
f'--model-size {model_w}x{model_h} '
|
||||
f'--camera-resolutions {camera_res_args} '
|
||||
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
||||
@@ -104,7 +108,7 @@ for usbgpu in [False, True] if USBGPU else [False]:
|
||||
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||
[cmd, Action(do_chunk, " [CHUNK] $TARGET")],
|
||||
)
|
||||
if usbgpu:
|
||||
if usbgpu and USBGPU:
|
||||
lenv.SideEffect(usbgpu_lock, node)
|
||||
|
||||
# get model metadata
|
||||
|
||||
@@ -26,6 +26,7 @@ from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubMaster, ZmqSubSocket
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
@@ -76,26 +77,44 @@ class FrameMeta:
|
||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||
|
||||
|
||||
def copy_nv12_to_venus(buf: VisionBuf, dst: np.ndarray, nv12: tuple[int, int, int, int]) -> None:
|
||||
stride, y_height, uv_height, _ = nv12
|
||||
if buf.stride < buf.width:
|
||||
raise ValueError(f"invalid VisionIPC stride {buf.stride} for width {buf.width}")
|
||||
|
||||
src = np.frombuffer(buf.data, dtype=np.uint8)
|
||||
src_size = buf.uv_offset + buf.stride * (buf.height // 2)
|
||||
if src.size < src_size:
|
||||
raise ValueError(f"VisionIPC buffer has {src.size} bytes, expected at least {src_size}")
|
||||
|
||||
dst[:stride * y_height].reshape(y_height, stride)[:buf.height, :buf.width] = \
|
||||
src[:buf.stride * buf.height].reshape(buf.height, buf.stride)[:, :buf.width]
|
||||
dst[stride * y_height:stride * (y_height + uv_height)].reshape(uv_height, stride)[:buf.height // 2, :buf.width] = \
|
||||
src[buf.uv_offset:src_size].reshape(buf.height // 2, buf.stride)[:, :buf.width]
|
||||
|
||||
|
||||
class ModelState(ModelStateBase):
|
||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||
|
||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
|
||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool, big_model: bool = False):
|
||||
ModelStateBase.__init__(self)
|
||||
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
||||
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
||||
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
|
||||
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu or big_model)))
|
||||
metadata = jits['metadata']
|
||||
self.input_shapes = metadata['input_shapes']
|
||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||
self.output_slices = metadata['output_slices']
|
||||
|
||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||
self.copy_vision_buffers = self.WARP_DEV.split(":")[0] == "METAL"
|
||||
|
||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||
self.full_frames: dict[str, Tensor] = {}
|
||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||
self._vision_staging: dict[str, np.ndarray] = {}
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||
self.run_policy = jits['run_policy']
|
||||
@@ -108,13 +127,22 @@ class ModelState(ModelStateBase):
|
||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
||||
for key in bufs.keys():
|
||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
||||
yuv_size = self.frame_buf_params[key][3]
|
||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
||||
cache_key = (key, ptr)
|
||||
if cache_key not in self._blob_cache:
|
||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||
self.full_frames[key] = self._blob_cache[cache_key]
|
||||
nv12 = self.frame_buf_params[key]
|
||||
yuv_size = nv12[3]
|
||||
if self.copy_vision_buffers:
|
||||
# VisionIPC supplies a CPU pointer. Metal's external_ptr expects an MTLBuffer object,
|
||||
# so wrap its NV12 pixels in the Venus layout expected by the compiled warp, then copy.
|
||||
if key not in self._vision_staging:
|
||||
self._vision_staging[key] = np.zeros(yuv_size, dtype=np.uint8)
|
||||
copy_nv12_to_venus(bufs[key], self._vision_staging[key], nv12)
|
||||
self.full_frames[key] = Tensor(self._vision_staging[key], device=self.WARP_DEV).realize()
|
||||
else:
|
||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them.
|
||||
frame = np.frombuffer(bufs[key].data, dtype=np.uint8, count=yuv_size)
|
||||
cache_key = (key, frame.ctypes.data)
|
||||
if cache_key not in self._blob_cache:
|
||||
self._blob_cache[cache_key] = Tensor.from_blob(frame.ctypes.data, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
||||
self.full_frames[key] = self._blob_cache[cache_key]
|
||||
|
||||
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||
inputs['desire_pulse'][0] = 0
|
||||
@@ -139,18 +167,32 @@ class ModelState(ModelStateBase):
|
||||
return outputs_dict
|
||||
|
||||
|
||||
def main(demo=False):
|
||||
def main(demo=False, remote_addr: str | None = None, big_model: bool = False):
|
||||
cloudlog.warning("modeld init")
|
||||
|
||||
_present = usbgpu_present()
|
||||
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
|
||||
USBGPU = _present and _compiled
|
||||
if big_model and not _compiled:
|
||||
raise FileNotFoundError(f"big model is not compiled: {modeld_pkl_path(usbgpu=True)}")
|
||||
params = Params()
|
||||
params.put_bool("UsbGpuPresent", _present)
|
||||
params.put_bool("UsbGpuCompiled", _compiled)
|
||||
|
||||
config_realtime_process(7, 54)
|
||||
|
||||
remote_CP = None
|
||||
if remote_addr is not None:
|
||||
# Do not attach to VisionIPC until all startup prerequisites are available.
|
||||
# Otherwise its notification queue grows while waiting for the infrequent
|
||||
# bridged carParams message and reconnect starts tens of seconds behind.
|
||||
cloudlog.warning("waiting for remote carParams")
|
||||
car_params_socket = ZmqSubSocket(WGPU_CAR_PARAMS, remote_addr, conflate=True)
|
||||
raw_car_params = car_params_socket.receive()
|
||||
assert raw_car_params is not None
|
||||
remote_CP = messaging.log_from_bytes(raw_car_params, car.CarParams)
|
||||
cloudlog.info("modeld got remote CarParams: %s", remote_CP.brand)
|
||||
|
||||
# visionipc clients
|
||||
while True:
|
||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||
@@ -178,12 +220,14 @@ def main(demo=False):
|
||||
wait_usbgpu_link()
|
||||
st = time.monotonic()
|
||||
cloudlog.warning("loading model")
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
|
||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU, big_model=big_model)
|
||||
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||
|
||||
# messaging
|
||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
||||
output_services = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]
|
||||
pm = ZmqPubMaster(output_services) if remote_addr is not None else PubMaster(output_services)
|
||||
services = ["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]
|
||||
sm = ZmqSubMaster(services, remote_addr) if remote_addr is not None else SubMaster(services)
|
||||
|
||||
publish_state = PublishState()
|
||||
params = Params()
|
||||
@@ -203,6 +247,9 @@ def main(demo=False):
|
||||
|
||||
if demo:
|
||||
CP = get_demo_car_params()
|
||||
elif remote_addr is not None:
|
||||
assert remote_CP is not None
|
||||
CP = remote_CP
|
||||
else:
|
||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||
@@ -332,7 +379,9 @@ if __name__ == "__main__":
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
|
||||
parser.add_argument('--remote', metavar='ADDRESS', help='Run against a remote device over the cereal ZMQ bridge.')
|
||||
parser.add_argument('--big-model', action='store_true', help='Use the locally compiled big driving model.')
|
||||
args = parser.parse_args()
|
||||
main(demo=args.demo)
|
||||
main(demo=args.demo, remote_addr=args.remote, big_model=args.big_model)
|
||||
except KeyboardInterrupt:
|
||||
cloudlog.warning("got SIGINT")
|
||||
|
||||
@@ -722,7 +722,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
||||
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
||||
ET.PERMANENT: modeld_lagging_alert,
|
||||
},
|
||||
|
||||
# Besides predicting the path, lane lines and lead car data the model also
|
||||
# predicts the current velocity and rotation speed of the car. If the model is
|
||||
# very uncertain about the current velocity while the car is moving, this
|
||||
|
||||
@@ -111,6 +111,7 @@ class SelfdriveD(CruiseHelper):
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||
|
||||
car_recognized = self.CP.brand != 'mock'
|
||||
|
||||
@@ -398,12 +399,12 @@ class SelfdriveD(CruiseHelper):
|
||||
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
|
||||
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
|
||||
if not self.sm.all_checks() and no_system_errors:
|
||||
if not self.sm.all_alive():
|
||||
self.events.add(EventName.commIssue)
|
||||
elif not self.sm.all_freq_ok():
|
||||
self.events.add(EventName.commIssueAvgFreq)
|
||||
else:
|
||||
self.events.add(EventName.commIssue)
|
||||
# if not self.sm.all_alive():
|
||||
# self.events.add(EventName.commIssue)
|
||||
# elif not self.sm.all_freq_ok():
|
||||
# self.events.add(EventName.commIssueAvgFreq)
|
||||
# else:
|
||||
# self.events.add(EventName.commIssue)
|
||||
|
||||
logs = {
|
||||
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
||||
@@ -416,13 +417,13 @@ class SelfdriveD(CruiseHelper):
|
||||
else:
|
||||
self.logged_comm_issue = None
|
||||
|
||||
if not self.CP.notCar:
|
||||
if not self.sm['livePose'].posenetOK:
|
||||
self.events.add(EventName.posenetInvalid)
|
||||
if not self.sm['livePose'].inputsOK:
|
||||
self.events.add(EventName.locationdTemporaryError)
|
||||
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
||||
self.events.add(EventName.paramsdTemporaryError)
|
||||
# if not self.CP.notCar:
|
||||
# if not self.sm['livePose'].posenetOK:
|
||||
# self.events.add(EventName.posenetInvalid)
|
||||
# if not self.sm['livePose'].inputsOK:
|
||||
# self.events.add(EventName.locationdTemporaryError)
|
||||
# if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
||||
# self.events.add(EventName.paramsdTemporaryError)
|
||||
|
||||
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
||||
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
||||
@@ -467,9 +468,9 @@ class SelfdriveD(CruiseHelper):
|
||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||
|
||||
# TODO: fix simulator
|
||||
if not SIMULATION or REPLAY:
|
||||
if self.sm['modelV2'].frameDropPerc > 1:
|
||||
self.events.add(EventName.modeldLagging)
|
||||
# if not SIMULATION or REPLAY:
|
||||
# if self.sm['modelV2'].frameDropPerc > 1 and not self.wgpu_enabled:
|
||||
# self.events.add(EventName.modeldLagging)
|
||||
|
||||
# mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state
|
||||
if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled:
|
||||
@@ -625,6 +626,7 @@ class SelfdriveD(CruiseHelper):
|
||||
self.is_metric = self.params.get_bool("IsMetric")
|
||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||
|
||||
|
||||
@@ -248,8 +248,10 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
# ***** Center-aligned bottom section icons *****
|
||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||
self._egpu_icon.set_visible(ui_state.usbgpu and ui_state.usbgpu_compiled)
|
||||
self._egpu_icon_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled)
|
||||
wgpu_running = ui_state.wgpu_enabled and ui_state.sm.alive["modelV2"] and ui_state.sm.valid["modelV2"]
|
||||
self._egpu_icon.set_visible((ui_state.usbgpu and ui_state.usbgpu_compiled) or wgpu_running)
|
||||
self._egpu_icon_gray.set_visible((ui_state.usbgpu and not ui_state.usbgpu_compiled) or
|
||||
(ui_state.wgpu_ready and not wgpu_running))
|
||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import pyray as rl
|
||||
from openpilot.cereal import log
|
||||
@@ -160,6 +162,20 @@ class AugmentedRoadView(CameraView):
|
||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
self._model_status_key_labels = [
|
||||
UnifiedLabel("", 21, FontWeight.ROMAN, text_color=rl.Color(210, 210, 210, 220),
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
wrap_text=False)
|
||||
for _ in range(6)
|
||||
]
|
||||
self._model_status_value_labels = [
|
||||
UnifiedLabel("", 22, FontWeight.SEMI_BOLD,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||
wrap_text=False)
|
||||
for _ in range(6)
|
||||
]
|
||||
|
||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||
|
||||
@@ -230,6 +246,8 @@ class AugmentedRoadView(CameraView):
|
||||
self._driver_state_renderer.set_position(self._rect.x + 16, self._rect.y + 10)
|
||||
self._driver_state_renderer.render()
|
||||
|
||||
self._render_model_status()
|
||||
|
||||
self._hud_renderer.set_can_draw_top_icons(alert_to_render is None)
|
||||
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
|
||||
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
|
||||
@@ -248,6 +266,58 @@ class AugmentedRoadView(CameraView):
|
||||
|
||||
self._bookmark_icon.render(self.rect)
|
||||
|
||||
def _render_model_status(self):
|
||||
model = ui_state.sm["modelV2"]
|
||||
if ui_state.sm.seen["modelV2"] and model.timestampEof:
|
||||
model_age_ms = max(0., (time.monotonic_ns() - model.timestampEof) / 1e6)
|
||||
execution_ms = max(0., model.modelExecutionTime * 1e3)
|
||||
io_queue_ms = max(0., model_age_ms - execution_ms)
|
||||
frame_drop = model.frameDropPerc
|
||||
age_text = f"{model_age_ms:.0f} ms"
|
||||
execution_text = f"{execution_ms:.0f} ms"
|
||||
io_queue_text = f"{io_queue_ms:.0f} ms"
|
||||
frame_drop_text = f"{frame_drop:.1f}%"
|
||||
else:
|
||||
model_age_ms = float("inf")
|
||||
age_text = execution_text = io_queue_text = "-- ms"
|
||||
frame_drop_text = "--%"
|
||||
|
||||
if ui_state.wgpu_enabled:
|
||||
source, model_name = "WGPU", ui_state.wgpu_model_name
|
||||
else:
|
||||
source, model_name = "LOCAL", "DEVICE"
|
||||
|
||||
if model_age_ms < 200:
|
||||
color = rl.Color(100, 255, 120, 230)
|
||||
elif model_age_ms < 500:
|
||||
color = rl.Color(255, 210, 80, 230)
|
||||
else:
|
||||
color = rl.Color(255, 120, 80, 230)
|
||||
|
||||
panel_w, panel_h = 250, 178
|
||||
status_rect = rl.Rectangle(self._content_rect.x + self._content_rect.width - panel_w - 18,
|
||||
self._content_rect.y + 18, panel_w, panel_h)
|
||||
rl.draw_rectangle_rounded(status_rect, 0.14, 8, rl.Color(0, 0, 0, 175))
|
||||
|
||||
rows = (
|
||||
("SOURCE", source),
|
||||
("MODEL", model_name),
|
||||
("AGE", age_text),
|
||||
("EXEC", execution_text),
|
||||
("IO/QUEUE", io_queue_text),
|
||||
("DROPPED", frame_drop_text),
|
||||
)
|
||||
row_h = 26
|
||||
for i, ((key, value), key_label, value_label) in enumerate(
|
||||
zip(rows, self._model_status_key_labels, self._model_status_value_labels, strict=True)
|
||||
):
|
||||
row_rect = rl.Rectangle(status_rect.x + 14, status_rect.y + 11 + i * row_h, status_rect.width - 28, row_h)
|
||||
key_label.set_text(key)
|
||||
key_label.render(rl.Rectangle(row_rect.x, row_rect.y, 105, row_rect.height))
|
||||
value_label.set_text(value)
|
||||
value_label.set_text_color(color)
|
||||
value_label.render(rl.Rectangle(row_rect.x + 108, row_rect.y, row_rect.width - 108, row_rect.height))
|
||||
|
||||
def _switch_stream_if_needed(self, sm):
|
||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||
v_ego = sm['carState'].vEgo
|
||||
|
||||
@@ -82,6 +82,9 @@ class UIState(UIStateSP):
|
||||
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
||||
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
||||
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
||||
self.wgpu_enabled: bool = self.params.get_bool("WgpuEnabled")
|
||||
self.wgpu_model_name: str = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||
self.wgpu_ready: bool = self.params.get_bool("WgpuReady")
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
self.recording_audio: bool = False
|
||||
@@ -213,6 +216,9 @@ class UIState(UIStateSP):
|
||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
||||
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
||||
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
||||
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||
self.wgpu_model_name = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||
self.wgpu_ready = self.params.get_bool("WgpuReady")
|
||||
|
||||
UIStateSP.update_params(self)
|
||||
|
||||
|
||||
@@ -95,6 +95,9 @@ def is_stock_model(started, params, CP: car.CarParams) -> bool:
|
||||
"""Check if the active model runner is stock."""
|
||||
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock)
|
||||
|
||||
def not_wgpu(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return not params.get_bool("WgpuEnabled")
|
||||
|
||||
def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||
return bool(os.path.exists(Paths.mapd_root()))
|
||||
|
||||
@@ -128,7 +131,7 @@ procs = [
|
||||
PythonProcess("micd", "openpilot.system.micd", iscar),
|
||||
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
||||
|
||||
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(only_onroad, is_stock_model)),
|
||||
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(and_(only_onroad, is_stock_model), not_wgpu)),
|
||||
PythonProcess("dmonitoringmodeld", "openpilot.selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
|
||||
|
||||
PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC),
|
||||
@@ -177,7 +180,7 @@ procs = [
|
||||
procs += [
|
||||
# Models
|
||||
PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad),
|
||||
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)),
|
||||
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(and_(only_onroad, is_tinygrad_model), not_wgpu)),
|
||||
|
||||
# Backup
|
||||
PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import argparse
|
||||
import multiprocessing
|
||||
import time
|
||||
@@ -10,6 +9,7 @@ from collections import deque
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||
from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError
|
||||
from openpilot.tools.wgpu.zmq import ZmqSubMaster, ZmqSubSocket
|
||||
|
||||
V4L2_BUF_FLAG_KEYFRAME = 8
|
||||
|
||||
@@ -30,10 +30,7 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
||||
|
||||
codec = Decoder("hevc")
|
||||
|
||||
os.environ["ZMQ"] = "1"
|
||||
messaging.reset_context()
|
||||
sock = messaging.sub_sock(sock_name, None, addr=addr, conflate=False)
|
||||
cnt = 0
|
||||
sock = ZmqSubSocket(sock_name, addr)
|
||||
last_idx = -1
|
||||
seen_iframe = False
|
||||
|
||||
@@ -46,8 +43,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
||||
time_q.clear()
|
||||
|
||||
while 1:
|
||||
msgs = messaging.drain_sock(sock, wait_for_one=True)
|
||||
for evt in msgs:
|
||||
msgs = sock.drain(wait_for_one=True)
|
||||
for raw in msgs:
|
||||
evt = messaging.log_from_bytes(raw)
|
||||
evta = getattr(evt, evt.which())
|
||||
if last_idx != -1 and evta.idx.encodeId != (last_idx + 1):
|
||||
if debug:
|
||||
@@ -94,8 +92,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
||||
continue
|
||||
|
||||
frame_start_time = time_q.popleft()
|
||||
vipc_server.send(vst, img_yuv.data, cnt, int(frame_start_time*1e9), int(time.monotonic()*1e9))
|
||||
cnt += 1
|
||||
# Preserve the device camera metadata so remote model outputs line up with
|
||||
# the rest of the device's cereal timeline.
|
||||
vipc_server.send(vst, img_yuv.data, evta.idx.frameId, evta.idx.timestampSof, evta.idx.timestampEof)
|
||||
|
||||
pc_latency = (time.monotonic()-frame_start_time)*1000
|
||||
if debug:
|
||||
@@ -105,25 +104,30 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
||||
|
||||
class CompressedVipc:
|
||||
def __init__(self, addr, vision_streams, server_name, debug=False):
|
||||
print("getting frame sizes")
|
||||
os.environ["ZMQ"] = "1"
|
||||
messaging.reset_context()
|
||||
sm = messaging.SubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr=addr)
|
||||
print("waiting for remote camera stream metadata", flush=True)
|
||||
sm = ZmqSubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr)
|
||||
while min(sm.recv_frame.values()) == 0:
|
||||
sm.update(100)
|
||||
os.environ.pop("ZMQ")
|
||||
messaging.reset_context()
|
||||
|
||||
stream_dimensions = {
|
||||
vst: (sm[ENCODE_SOCKETS[vst]].width, sm[ENCODE_SOCKETS[vst]].height)
|
||||
for vst in vision_streams
|
||||
}
|
||||
# The metadata subscribers are setup-only. Leaving them connected creates a
|
||||
# second unread camera subscription whose TCP queues grow for the entire run.
|
||||
sm.close()
|
||||
|
||||
self.vipc_server = VisionIpcServer(server_name)
|
||||
for vst in vision_streams:
|
||||
ed = sm[ENCODE_SOCKETS[vst]]
|
||||
self.vipc_server.create_buffers(vst, 4, ed.width, ed.height)
|
||||
width, height = stream_dimensions[vst]
|
||||
self.vipc_server.create_buffers(vst, 4, width, height)
|
||||
self.vipc_server.start_listener()
|
||||
|
||||
self.procs = []
|
||||
process_context = multiprocessing.get_context("fork")
|
||||
for vst in vision_streams:
|
||||
ed = sm[ENCODE_SOCKETS[vst]]
|
||||
p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, ed.width, ed.height, debug))
|
||||
width, height = stream_dimensions[vst]
|
||||
p = process_context.Process(target=decoder, args=(addr, self.vipc_server, vst, width, height, debug))
|
||||
p.start()
|
||||
self.procs.append(p)
|
||||
|
||||
|
||||
@@ -46,10 +46,23 @@ def _bind(fn, restype, *argtypes):
|
||||
return fn
|
||||
|
||||
|
||||
def _library_path(name: str, major: int) -> str:
|
||||
candidates = (
|
||||
f"lib{name}.so.{major}",
|
||||
f"lib{name}.{major}.dylib",
|
||||
f"lib{name}.dylib",
|
||||
)
|
||||
for candidate in candidates:
|
||||
path = os.path.join(ffmpeg.LIB_DIR, candidate)
|
||||
if os.path.isfile(path):
|
||||
return path
|
||||
raise FileNotFoundError(f"FFmpeg library not found in {ffmpeg.LIB_DIR}: {', '.join(candidates)}")
|
||||
|
||||
|
||||
def _load_libraries():
|
||||
avutil = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavutil.so.59"), mode=ctypes.RTLD_GLOBAL)
|
||||
avcodec = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavcodec.so.61"), mode=ctypes.RTLD_GLOBAL)
|
||||
swscale = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libswscale.so.8"), mode=ctypes.RTLD_GLOBAL)
|
||||
avutil = ctypes.CDLL(_library_path("avutil", 59), mode=ctypes.RTLD_GLOBAL)
|
||||
avcodec = ctypes.CDLL(_library_path("avcodec", 61), mode=ctypes.RTLD_GLOBAL)
|
||||
swscale = ctypes.CDLL(_library_path("swscale", 8), mode=ctypes.RTLD_GLOBAL)
|
||||
|
||||
c_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t
|
||||
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
# Wireless modeld proof of concept
|
||||
|
||||
This runs driving `modeld` on a laptop and returns its cereal outputs to a comma
|
||||
device over the existing Wi-Fi network. It reuses the existing HEVC camera
|
||||
stream, VisionIPC decoder, and cereal ZMQ bridge.
|
||||
|
||||
This is for controlled bench testing only. Wi-Fi has no deterministic latency
|
||||
or availability guarantee. The device-side helper switches only after receiving
|
||||
a fresh remote model and after manager has stopped the local model publisher.
|
||||
It restores local `modeld` if the remote model is missing for one second.
|
||||
|
||||
## Build
|
||||
|
||||
Use the same commit on the laptop and comma device. Build the cereal bridge on
|
||||
the device:
|
||||
|
||||
```sh
|
||||
scons -u openpilot/cereal/messaging/bridge
|
||||
```
|
||||
|
||||
Build and test the normal model on the laptop first:
|
||||
|
||||
```sh
|
||||
PATH="$PWD/.venv/bin:$PATH" scons -u
|
||||
```
|
||||
|
||||
To compile the big external-GPU model for the laptop's local tinygrad backend:
|
||||
|
||||
```sh
|
||||
PATH="$PWD/.venv/bin:$PATH" WGPU=1 scons -u \
|
||||
openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest
|
||||
```
|
||||
|
||||
On macOS, the build selects tinygrad's Metal backend when it is available.
|
||||
On an 8-GPU-core M5 MacBook Air, the small model's compiled policy pass measured
|
||||
about 6–11 ms, while the big model measured about 79–81 ms. The latter already
|
||||
misses the 50 ms model cadence before network and codec latency, so start with
|
||||
the small model on that class of laptop.
|
||||
|
||||
## Run
|
||||
|
||||
Find the laptop's LAN IP address that the comma device can reach. The helper can
|
||||
start while onroad: it forwards camera/state while local `modeld` remains active,
|
||||
then performs an exclusive publisher handoff after the laptop produces a fresh
|
||||
valid model:
|
||||
|
||||
```sh
|
||||
cd /data/openpilot
|
||||
python3 -m openpilot.tools.wgpu.device LAPTOP_IP
|
||||
```
|
||||
|
||||
Keep that terminal open. On the laptop, run:
|
||||
|
||||
```sh
|
||||
cd /path/to/openpilot
|
||||
python3 -m openpilot.tools.wgpu.host COMMA_IP
|
||||
```
|
||||
|
||||
Add `--big-model` after `COMMA_IP` to use the locally compiled big model.
|
||||
|
||||
The device helper sends cached `carParams` over a dedicated startup channel, so
|
||||
the host does not attach to the camera streams and accumulate stale frames while
|
||||
waiting for the periodic state bridge. Stop either side with Ctrl+C. A host
|
||||
disconnect automatically stops remote publication and restores local `modeld`
|
||||
after a one-second timeout; the device helper then stays running and waits for
|
||||
the next host session. Stop the device helper itself with Ctrl+C before changing
|
||||
branches or rebooting.
|
||||
|
||||
While WGPU is active, model lag does not create an engagement-blocking alert.
|
||||
The fixed-column diagnostics panel at the top-right of the mici onroad UI shows
|
||||
the active source (`LOCAL` or `WGPU`), remote model size (`SMALL` or `BIG`),
|
||||
total model-frame age, execution time, remaining I/O and queue time, and dropped
|
||||
frames. The home GPU icon is gray while the device helper is ready and waiting,
|
||||
and green while a valid remote model is active.
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubSocket
|
||||
|
||||
|
||||
MODEL_OUTPUTS = "modelV2,drivingModelData,cameraOdometry,modelDataV2SP"
|
||||
MODEL_PROCESSES = {"modeld", "modeld_tinygrad"}
|
||||
REMOTE_MODEL_TIMEOUT = 1.0
|
||||
WGPU_STATUS = "wgpuStatus"
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
BRIDGE = ROOT / "openpilot/cereal/messaging/bridge"
|
||||
|
||||
|
||||
def stop_process(proc: subprocess.Popen) -> None:
|
||||
proc.terminate()
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
proc.wait()
|
||||
|
||||
|
||||
def handle_sigterm(*_) -> None:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def receive_model(sock: ZmqSubSocket) -> tuple[bool, float]:
|
||||
raw = sock.receive(non_blocking=True)
|
||||
if raw is None:
|
||||
return False, float("inf")
|
||||
event = messaging.log_from_bytes(raw)
|
||||
if event.which() != "modelV2" or not event.valid:
|
||||
return True, float("inf")
|
||||
model_age = (time.monotonic_ns() - event.modelV2.timestampEof) / 1e9
|
||||
return True, model_age
|
||||
|
||||
|
||||
def receive_model_name(sock: ZmqSubSocket) -> str | None:
|
||||
raw = sock.receive(non_blocking=True)
|
||||
if raw is None:
|
||||
return None
|
||||
model_name = raw.decode(errors="replace").upper()
|
||||
return model_name if model_name in ("SMALL", "BIG") else None
|
||||
|
||||
|
||||
def wait_for_local_modeld_stop(timeout: float = 10.) -> None:
|
||||
sm = messaging.SubMaster(["managerState"])
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
sm.update(100)
|
||||
if sm.seen["managerState"]:
|
||||
running = {p.name for p in sm["managerState"].processes if p.running}
|
||||
if not running.intersection(MODEL_PROCESSES):
|
||||
return
|
||||
raise RuntimeError("timed out waiting for local modeld publisher to stop")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Route modeld traffic between this device and a wireless host.")
|
||||
parser.add_argument("host", help="Laptop IP address reachable from this device")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not BRIDGE.is_file():
|
||||
raise FileNotFoundError(f"build the cereal bridge first: {BRIDGE}")
|
||||
|
||||
params = Params()
|
||||
forward: subprocess.Popen | None = None
|
||||
reverse: subprocess.Popen | None = None
|
||||
wgpu_enabled = False
|
||||
try:
|
||||
forward = subprocess.Popen([str(BRIDGE)])
|
||||
params.put_bool("WgpuReady", True, block=True)
|
||||
car_params_pub = ZmqPubMaster([WGPU_CAR_PARAMS])
|
||||
remote_model = ZmqSubSocket("modelV2", args.host, conflate=True)
|
||||
remote_status = ZmqSubSocket(WGPU_STATUS, args.host, conflate=True)
|
||||
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||
|
||||
while True:
|
||||
# Keep local modeld publishing while a host connects and warms up.
|
||||
print(f"forwarding camera/state to {args.host}; waiting for a fresh remote model")
|
||||
model_name = None
|
||||
while True:
|
||||
if car_params is None:
|
||||
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||
if car_params is not None:
|
||||
# This dedicated conflated startup channel lets remote modeld obtain CP
|
||||
# before it connects to VisionIPC and accumulates stale frame metadata.
|
||||
car_params_pub.send_raw(WGPU_CAR_PARAMS, car_params)
|
||||
received, model_age = receive_model(remote_model)
|
||||
model_name = receive_model_name(remote_status) or model_name
|
||||
if received and 0 <= model_age < REMOTE_MODEL_TIMEOUT and model_name is not None:
|
||||
break
|
||||
if forward.poll() is not None:
|
||||
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||
time.sleep(0.05)
|
||||
|
||||
# Stop the local publisher before attaching the reverse bridge. msgq permits
|
||||
# only one publisher for each model service.
|
||||
params.put("WgpuModelName", model_name, block=True)
|
||||
params.put_bool("WgpuEnabled", True, block=True)
|
||||
wgpu_enabled = True
|
||||
wait_for_local_modeld_stop()
|
||||
reverse = subprocess.Popen([str(BRIDGE), args.host, MODEL_OUTPUTS])
|
||||
print("wgpu active; Ctrl+C or loss of the remote model restores local modeld")
|
||||
|
||||
last_remote_model = time.monotonic()
|
||||
while True:
|
||||
received, _ = receive_model(remote_model)
|
||||
if received:
|
||||
last_remote_model = time.monotonic()
|
||||
if forward.poll() is not None:
|
||||
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||
if reverse.poll() is not None:
|
||||
print(f"reverse bridge exited with status {reverse.returncode}; restoring local modeld")
|
||||
break
|
||||
if time.monotonic() - last_remote_model > REMOTE_MODEL_TIMEOUT:
|
||||
print("remote model timed out; restoring local modeld")
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
stop_process(reverse)
|
||||
reverse = None
|
||||
params.put_bool("WgpuEnabled", False, block=True)
|
||||
wgpu_enabled = False
|
||||
params.remove("WgpuModelName")
|
||||
print("wgpu disabled; local modeld restored; ready for the next host run")
|
||||
finally:
|
||||
# Stop remote publication before allowing the local publisher to restart.
|
||||
if reverse is not None and reverse.poll() is None:
|
||||
stop_process(reverse)
|
||||
if wgpu_enabled:
|
||||
params.put_bool("WgpuEnabled", False, block=True)
|
||||
params.put_bool("WgpuReady", False, block=True)
|
||||
params.remove("WgpuModelName")
|
||||
if forward is not None and forward.poll() is None:
|
||||
stop_process(forward)
|
||||
print("wgpu disabled; local modeld restored")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.tools.wgpu.zmq import ZmqPubMaster
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
CAMERASTREAM = ROOT / "openpilot/tools/camerastream/compressed_vipc.py"
|
||||
WGPU_STATUS = "wgpuStatus"
|
||||
|
||||
|
||||
def stop_process(proc: subprocess.Popen) -> None:
|
||||
try:
|
||||
os.killpg(proc.pid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
proc.wait()
|
||||
return
|
||||
try:
|
||||
proc.wait(timeout=5)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Run modeld on this host for a remote comma device.")
|
||||
parser.add_argument("device", help="comma device hostname or IP address")
|
||||
parser.add_argument("--big-model", action="store_true", help="use the locally compiled big driving model")
|
||||
args = parser.parse_args()
|
||||
|
||||
camera = subprocess.Popen([sys.executable, str(CAMERASTREAM), args.device, "--cams", "0,2"], start_new_session=True)
|
||||
model_args = [sys.executable, "-m", "openpilot.selfdrive.modeld.modeld", "--remote", args.device]
|
||||
if args.big_model:
|
||||
model_args.append("--big-model")
|
||||
model = subprocess.Popen(model_args, cwd=ROOT, start_new_session=True)
|
||||
status = ZmqPubMaster([WGPU_STATUS])
|
||||
model_name = b"BIG" if args.big_model else b"SMALL"
|
||||
|
||||
procs = {"camera bridge": camera, "modeld": model}
|
||||
try:
|
||||
while all(proc.poll() is None for proc in procs.values()):
|
||||
status.send_raw(WGPU_STATUS, model_name)
|
||||
time.sleep(0.25)
|
||||
failed_name, failed = next((name, proc) for name, proc in procs.items() if proc.poll() is not None)
|
||||
raise RuntimeError(f"wgpu {failed_name} exited with status {failed.returncode}")
|
||||
finally:
|
||||
for proc in procs.values():
|
||||
if proc.poll() is None:
|
||||
stop_process(proc)
|
||||
print("wgpu host stopped")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -0,0 +1,105 @@
|
||||
import time
|
||||
|
||||
import zmq
|
||||
|
||||
import openpilot.cereal.messaging as messaging
|
||||
|
||||
|
||||
WGPU_CAR_PARAMS = "wgpuCarParams"
|
||||
|
||||
|
||||
def service_port(endpoint: str) -> int:
|
||||
# Keep this in sync with cereal/messaging/bridge_zmq.cc.
|
||||
value = 0xcbf29ce484222325
|
||||
for char in endpoint.encode():
|
||||
value ^= char
|
||||
value = (value * 0x100000001b3) & 0xffffffffffffffff
|
||||
return 8023 + (value % (65535 - 8023))
|
||||
|
||||
|
||||
class ZmqSubSocket:
|
||||
def __init__(self, endpoint: str, address: str, conflate: bool = False):
|
||||
self.context = zmq.Context()
|
||||
self.socket = self.context.socket(zmq.SUB)
|
||||
self.socket.setsockopt(zmq.SUBSCRIBE, b"")
|
||||
self.socket.setsockopt(zmq.RECONNECT_IVL_MAX, 500)
|
||||
if conflate:
|
||||
self.socket.setsockopt(zmq.CONFLATE, 1)
|
||||
self.socket.connect(f"tcp://{address}:{service_port(endpoint)}")
|
||||
|
||||
def receive(self, non_blocking: bool = False) -> bytes | None:
|
||||
try:
|
||||
return self.socket.recv(flags=zmq.NOBLOCK if non_blocking else 0)
|
||||
except zmq.Again:
|
||||
return None
|
||||
|
||||
def drain(self, wait_for_one: bool = False) -> list[bytes]:
|
||||
messages = []
|
||||
if wait_for_one:
|
||||
message = self.receive()
|
||||
if message is not None:
|
||||
messages.append(message)
|
||||
while (message := self.receive(non_blocking=True)) is not None:
|
||||
messages.append(message)
|
||||
return messages
|
||||
|
||||
def close(self) -> None:
|
||||
self.socket.close(linger=0)
|
||||
self.context.term()
|
||||
|
||||
|
||||
class ZmqSubMaster:
|
||||
def __init__(self, services: list[str], address: str):
|
||||
self.services = services
|
||||
self.sockets = {service: ZmqSubSocket(service, address, conflate=True) for service in services}
|
||||
self.poller = zmq.Poller()
|
||||
self.socket_to_service = {}
|
||||
for service, sub in self.sockets.items():
|
||||
self.poller.register(sub.socket, zmq.POLLIN)
|
||||
self.socket_to_service[sub.socket] = service
|
||||
|
||||
self.data = {service: getattr(messaging.new_message(service).as_reader(), service) for service in services}
|
||||
self.seen = dict.fromkeys(services, False)
|
||||
self.updated = dict.fromkeys(services, False)
|
||||
self.recv_frame = dict.fromkeys(services, 0)
|
||||
self.frame = -1
|
||||
|
||||
def __getitem__(self, service: str):
|
||||
return self.data[service]
|
||||
|
||||
def update(self, timeout: int = 100) -> None:
|
||||
self.frame += 1
|
||||
self.updated = dict.fromkeys(self.services, False)
|
||||
for socket, _ in self.poller.poll(timeout):
|
||||
service = self.socket_to_service[socket]
|
||||
raw = self.sockets[service].receive(non_blocking=True)
|
||||
if raw is None:
|
||||
continue
|
||||
event = messaging.log_from_bytes(raw)
|
||||
self.data[service] = getattr(event, service)
|
||||
self.seen[service] = True
|
||||
self.updated[service] = True
|
||||
self.recv_frame[service] = self.frame
|
||||
|
||||
def close(self) -> None:
|
||||
for sub in self.sockets.values():
|
||||
self.poller.unregister(sub.socket)
|
||||
sub.close()
|
||||
|
||||
|
||||
class ZmqPubMaster:
|
||||
def __init__(self, services: list[str]):
|
||||
context = zmq.Context.instance()
|
||||
self.sockets = {}
|
||||
for service in services:
|
||||
socket = context.socket(zmq.PUB)
|
||||
socket.bind(f"tcp://*:{service_port(service)}")
|
||||
self.sockets[service] = socket
|
||||
# Give already-running bridge subscribers time to finish their handshake.
|
||||
time.sleep(0.1)
|
||||
|
||||
def send(self, service: str, message) -> None:
|
||||
self.sockets[service].send(message.to_bytes(), flags=zmq.NOBLOCK)
|
||||
|
||||
def send_raw(self, service: str, data: bytes) -> None:
|
||||
self.sockets[service].send(data, flags=zmq.NOBLOCK)
|
||||
Reference in New Issue
Block a user