From d40df6f829c18bf17ae2e4a825220778e2780e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Wed, 26 Aug 2026 12:06:53 -0700 Subject: [PATCH 01/53] modeld: fall back on invalid big model outputs (#38700) --- openpilot/selfdrive/modeld/modeld.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 7f049912dd..783231b7b8 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -163,7 +163,7 @@ class ModelState: return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None: + inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -189,9 +189,7 @@ class ModelState: ) model_output = outs.numpy()[0] if self.usbgpu and not np.all(np.isfinite(model_output)): - # TODO remove with prev_feat - cloudlog.error("model output not finite, dropping frame") - return None + raise RuntimeError("model output not finite") outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] From 980fb79c1af4980a45c291107fd7f71f558fd556 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 12:21:14 -0700 Subject: [PATCH 02/53] update orange GPU icon (#38701) mici: update failed eGPU icon --- openpilot/selfdrive/assets/icons_mici/egpu_orange.png | 4 ++-- openpilot/selfdrive/ui/mici/onroad/hud_renderer.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png b/openpilot/selfdrive/assets/icons_mici/egpu_orange.png index c2fc06e34e..d3982abf87 100644 --- a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png +++ b/openpilot/selfdrive/assets/icons_mici/egpu_orange.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58bd6155433f623b1f75d134bd8ca4745d9aa71f6767eb807cdbcf7deb3089a1 -size 10876 +oid sha256:845c40ff0d37612e8f2f482a36845744b5ae91ce2fcfc8117990d7d278b59820 +size 13079 diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index 0d1532057e..ad8f67809c 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -126,7 +126,7 @@ class HudRenderer(Widget): self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) - self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 60, 44) + self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 75, 44) self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) self._egpu_icon: rl.Texture | None = None From 63548ce10d10d5725cbcce559d23b2e47d82a281 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 15:20:04 -0700 Subject: [PATCH 03/53] bump tinygrad (#38702) --- tinygrad_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinygrad_repo b/tinygrad_repo index 138fb4a783..c015351ac5 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 138fb4a783d82f4e877ad2fe3692aaf8d1de2e46 +Subproject commit c015351ac5c00c10c58dbdaf530ef5b2883ab948 From 5cfdb2f4dac82ba89ebf310db13ff08d49cf4916 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 15:42:59 -0700 Subject: [PATCH 04/53] rename usbgpu to chestnut (#38703) chestnut: rename eGPU interfaces --- openpilot/common/params_keys.h | 4 +- .../icons_mici/{egpu.png => chestnut.png} | 0 ...{egpu_crossed.png => chestnut_crossed.png} | 0 .../{egpu_gray.png => chestnut_gray.png} | 0 .../{egpu_green.png => chestnut_green.png} | 0 .../{egpu_orange.png => chestnut_orange.png} | 0 openpilot/selfdrive/modeld/SConscript | 26 +++++------ .../selfdrive/modeld/dmonitoringmodeld.py | 2 +- openpilot/selfdrive/modeld/helpers.py | 14 +++--- openpilot/selfdrive/modeld/modeld.py | 40 ++++++++--------- openpilot/selfdrive/selfdrived/selfdrived.py | 8 ++-- openpilot/selfdrive/ui/mici/layouts/home.py | 12 ++--- .../selfdrive/ui/mici/onroad/hud_renderer.py | 44 +++++++++---------- openpilot/selfdrive/ui/ui_state.py | 22 +++++----- openpilot/system/hardware/hardwared.py | 4 +- 15 files changed, 88 insertions(+), 88 deletions(-) rename openpilot/selfdrive/assets/icons_mici/{egpu.png => chestnut.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_crossed.png => chestnut_crossed.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_gray.png => chestnut_gray.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_green.png => chestnut_green.png} (100%) rename openpilot/selfdrive/assets/icons_mici/{egpu_orange.png => chestnut_orange.png} (100%) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 2a49690b7e..21b7463d92 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -127,7 +127,7 @@ inline static std::unordered_map keys = { {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, - {"UsbGpuActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, - {"UsbGpuLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/assets/icons_mici/egpu.png b/openpilot/selfdrive/assets/icons_mici/chestnut.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu.png rename to openpilot/selfdrive/assets/icons_mici/chestnut.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_crossed.png b/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_crossed.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_gray.png b/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_gray.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_gray.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_green.png b/openpilot/selfdrive/assets/icons_mici/chestnut_green.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_green.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_green.png diff --git a/openpilot/selfdrive/assets/icons_mici/egpu_orange.png b/openpilot/selfdrive/assets/icons_mici/chestnut_orange.png similarity index 100% rename from openpilot/selfdrive/assets/icons_mici/egpu_orange.png rename to openpilot/selfdrive/assets/icons_mici/chestnut_orange.png diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index 30a31aae27..f046be9915 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -7,7 +7,7 @@ from openpilot.common.file_chunker import chunk_file, get_chunk_targets, get_exi from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE from openpilot.selfdrive.modeld.constants import ModelConstants -from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, usbgpu_present, modeld_pkl_path +from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path CAMERA_CONFIGS = [ @@ -36,18 +36,18 @@ else: tg_devices = { # which device to put jit inputs to at runtime 'openpilot.selfdrive.modeld.modeld': { 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, - 'usbgpu': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} + 'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} }, 'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'default': {'DEV': tg_backend} }, } -USBGPU = usbgpu_present() -if USBGPU: - usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' +CHESTNUT = chestnut_present() +if CHESTNUT: + chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it - usbgpu_lock = File("models/.usb_gpu.lock").abspath + chestnut_lock = File("models/.chestnut.lock").abspath def write_tg_devices(target, source, env): with open(str(target[0]), "w") as f: @@ -73,10 +73,10 @@ 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]: - 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) +for chestnut in [False, True] if CHESTNUT else [False]: + target_pkl_path = File(modeld_pkl_path(chestnut)).abspath + # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut + file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut 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) # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. @@ -103,14 +103,14 @@ for usbgpu in [False, True] if USBGPU else [False]: chunk_file(pkl, chunks) def do_chunk(target, source, env, pkl=target_pkl_path, chunks=chunk_targets): chunk_file(pkl, chunks) - actions = Action(do_compile, " [USBGPU] $TARGET") if usbgpu else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] + actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] node = lenv.Command( chunk_targets, tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], actions, ) - if usbgpu: - lenv.SideEffect(usbgpu_lock, node) + if chestnut: + lenv.SideEffect(chestnut_lock, node) # get model metadata fn = File(f"models/dmonitoring_model").abspath diff --git a/openpilot/selfdrive/modeld/dmonitoringmodeld.py b/openpilot/selfdrive/modeld/dmonitoringmodeld.py index 67161c35bd..4010725b89 100755 --- a/openpilot/selfdrive/modeld/dmonitoringmodeld.py +++ b/openpilot/selfdrive/modeld/dmonitoringmodeld.py @@ -29,7 +29,7 @@ class ModelState: output: np.ndarray def __init__(self, cam_w: int, cam_h: int): - self.DEV = get_tg_input_devices(PROCESS_NAME, usbgpu=False)['DEV'] + self.DEV = get_tg_input_devices(PROCESS_NAME, chestnut=False)['DEV'] with open(METADATA_PATH, 'rb') as f: model_metadata = pickle.load(f) self.input_shapes = model_metadata['input_shapes'] diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 37ab0b26d7..84236f3fd0 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -13,12 +13,12 @@ MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' -def get_tg_input_devices(process_name: str, usbgpu: bool): +def get_tg_input_devices(process_name: str, chestnut: bool): with open(TG_INPUT_DEVICES_PATH) as f: - return json.load(f)[process_name]['default' if not usbgpu else 'usbgpu'] + return json.load(f)[process_name]['default' if not chestnut else 'chestnut'] -def modeld_pkl_path(usbgpu: bool): - prefix = 'big_' if usbgpu else '' +def modeld_pkl_path(chestnut: bool): + prefix = 'big_' if chestnut else '' return MODELS_DIR / f'{prefix}driving_tinygrad.pkl' def dump_oob(obj, f): @@ -45,7 +45,7 @@ def load_oob(f): yield pb return pickle.load(io.BytesIO(opcodes), buffers=buffers()) -def usbgpu_present() -> bool: +def chestnut_present() -> bool: for d in USB_DEVICES_PATH.glob("*"): try: usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) @@ -56,5 +56,5 @@ def usbgpu_present() -> bool: pass return False -def usbgpu_compiled() -> bool: - return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file() +def chestnut_compiled() -> bool: + return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 783231b7b8..7a76841516 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 from functools import cached_property import os -os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom +os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device import struct @@ -30,7 +30,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -137,17 +137,17 @@ class FrameMeta: class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse - def __init__(self, cam_w: int, cam_h: int, usbgpu: bool): - input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu) + def __init__(self, cam_w: int, cam_h: int, chestnut: bool): + input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) 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(chestnut))) 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.usbgpu = usbgpu + self.chestnut = chestnut 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) @@ -188,7 +188,7 @@ class ModelState: **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) model_output = outs.numpy()[0] - if self.usbgpu and not np.all(np.isfinite(model_output)): + if self.chestnut and not np.all(np.isfinite(model_output)): raise RuntimeError("model output not finite") outputs_dict = self.parser.parse_outputs(self.slice_outputs(model_output, self.output_slices)) self.npy['prev_feat'][:] = model_output[self.output_slices['hidden_state']] @@ -211,12 +211,12 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - USBGPU = usbgpu_present() and usbgpu_compiled() - if USBGPU: + CHESTNUT = chestnut_present() and chestnut_compiled() + if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() - params.put_bool("UsbGpuLoading", USBGPU) - params.remove("UsbGpuActive") + params.put_bool("ChestnutLoading", CHESTNUT) + params.remove("ChestnutActive") config_realtime_process(7, 54) @@ -246,7 +246,7 @@ def main(demo=False): st = time.monotonic() cloudlog.warning("loading model") model = None - if USBGPU: + if CHESTNUT: big_model = None def load_big(): nonlocal big_model @@ -260,22 +260,22 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model - params.put_bool("UsbGpuActive", model is not None) + params.put_bool("ChestnutActive", model is not None) - small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None + small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: model = small_model - params.put_bool("UsbGpuLoading", False) + params.put_bool("ChestnutLoading", False) cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if USBGPU else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutState(pm, model.usbgpu) if USBGPU else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -385,11 +385,11 @@ def main(demo=False): try: model_output = model.run(bufs, transforms, inputs) except Exception: - if not params.get_bool("UsbGpuActive"): + if not params.get_bool("ChestnutActive"): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") - params.put_bool("UsbGpuActive", False) + params.put_bool("ChestnutActive", False) model = small_model if chestnut_state is not None: chestnut_state.big = False @@ -408,7 +408,7 @@ def main(demo=False): fill_model_msg(modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, extrinsics_calibration_seen) - modelv2_send.modelV2.big = model.usbgpu + modelv2_send.modelV2.big = model.chestnut desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 1f1f6f7349..638448d897 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -159,17 +159,17 @@ class SelfdriveD: self.events.add(EventName.joystickDebug) self.startup_event = None - loading = self.params.get_bool("UsbGpuLoading") + loading = self.params.get_bool("ChestnutLoading") if self.big_model_loading and not loading: self.big_model_ready_t = time.monotonic() self.big_model_loading = loading if self.big_model_loading: self.events.add(EventName.bigModelLoading) - big_active = self.params.get("UsbGpuActive") - usbgpu_present = self.sm['deviceState'].chestnutPresent + big_active = self.params.get("ChestnutActive") + chestnut_present = self.sm['deviceState'].chestnutPresent model_unavailable = big_active is True and self.sm.seen['modelV2'] and not self.sm.alive['modelV2'] - big_failed = big_active is False or model_unavailable or (self.big_model_active and not usbgpu_present) + big_failed = big_active is False or model_unavailable or (self.big_model_active and not chestnut_present) if big_failed and not self.big_model_failed: self.events.add(EventName.bigModelFailed) self.big_model_failed = big_failed diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 8b21a1a98c..50dc95901f 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._egpu_icon = IconWidget("icons_mici/egpu_green.png", (50, 37)) - self._egpu_icon_gray = IconWidget("icons_mici/egpu_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (50, 37)) + self._chestnut_icon_gray = IconWidget("icons_mici/chestnut_gray.png", (50, 37)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -150,8 +150,8 @@ class MiciHomeLayout(Widget): IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), self._experimental_icon, - self._egpu_icon, - self._egpu_icon_gray, + self._chestnut_icon, + self._chestnut_icon_gray, self._body_icon, self._mic_icon, ], spacing=18) @@ -248,8 +248,8 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._egpu_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.usbgpu_compiled) - self._egpu_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.usbgpu_compiled) + self._chestnut_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.chestnut_compiled) + self._chestnut_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.chestnut_compiled) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index ad8f67809c..e844f5e9e7 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -108,7 +108,7 @@ class HudRenderer(Widget): self.v_ego_cluster_seen: bool = False self._engaged: bool = False self._small_model_engaged: bool = False - self._egpu_fade_time: float = 0 + self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True self._show_wheel_critical = False @@ -124,17 +124,17 @@ class HudRenderer(Widget): self._txt_wheel: rl.Texture = gui_app.texture('icons_mici/wheel.png', 50, 50) self._txt_wheel_critical: rl.Texture = gui_app.texture('icons_mici/wheel_critical.png', 50, 50) self._txt_exclamation_point: rl.Texture = gui_app.texture('icons_mici/exclamation_point.png', 9, 44) - self._txt_egpu: rl.Texture = gui_app.texture('icons_mici/egpu.png', 60, 44) - self._txt_egpu_green: rl.Texture = gui_app.texture('icons_mici/egpu_green.png', 60, 44) - self._txt_egpu_orange: rl.Texture = gui_app.texture('icons_mici/egpu_orange.png', 75, 44) - self._txt_egpu_crossed: rl.Texture = gui_app.texture('icons_mici/egpu_crossed.png', 60, 52) - self._egpu_icon: rl.Texture | None = None + self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) + self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) + self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) + self._txt_chestnut_crossed: rl.Texture = gui_app.texture('icons_mici/chestnut_crossed.png', 60, 52) + self._chestnut_icon: rl.Texture | None = None self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) self._set_speed_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) - self._egpu_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) + self._chestnut_alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps) def set_wheel_critical_icon(self, critical: bool): """Set the wheel icon to critical or normal state.""" @@ -165,11 +165,11 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.usbgpu_loading and ui_state.usbgpu_active is not True and + if (engaged and not self._engaged and not ui_state.chestnut_loading and ui_state.chestnut_active is not True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): self._small_model_engaged = True if engaged != self._engaged: - self._egpu_fade_time = rl.get_time() if engaged else 0 + self._chestnut_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() self._engaged = engaged @@ -191,7 +191,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.usbgpu and ui_state.usbgpu_compiled: + if ui_state.chestnut and ui_state.chestnut_compiled: self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +200,30 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.usbgpu_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.usbgpu_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and + big_failed = (ui_state.chestnut_active is False or not ui_state.sm['deviceState'].chestnutPresent or + (ui_state.chestnut_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and not ui_state.sm.alive['modelV2']) or - (ui_state.usbgpu_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) + (ui_state.chestnut_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) self._small_model_engaged &= big_failed - loading = ui_state.usbgpu_loading or (ui_state.usbgpu_active is None and not big_failed) + loading = ui_state.chestnut_loading or (ui_state.chestnut_active is None and not big_failed) if loading: pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) - icon = self._txt_egpu + icon = self._txt_chestnut opacity = 0.35 + 0.65 * pulse elif self._small_model_engaged: - icon = self._txt_egpu_crossed + icon = self._txt_chestnut_crossed opacity = 0.65 elif big_failed: - icon = self._txt_egpu_orange + icon = self._txt_chestnut_orange opacity = 1.0 else: - icon = self._txt_egpu_green + icon = self._txt_chestnut_green opacity = 1.0 - if icon is not self._egpu_icon: - self._egpu_fade_time = rl.get_time() - self._egpu_icon = icon - alpha = self._egpu_alpha_filter.update(loading or 0 < rl.get_time() - self._egpu_fade_time < SET_SPEED_PERSISTENCE) + if icon is not self._chestnut_icon: + self._chestnut_fade_time = rl.get_time() + self._chestnut_icon = icon + alpha = self._chestnut_alpha_filter.update(loading or 0 < rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 42e8086351..63a71758fa 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,7 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC -from openpilot.selfdrive.modeld.helpers import usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import chestnut_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 PARAM_UPDATE_TIME = 1 / 5.0 @@ -77,10 +77,10 @@ class UIState: self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.usbgpu: bool = False - self.usbgpu_compiled: bool = usbgpu_compiled() - self.usbgpu_active: bool | None = self.params.get("UsbGpuActive") - self.usbgpu_loading: bool = self.params.get_bool("UsbGpuLoading") + self.chestnut: bool = False + self.chestnut_compiled: bool = chestnut_compiled() + self.chestnut_active: bool | None = None + self.chestnut_loading: bool = False self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -208,12 +208,12 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep usbgpu UI active until offroad transition when gpu disappears - self.usbgpu = self.sm["deviceState"].chestnutPresent or (self.usbgpu and self.started) - if not self.usbgpu_compiled: - self.usbgpu_compiled = usbgpu_compiled() - self.usbgpu_active = self.params.get("UsbGpuActive") - self.usbgpu_loading = self.params.get_bool("UsbGpuLoading") + # keep chestnut UI active until offroad transition when gpu disappears + self.chestnut = self.sm["deviceState"].chestnutPresent or (self.chestnut and self.started) + if not self.chestnut_compiled: + self.chestnut_compiled = chestnut_compiled() + self.chestnut_active = self.params.get("ChestnutActive") + self.chestnut_loading = self.params.get_bool("ChestnutLoading") class Device: diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 16eb18d153..64ca4415d1 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,7 +16,7 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, usbgpu_compiled +from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR @@ -238,7 +238,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or usbgpu_compiled() + big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) From fa75fdd852d29eedf454afdcfa76c34451ada8e9 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Wed, 26 Aug 2026 17:57:55 -0700 Subject: [PATCH 05/53] chestnut stats: overlap with gpu work (#38704) * chestnut stats: overlap with gpu work * ci --------- Co-authored-by: elkoled --- openpilot/selfdrive/modeld/modeld.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 7a76841516..0db66ed880 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -1,4 +1,6 @@ #!/usr/bin/env python3 +from collections.abc import Callable +import ctypes from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom @@ -90,8 +92,10 @@ class ChestnutState: if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: smu = Device["AMD"].iface.dev_impl.smu + metrics_t = smu.smu_mod.SmuMetricsExternal_t smu._send_msg(smu.smu_mod.PPSMC_MSG_TransferTableSmu2Dram, smu.smu_mod.TABLE_SMU_METRICS, timeout=100) - metrics = smu.read_table(smu.smu_mod.SmuMetricsExternal_t, smu.smu_mod.TABLE_SMU_METRICS).SmuMetrics + metrics_buf = bytearray(smu.adev.vram.view(smu.driver_table_paddr, ctypes.sizeof(metrics_t))[:]) + metrics = metrics_t.from_buffer(metrics_buf).SmuMetrics self.metrics = {'tempC': metrics.AvgTemperature[smu.smu_mod.TEMP_HOTSPOT], 'memoryTempC': metrics.AvgTemperature[smu.smu_mod.TEMP_MEM], 'powerDrawW': metrics.AverageSocketPower, @@ -163,7 +167,7 @@ class ModelState: return parsed_model_outputs def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: for key in bufs.keys(): ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data yuv_size = self.frame_buf_params[key][3] @@ -187,6 +191,8 @@ class ModelState: outs, = self.run_policy( **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped ) + if after_enqueue is not None: + after_enqueue() model_output = outs.numpy()[0] if self.chestnut and not np.all(np.isfinite(model_output)): raise RuntimeError("model output not finite") @@ -383,7 +389,9 @@ def main(demo=False): mt1 = time.perf_counter() try: - model_output = model.run(bufs, transforms, inputs) + send_chestnut = (chestnut_state is not None and + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): raise @@ -425,10 +433,6 @@ def main(demo=False): pm.send('cameraOdometry', posenet_send) last_vipc_frame_id = meta_main.frame_id - if chestnut_state is not None and run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0: - chestnut_state.send() - - if __name__ == "__main__": try: import argparse From 4a13639cfd122ccb9113a4d6ce225dcbd8e61914 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Wed, 26 Aug 2026 19:12:49 -0700 Subject: [PATCH 06/53] reduce chestnut states (#38705) ui: unify chestnut status presentation --- .../assets/icons_mici/chestnut_crossed.png | 3 -- .../assets/icons_mici/chestnut_gray.png | 3 -- openpilot/selfdrive/ui/mici/layouts/home.py | 12 +++--- .../selfdrive/ui/mici/onroad/hud_renderer.py | 37 ++++++------------ openpilot/selfdrive/ui/ui_state.py | 38 +++++++++++++++++-- 5 files changed, 53 insertions(+), 40 deletions(-) delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png delete mode 100644 openpilot/selfdrive/assets/icons_mici/chestnut_gray.png diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png b/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png deleted file mode 100644 index 4fc5decb59..0000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_crossed.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8a8c5fece2a1c7587feb41cbe04c6aee08e768ecd9b5d00da6af9832a4ccc842 -size 2034 diff --git a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png b/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png deleted file mode 100644 index a6aeb84689..0000000000 --- a/openpilot/selfdrive/assets/icons_mici/chestnut_gray.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7409c53d7c72681c24982fd83b56ce70f80797c9c0f936d9296a5c18557ac472 -size 7279 diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 50dc95901f..519580925f 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES HEAD_BUTTON_FONT_SIZE = 40 @@ -139,8 +139,8 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) - self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (50, 37)) - self._chestnut_icon_gray = IconWidget("icons_mici/chestnut_gray.png", (50, 37)) + self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -151,7 +151,7 @@ class MiciHomeLayout(Widget): NetworkIcon(), self._experimental_icon, self._chestnut_icon, - self._chestnut_icon_gray, + self._chestnut_failed_icon, self._body_icon, self._mic_icon, ], spacing=18) @@ -248,8 +248,8 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.sm["deviceState"].chestnutPresent and ui_state.chestnut_compiled) - self._chestnut_icon_gray.set_visible(ui_state.sm["deviceState"].chestnutPresent and not ui_state.chestnut_compiled) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py index e844f5e9e7..3bb2f70b84 100644 --- a/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/hud_renderer.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.common.constants import CV from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus, ChestnutState from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached @@ -107,7 +107,6 @@ class HudRenderer(Widget): self.speed: float = 0.0 self.v_ego_cluster_seen: bool = False self._engaged: bool = False - self._small_model_engaged: bool = False self._chestnut_fade_time: float = 0 self._can_draw_top_icons = True @@ -127,9 +126,7 @@ class HudRenderer(Widget): self._txt_chestnut: rl.Texture = gui_app.texture('icons_mici/chestnut.png', 60, 44) self._txt_chestnut_green: rl.Texture = gui_app.texture('icons_mici/chestnut_green.png', 60, 44) self._txt_chestnut_orange: rl.Texture = gui_app.texture('icons_mici/chestnut_orange.png', 75, 44) - self._txt_chestnut_crossed: rl.Texture = gui_app.texture('icons_mici/chestnut_crossed.png', 60, 52) self._chestnut_icon: rl.Texture | None = None - self._wheel_alpha_filter = FirstOrderFilter(0, 0.05, 1 / gui_app.target_fps) self._wheel_y_filter = FirstOrderFilter(0, 0.1, 1 / gui_app.target_fps) @@ -165,13 +162,10 @@ class HudRenderer(Widget): controls_state.deprecated.vCruise if v_cruise_cluster == 0.0 else v_cruise_cluster ) engaged = sm['selfdriveState'].enabled - if (engaged and not self._engaged and not ui_state.chestnut_loading and ui_state.chestnut_active is not True and - ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame): - self._small_model_engaged = True - if engaged != self._engaged: - self._chestnut_fade_time = rl.get_time() if engaged else 0 if (set_speed != self.set_speed and engaged) or (engaged and not self._engaged): self._set_speed_changed_time = rl.get_time() + if engaged != self._engaged: + self._chestnut_fade_time = rl.get_time() if engaged else 0 self._engaged = engaged self.set_speed = set_speed self.is_cruise_set = 0 < self.set_speed < SET_SPEED_NA @@ -191,8 +185,7 @@ class HudRenderer(Widget): if self.is_cruise_set: self._draw_set_speed(rect) - if ui_state.chestnut and ui_state.chestnut_compiled: - self._draw_model_source(rect) + self._draw_model_source(rect) self._draw_steering_wheel(rect) @@ -200,30 +193,24 @@ class HudRenderer(Widget): if ui_state.sm.recv_frame['selfdriveState'] < ui_state.started_frame: return - big_failed = (ui_state.chestnut_active is False or not ui_state.sm['deviceState'].chestnutPresent or - (ui_state.chestnut_active is True and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame and - not ui_state.sm.alive['modelV2']) or - (ui_state.chestnut_active is None and ui_state.sm.recv_frame['modelV2'] > ui_state.started_frame)) - self._small_model_engaged &= big_failed - loading = ui_state.chestnut_loading or (ui_state.chestnut_active is None and not big_failed) + loading = ui_state.chestnut_state == ChestnutState.LOADING if loading: - pulse = 0.5 - 0.5 * math.cos(rl.get_time() * 6.0) icon = self._txt_chestnut - opacity = 0.35 + 0.65 * pulse - elif self._small_model_engaged: - icon = self._txt_chestnut_crossed - opacity = 0.65 - elif big_failed: + opacity = 0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0)) + elif ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED): icon = self._txt_chestnut_orange opacity = 1.0 - else: + elif ui_state.chestnut_state == ChestnutState.ACTIVE: icon = self._txt_chestnut_green opacity = 1.0 + else: + return if icon is not self._chestnut_icon: self._chestnut_fade_time = rl.get_time() self._chestnut_icon = icon - alpha = self._chestnut_alpha_filter.update(loading or 0 < rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE) + visible = loading or rl.get_time() - self._chestnut_fade_time < SET_SPEED_PERSISTENCE + alpha = self._chestnut_alpha_filter.update(visible) if alpha < 1e-2: return diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index 63a71758fa..c169cbeaff 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -24,6 +24,15 @@ class UIStatus(Enum): OVERRIDE = "override" +class ChestnutState(Enum): + DISCONNECTED = "disconnected" + UNCOMPILED = "uncompiled" + READY = "ready" + LOADING = "loading" + ACTIVE = "active" + FAILED = "failed" + + class UIState: _instance: 'UIState | None' = None @@ -77,10 +86,11 @@ class UIState: self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM") self.experimental_mode: bool = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed: bool = self.params.get_bool("ExperimentalModeConfirmed") - self.chestnut: bool = False + self.chestnut_present: bool = False self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None self.chestnut_loading: bool = False + self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False self.recording_audio: bool = False @@ -126,6 +136,7 @@ class UIState: self.sm.update(0) self._update_state() self._update_status() + self._update_chestnut_state() device.update() def _params_refresh_worker(self): @@ -186,12 +197,35 @@ class UIState: self.status = UIStatus.DISENGAGED self.started_frame = self.sm.frame self.started_time = time.monotonic() + self.chestnut_present = self.sm["deviceState"].chestnutPresent for callback in self._offroad_transition_callbacks: callback() self._started_prev = self.started + def _update_chestnut_state(self) -> None: + detected = self.sm["deviceState"].chestnutPresent + if not self.started: + self.chestnut_present = detected + self.chestnut_state = (ChestnutState.READY if detected and self.chestnut_compiled else + ChestnutState.UNCOMPILED if detected else ChestnutState.DISCONNECTED) + return + + model_seen = self.sm.recv_frame["modelV2"] > self.started_frame + if not self.chestnut_present: + self.chestnut_state = ChestnutState.DISCONNECTED + elif not self.chestnut_compiled: + self.chestnut_state = ChestnutState.UNCOMPILED + elif self.chestnut_state == ChestnutState.FAILED or not detected or (model_seen and (not self.sm.alive["modelV2"] or not self.sm["modelV2"].big)): + self.chestnut_state = ChestnutState.FAILED + elif self.chestnut_loading or not model_seen: + self.chestnut_state = ChestnutState.LOADING + elif self.chestnut_active is False: + self.chestnut_state = ChestnutState.FAILED + else: + self.chestnut_state = ChestnutState.ACTIVE + def update_params(self) -> None: # For slower operations # Update longitudinal control state @@ -208,8 +242,6 @@ class UIState: self.always_on_dm = self.params.get_bool("AlwaysOnDM") self.experimental_mode = self.params.get_bool("ExperimentalMode") self.experimental_mode_confirmed = self.params.get_bool("ExperimentalModeConfirmed") - # keep chestnut UI active until offroad transition when gpu disappears - self.chestnut = self.sm["deviceState"].chestnutPresent or (self.chestnut and self.started) if not self.chestnut_compiled: self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive") From 31ea1850f73a0f8049dd5a655b7bc1f03d5389be Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:54:52 -0700 Subject: [PATCH 07/53] ui: remove raygui usage (#38708) * ui: remove raygui usage * match previous gui_text_box line spacing * Revert "match previous gui_text_box line spacing" This reverts commit ffd2fe31725c6d50bffaebc621c1e170d0926c66. * Reapply "match previous gui_text_box line spacing" This reverts commit d41404f09607e225f43868f7747f22dc0bb2cf16. --- openpilot/selfdrive/ui/body/layouts/onroad.py | 6 +- openpilot/selfdrive/ui/layouts/home.py | 4 +- openpilot/selfdrive/ui/layouts/onboarding.py | 8 +- openpilot/selfdrive/ui/mici/layouts/home.py | 6 +- .../ui/mici/layouts/offroad_alerts.py | 14 +-- .../selfdrive/ui/mici/layouts/onboarding.py | 4 +- .../ui/mici/onroad/alert_renderer.py | 6 +- .../ui/mici/onroad/augmented_road_view.py | 6 +- .../ui/mici/onroad/cabin_camera_dialog.py | 18 ++-- openpilot/selfdrive/ui/mici/widgets/button.py | 14 +-- .../selfdrive/ui/onroad/alert_renderer.py | 10 +-- .../ui/onroad/cabin_camera_dialog.py | 4 +- openpilot/system/ui/lib/application.py | 23 ++--- openpilot/system/ui/lib/utils.py | 19 ----- openpilot/system/ui/mici_setup.py | 12 +-- openpilot/system/ui/mici_updater.py | 4 +- openpilot/system/ui/tici_reset.py | 5 +- openpilot/system/ui/tici_setup.py | 26 +++--- openpilot/system/ui/tici_updater.py | 9 +- openpilot/system/ui/widgets/button.py | 6 +- openpilot/system/ui/widgets/keyboard.py | 6 +- openpilot/system/ui/widgets/label.py | 85 ++++++------------- openpilot/system/ui/widgets/list_view.py | 10 +-- openpilot/system/ui/widgets/network.py | 10 +-- openpilot/system/ui/widgets/option_dialog.py | 4 +- openpilot/system/ui/widgets/slider.py | 6 +- 26 files changed, 140 insertions(+), 185 deletions(-) delete mode 100644 openpilot/system/ui/lib/utils.py diff --git a/openpilot/selfdrive/ui/body/layouts/onroad.py b/openpilot/selfdrive/ui/body/layouts/onroad.py index a48e525628..94f1342abd 100644 --- a/openpilot/selfdrive/ui/body/layouts/onroad.py +++ b/openpilot/selfdrive/ui/body/layouts/onroad.py @@ -1,7 +1,7 @@ import time import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.selfdrive.ui.ui_state import ui_state @@ -26,8 +26,8 @@ class BodyLayout(Widget): self._last_input_time = time.monotonic() self._was_active = False self._offroad_label = UnifiedLabel("turn on ignition to use", 95 if gui_app.big_ui() else 45, FontWeight.DISPLAY, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) def draw_dot_grid(self, rect: rl.Rectangle, dots: list[tuple[int, int]], color: rl.Color): spacing = min(rect.height / GRID_ROWS, rect.width / GRID_COLS) diff --git a/openpilot/selfdrive/ui/layouts/home.py b/openpilot/selfdrive/ui/layouts/home.py index 183c2d4588..613fc3de3d 100644 --- a/openpilot/selfdrive/ui/layouts/home.py +++ b/openpilot/selfdrive/ui/layouts/home.py @@ -8,7 +8,7 @@ from openpilot.selfdrive.ui.widgets.exp_mode_button import ExperimentalModeButto from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment from openpilot.system.ui.lib.multilang import tr, trn from openpilot.system.ui.widgets.label import gui_label from openpilot.system.ui.widgets import Widget @@ -178,7 +178,7 @@ class HomeLayout(Widget): version_rect = rl.Rectangle(self.header_rect.x + self.header_rect.width - version_text_width, self.header_rect.y, version_text_width, self.header_rect.height) - gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + gui_label(version_rect, self._version_text, 48, rl.WHITE, alignment=TextAlignment.RIGHT) def _render_home_content(self): self._render_left_column() diff --git a/openpilot/selfdrive/ui/layouts/onboarding.py b/openpilot/selfdrive/ui/layouts/onboarding.py index 97ca89b50c..91b674d52d 100644 --- a/openpilot/selfdrive/ui/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/layouts/onboarding.py @@ -5,7 +5,7 @@ from enum import IntEnum import pyray as rl from openpilot.common.basedir import BASEDIR -from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -112,9 +112,9 @@ class TermsPage(Widget): self._on_accept = on_accept self._on_decline = on_decline - self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + self._title = Label(tr("Welcome to openpilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.LEFT) self._desc = Label(tr("You must accept the Terms and Conditions to use openpilot. Read the latest terms at https://comma.ai/terms before continuing."), - font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT) self._decline_btn = Button(tr("Decline"), click_callback=on_decline) self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept) @@ -147,7 +147,7 @@ class DeclinePage(Widget): def __init__(self, back_callback=None): super().__init__() self._text = Label(tr("You must accept the Terms and Conditions in order to use openpilot."), - font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT) + font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=TextAlignment.LEFT) self._back_btn = Button(tr("Back"), click_callback=back_callback) self._uninstall_btn = Button(tr("Decline, uninstall openpilot"), button_style=ButtonStyle.DANGER, click_callback=self._on_uninstall_clicked) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 519580925f..64f2961326 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -8,7 +8,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.layouts import HBoxLayout from openpilot.system.ui.widgets.icon_widget import IconWidget from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical from openpilot.selfdrive.ui.ui_state import ui_state, ChestnutState from openpilot.common.version import RELEASE_BRANCHES @@ -69,8 +69,8 @@ class AlertsPill(Widget): count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h) gui_label(count_rect, str(alert_count), font_size=36, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) class NetworkIcon(Widget): diff --git a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py index c27e05f752..40e074d455 100644 --- a/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py +++ b/openpilot/selfdrive/ui/mici/layouts/offroad_alerts.py @@ -11,7 +11,7 @@ from openpilot.common.hardware import HARDWARE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import Scroller -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr REFRESH_INTERVAL = 5.0 # seconds @@ -62,12 +62,12 @@ class AlertItem(Widget): self._icon_green = gui_app.texture("icons_mici/offroad_alerts/green_wheel.png", self.ICON_SIZE, self.ICON_SIZE) self._title_label = UnifiedLabel(text="", font_size=32, font_weight=FontWeight.SEMI_BOLD, text_color=self.TEXT_COLOR, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, line_height=0.95) + alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.TOP, line_height=0.95) self._body_label = UnifiedLabel(text="", font_size=28, font_weight=FontWeight.ROMAN, text_color=self.TEXT_COLOR, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, line_height=0.95) + alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.BOTTOM, line_height=0.95) self._title_text = "" self._body_text = "" @@ -200,8 +200,8 @@ class MiciOffroadAlerts(Scroller): # Create empty state label self._empty_label = UnifiedLabel(tr("no alerts"), 65, FontWeight.DISPLAY, rl.WHITE, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) # Build initial alert list self._build_alerts() diff --git a/openpilot/selfdrive/ui/mici/layouts/onboarding.py b/openpilot/selfdrive/ui/mici/layouts/onboarding.py index 2b448ac73d..0590440c6b 100644 --- a/openpilot/selfdrive/ui/mici/layouts/onboarding.py +++ b/openpilot/selfdrive/ui/mici/layouts/onboarding.py @@ -4,7 +4,7 @@ import pyray as rl from collections.abc import Callable from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.qrcode import make_texture -from openpilot.system.ui.lib.application import FontWeight, gui_app +from openpilot.system.ui.lib.application import FontWeight, gui_app, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import SmallCircleIconButton from openpilot.system.ui.widgets.scroller import NavScroller, Scroller @@ -33,7 +33,7 @@ class CabinCameraSetupDialog(BaseCabinCameraDialog): if not self._camera_view.frame: gui_label(rect, tr("camera starting"), font_size=64, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + alignment=TextAlignment.CENTER) rl.end_scissor_mode() return diff --git a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py index d2896e1807..e42ee6c438 100644 --- a/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/mici/onroad/alert_renderer.py @@ -10,7 +10,7 @@ from opendbc.car.structs import car from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.filter_simple import BounceFilter, FirstOrderFilter from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel @@ -319,7 +319,7 @@ class AlertRenderer(Widget): self._alert_text1_label.set_text(alert_text1) self._alert_text1_label.set_text_color(color) self._alert_text1_label.set_font_size(font_size) - self._alert_text1_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + self._alert_text1_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) self._alert_text1_label.render(text_rect1) alert_text2 = alert.text2.lower() @@ -351,5 +351,5 @@ class AlertRenderer(Widget): self._alert_text2_label.set_text(alert_text2) self._alert_text2_label.set_text_color(color) self._alert_text2_label.set_font_size(small_font_size) - self._alert_text2_label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_LEFT if icon_side != 'left' else rl.GuiTextAlignment.TEXT_ALIGN_RIGHT) + self._alert_text2_label.set_alignment(TextAlignment.LEFT if icon_side != 'left' else TextAlignment.RIGHT) self._alert_text2_label.render(text_rect2) diff --git a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py index 4f4e4b2f6c..ddc04d7560 100644 --- a/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py +++ b/openpilot/selfdrive/ui/mici/onroad/augmented_road_view.py @@ -11,7 +11,7 @@ from openpilot.selfdrive.ui.mici.onroad.hud_renderer import HudRenderer from openpilot.selfdrive.ui.mici.onroad.model_renderer import ModelRenderer from openpilot.selfdrive.ui.mici.onroad.confidence_ball import ConfidenceBall from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView -from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent +from openpilot.system.ui.lib.application import FontWeight, gui_app, MousePos, MouseEvent, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets import Widget from openpilot.common.filter_simple import BounceFilter @@ -154,8 +154,8 @@ class AugmentedRoadView(CameraView): self._confidence_ball = ConfidenceBall() self._offroad_label = UnifiedLabel("start the car to\nuse openpilot", 54, FontWeight.DISPLAY, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png") diff --git a/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py index e86c0aa739..b0bdb9749c 100644 --- a/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py +++ b/openpilot/selfdrive/ui/mici/onroad/cabin_camera_dialog.py @@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.mici.onroad.cameraview import CameraView from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -76,7 +76,7 @@ class BaseCabinCameraDialog(Widget): if not self._camera_view.frame: gui_label(rect, tr("camera starting"), font_size=54, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + alignment=TextAlignment.CENTER) rl.end_scissor_mode() self._publish_alert_sound(None) return @@ -124,12 +124,12 @@ class BaseCabinCameraDialog(Widget): awareness_pct = dm_state.visionPolicyState.awarenessPercent if is_vision else dm_state.wheeltouchPolicyState.awarenessPercent gui_label(rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height), f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.TOP, color=rl.Color(0, 0, 0, 180)) gui_label(rect, f"Awareness: {awareness_pct:.0f}%", font_size=44, font_weight=FontWeight.MEDIUM, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.TOP, color=rl.Color(255, 255, 255, int(255 * 0.9))) if dm_state.alertLevel == log.DriverMonitoringState.AlertLevel.none: @@ -137,16 +137,16 @@ class BaseCabinCameraDialog(Widget): # Show alert level alert_level_str = f"{'Pay Attention' if is_vision else 'Touch Wheel'} - level {dm_state.alertLevel}" - alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT + alignment = TextAlignment.RIGHT if self.driver_state_renderer.is_rhd else TextAlignment.LEFT shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) gui_label(shadow_rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, alignment=alignment, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, + alignment_vertical=TextAlignmentVertical.BOTTOM, color=rl.Color(0, 0, 0, 180)) gui_label(rect, alert_level_str, font_size=40, font_weight=FontWeight.BOLD, alignment=alignment, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, + alignment_vertical=TextAlignmentVertical.BOTTOM, color=rl.Color(255, 255, 255, int(255 * 0.9))) def _load_eye_textures(self): diff --git a/openpilot/selfdrive/ui/mici/widgets/button.py b/openpilot/selfdrive/ui/mici/widgets/button.py index e2912d00cf..59a3f95191 100644 --- a/openpilot/selfdrive/ui/mici/widgets/button.py +++ b/openpilot/selfdrive/ui/mici/widgets/button.py @@ -6,7 +6,7 @@ from collections.abc import Callable from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.scroller import DO_ZOOM -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignmentVertical from openpilot.common.filter_simple import BounceFilter if TYPE_CHECKING: @@ -125,10 +125,10 @@ class BigButton(Widget): self._rotate_icon_t: float | None = None self._label = UnifiedLabel(text, font_size=self._get_label_font_size(), font_weight=FontWeight.BOLD, - text_color=LABEL_COLOR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM, scroll=scroll, + text_color=LABEL_COLOR, alignment_vertical=TextAlignmentVertical.BOTTOM, scroll=scroll, line_height=0.9) self._sub_label = UnifiedLabel(value, font_size=COMPLICATION_SIZE, font_weight=FontWeight.ROMAN, - text_color=COMPLICATION_GREY, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + text_color=COMPLICATION_GREY, alignment_vertical=TextAlignmentVertical.BOTTOM) self._update_label_layout() self._load_images() @@ -167,9 +167,9 @@ class BigButton(Widget): def _update_label_layout(self): self._label.set_font_size(self._get_label_font_size()) if self.value: - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._label.set_alignment_vertical(TextAlignmentVertical.TOP) else: - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + self._label.set_alignment_vertical(TextAlignmentVertical.BOTTOM) def set_text(self, text: str): self.text = text @@ -356,8 +356,8 @@ class GreyBigButton(BigButton): self._sub_label.set_font_size(36) self._sub_label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._sub_label.set_font_weight(FontWeight.DISPLAY_REGULAR) - self._sub_label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE if not self._label.text else - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + self._sub_label.set_alignment_vertical(TextAlignmentVertical.MIDDLE if not self._label.text else + TextAlignmentVertical.BOTTOM) self._sub_label.set_line_height(0.95) @property diff --git a/openpilot/selfdrive/ui/onroad/alert_renderer.py b/openpilot/selfdrive/ui/onroad/alert_renderer.py index 62511b87db..7add53df5d 100644 --- a/openpilot/selfdrive/ui/onroad/alert_renderer.py +++ b/openpilot/selfdrive/ui/onroad/alert_renderer.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from openpilot.cereal import messaging, log from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.common.hardware import COMMA_HARDWARE -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -76,10 +76,10 @@ class AlertRenderer(Widget): self.font_bold: rl.Font = gui_app.font(FontWeight.BOLD) # font size is set dynamically - self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) - self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP) + self._full_text1_label = Label("", font_size=0, font_weight=FontWeight.BOLD, text_alignment=TextAlignment.CENTER, + text_alignment_vertical=TextAlignmentVertical.TOP) + self._full_text2_label = Label("", font_size=ALERT_FONT_BIG, text_alignment=TextAlignment.CENTER, + text_alignment_vertical=TextAlignmentVertical.TOP) def get_alert(self, sm: messaging.SubMaster) -> Alert | None: """Generate the current alert based on selfdrive state.""" diff --git a/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py index 7bb1917c35..58a645381b 100644 --- a/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py +++ b/openpilot/selfdrive/ui/onroad/cabin_camera_dialog.py @@ -4,7 +4,7 @@ from openpilot.cereal.visionipc import VisionStreamType from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state, device -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets.label import gui_label @@ -38,7 +38,7 @@ class CabinCameraDialog(CameraView): tr("camera starting"), font_size=100, font_weight=FontWeight.BOLD, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + alignment=TextAlignment.CENTER, ) return -1 diff --git a/openpilot/system/ui/lib/application.py b/openpilot/system/ui/lib/application.py index 5e980937d7..230ddc635e 100644 --- a/openpilot/system/ui/lib/application.py +++ b/openpilot/system/ui/lib/application.py @@ -13,7 +13,7 @@ import subprocess from contextlib import contextmanager from collections.abc import Callable from collections import deque -from enum import StrEnum +from enum import IntEnum, StrEnum from pathlib import Path from typing import NamedTuple from importlib.resources import as_file, files @@ -115,6 +115,18 @@ class FontWeight(StrEnum): DISPLAY = "Inter-Bold.ttf" +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 + + +class TextAlignmentVertical(IntEnum): + TOP = 0 + MIDDLE = 1 + BOTTOM = 2 + + def font_fallback(font: rl.Font) -> rl.Font: """Use a Noto fallback for languages not covered by Inter.""" if multilang.requires_font_fallback(): @@ -330,7 +342,6 @@ class GuiApplication: rl.set_target_fps(0 if OFFSCREEN or vblank_control else fps) self._target_fps = fps - self._set_styles() self._load_fonts() self._patch_text_functions() self._patch_scissor_mode() @@ -731,14 +742,6 @@ class GuiApplication: self._fonts[font_weight_file] = font if multilang.requires_font_fallback(): self.fallback_font() - rl.gui_set_font(self._fonts[FontWeight.NORMAL]) - - def _set_styles(self): - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BORDER_WIDTH, 0) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, DEFAULT_TEXT_SIZE) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.BACKGROUND_COLOR, rl.color_to_int(rl.BLACK)) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(DEFAULT_TEXT_COLOR)) - rl.gui_set_style(rl.GuiControl.DEFAULT, rl.GuiControlProperty.BASE_COLOR_NORMAL, rl.color_to_int(rl.Color(50, 50, 50, 255))) def _patch_text_functions(self): # Wrap pyray text APIs to apply a global text size scale so our px sizes match Qt diff --git a/openpilot/system/ui/lib/utils.py b/openpilot/system/ui/lib/utils.py deleted file mode 100644 index e97b3ba9d9..0000000000 --- a/openpilot/system/ui/lib/utils.py +++ /dev/null @@ -1,19 +0,0 @@ -import pyray as rl -from collections.abc import Sequence - - -class GuiStyleContext: - def __init__(self, styles: Sequence[tuple[int, int, int]]): - """styles is a list of tuples (control, prop, new_value)""" - self.styles = styles - self.prev_styles: list[tuple[int, int, int]] = [] - - def __enter__(self): - for control, prop, new_value in self.styles: - prev_value = rl.gui_get_style(control, prop) - self.prev_styles.append((control, prop, prev_value)) - rl.gui_set_style(control, prop, new_value) - - def __exit__(self, exc_type, exc_value, traceback): - for control, prop, prev_value in self.prev_styles: - rl.gui_set_style(control, prop, prev_value) diff --git a/openpilot/system/ui/mici_setup.py b/openpilot/system/ui/mici_setup.py index e4977ffdf4..45d63f5d2c 100755 --- a/openpilot/system/ui/mici_setup.py +++ b/openpilot/system/ui/mici_setup.py @@ -18,7 +18,7 @@ from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.swaglog import cloudlog from openpilot.common.time_helpers import system_time_valid from openpilot.common.utils import run_cmd -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.wifi_manager import WifiManager, ConnectStatus from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.nav_widget import NavWidget @@ -105,8 +105,8 @@ class StartPage(Widget): super().__init__() self._title = UnifiedLabel("start", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), - font_weight=FontWeight.DISPLAY, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.DISPLAY, alignment=TextAlignment.CENTER, + alignment_vertical=TextAlignmentVertical.MIDDLE) self._start_bg_txt = gui_app.texture("icons_mici/setup/start_button.png", 500, 224, keep_aspect_ratio=False) self._start_bg_pressed_txt = gui_app.texture("icons_mici/setup/start_button_pressed.png", 500, 224, keep_aspect_ratio=False) @@ -197,7 +197,7 @@ class DownloadingPage(NavWidget): self._title_label = UnifiedLabel("downloading...", 64, text_color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY) self._progress_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), - font_weight=FontWeight.ROMAN, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + font_weight=FontWeight.ROMAN, alignment_vertical=TextAlignmentVertical.BOTTOM) self._progress = 0 def _back_enabled(self) -> bool: @@ -261,8 +261,8 @@ class BigPillButton(BigButton): super().__init__(*args, **kwargs) self._label.set_font_size(48) - self._label.set_alignment(rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - self._label.set_alignment_vertical(rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + self._label.set_alignment(TextAlignment.CENTER) + self._label.set_alignment_vertical(TextAlignmentVertical.MIDDLE) def _load_images(self): if self._green: diff --git a/openpilot/system/ui/mici_updater.py b/openpilot/system/ui/mici_updater.py index d9009b8259..cc16145793 100755 --- a/openpilot/system/ui/mici_updater.py +++ b/openpilot/system/ui/mici_updater.py @@ -7,7 +7,7 @@ import pyray as rl from openpilot.common.realtime import config_realtime_process, set_core_affinity from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE from openpilot.common.swaglog import cloudlog -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignmentVertical from openpilot.system.ui.widgets.nav_widget import NavWidget from openpilot.system.ui.widgets.scroller import Scroller from openpilot.system.ui.widgets.label import UnifiedLabel @@ -30,7 +30,7 @@ class ProgressPage(NavWidget): font_weight=FontWeight.DISPLAY, line_height=0.8) self._progress_percent_label = UnifiedLabel("", 132, text_color=rl.Color(255, 255, 255, int(255 * 0.9 * 0.65)), font_weight=FontWeight.ROMAN, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM) + alignment_vertical=TextAlignmentVertical.BOTTOM) def _back_enabled(self) -> bool: return False diff --git a/openpilot/system/ui/tici_reset.py b/openpilot/system/ui/tici_reset.py index 5c175dbfa6..ffe90bbb87 100755 --- a/openpilot/system/ui/tici_reset.py +++ b/openpilot/system/ui/tici_reset.py @@ -11,7 +11,7 @@ from openpilot.common.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle -from openpilot.system.ui.widgets.label import gui_label, gui_text_box +from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label USERDATA = "/dev/disk/by-partlabel/userdata" TIMEOUT = 3*60 @@ -38,6 +38,7 @@ class Reset(Widget): self._cancel_button = Button("Cancel", gui_app.request_close) self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) self._reboot_button = Button("Reboot", self._reboot) + self._body_label = UnifiedLabel(self._get_body_text, 90, line_height=1 / 0.9) @staticmethod def _reboot() -> None: @@ -75,7 +76,7 @@ class Reset(Widget): gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) text_rect = rl.Rectangle(content_rect.x + 140, content_rect.y + 140, content_rect.width - 280, content_rect.height - 90 - 100 * FONT_SCALE) - gui_text_box(text_rect, self._get_body_text(), 90) + self._body_label.render(text_rect) button_height = 160 button_spacing = 50 diff --git a/openpilot/system/ui/tici_setup.py b/openpilot/system/ui/tici_setup.py index 36f47ccf1b..17c3cf5e68 100755 --- a/openpilot/system/ui/tici_setup.py +++ b/openpilot/system/ui/tici_setup.py @@ -13,7 +13,7 @@ import pyray as rl from openpilot.cereal import log from openpilot.common.hardware import HARDWARE from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE +from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard @@ -67,17 +67,17 @@ class Setup(Widget): self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) - self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255), text_padding=20) self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + text_alignment=TextAlignment.LEFT, text_padding=20) self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) - self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", - BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20) self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) @@ -85,32 +85,32 @@ class Setup(Widget): button_style=ButtonStyle.PRIMARY) self._software_selection_continue_button.set_enabled(False) self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) - self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) - self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) + self._download_failed_url_label = Label("", 52, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT, text_padding=20) self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._network_setup_continue_button.set_enabled(False) - self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback, button_style=ButtonStyle.PRIMARY) self._custom_software_warning_continue_button.set_enabled(False) self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) - self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 81, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255), text_padding=60) self._yellow_warning_icon = gui_app.texture("icons/yellow_warning.png", int(68 * FONT_SCALE), int(68 * FONT_SCALE)) self._custom_software_warning_body_labels = [ - Label(text, 68, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - text_alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + Label(text, 68, text_alignment=TextAlignment.LEFT, + text_alignment_vertical=TextAlignmentVertical.TOP, text_padding=60, icon=self._yellow_warning_icon if has_icon else None) for text, has_icon in [ ("Use caution when installing third-party software.", False), diff --git a/openpilot/system/ui/tici_updater.py b/openpilot/system/ui/tici_updater.py index 27cf6579a8..2cb86cca88 100755 --- a/openpilot/system/ui/tici_updater.py +++ b/openpilot/system/ui/tici_updater.py @@ -10,7 +10,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight, FONT_SCALE from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle -from openpilot.system.ui.widgets.label import gui_text_box, gui_label +from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label from openpilot.system.ui.widgets.network import WifiManagerUI # Constants @@ -50,6 +50,8 @@ class Updater(Widget): self._install_button = Button("Install", click_callback=self.install_update, button_style=ButtonStyle.PRIMARY) self._back_button = Button("Back", click_callback=lambda: self.set_current_screen(Screen.PROMPT)) self._reboot_button = Button("Reboot", click_callback=lambda: HARDWARE.reboot()) + self._desc_label = UnifiedLabel("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + + "The download size is approximately 1GB.", BODY_FONT_SIZE, line_height=1 / 0.9) def set_current_screen(self, screen: Screen): self.current_screen = screen @@ -99,11 +101,8 @@ class Updater(Widget): gui_label(title_rect, "Update Required", TITLE_FONT_SIZE, font_weight=FontWeight.BOLD) # Description - desc_text = ("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. " + - "The download size is approximately 1GB.") - desc_rect = rl.Rectangle(MARGIN + 50, 250 + TITLE_FONT_SIZE * FONT_SCALE + 75, rect.width - MARGIN * 2 - 100, BODY_FONT_SIZE * FONT_SCALE * 4) - gui_text_box(desc_rect, desc_text, BODY_FONT_SIZE) + self._desc_label.render(desc_rect) # Buttons at the bottom button_y = rect.height - MARGIN - BUTTON_HEIGHT diff --git a/openpilot/system/ui/widgets/button.py b/openpilot/system/ui/widgets/button.py index 36ef3bedab..c031b2f141 100644 --- a/openpilot/system/ui/widgets/button.py +++ b/openpilot/system/ui/widgets/button.py @@ -3,7 +3,7 @@ from enum import IntEnum import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import Label from openpilot.common.filter_simple import FirstOrderFilter @@ -86,7 +86,7 @@ class Button(Widget): font_weight: FontWeight = FontWeight.MEDIUM, button_style: ButtonStyle = ButtonStyle.NORMAL, border_radius: int = 10, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, + text_alignment: int = TextAlignment.CENTER, text_padding: int = 20, icon=None, elide_right: bool = False, @@ -139,7 +139,7 @@ class ButtonRadio(Button): icon, click_callback: Callable[[], None] | None = None, font_size: int = DEFAULT_BUTTON_FONT_SIZE, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + text_alignment: int = TextAlignment.LEFT, border_radius: int = 10, text_padding: int = 20, ): diff --git a/openpilot/system/ui/widgets/keyboard.py b/openpilot/system/ui/widgets/keyboard.py index 49c59a431f..c9d2187cbb 100644 --- a/openpilot/system/ui/widgets/keyboard.py +++ b/openpilot/system/ui/widgets/keyboard.py @@ -5,7 +5,7 @@ from collections.abc import Callable import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import DialogResult, Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button @@ -65,8 +65,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = Label("", 90, FontWeight.BOLD, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) - self._sub_title = Label("", 55, FontWeight.NORMAL, rl.GuiTextAlignment.TEXT_ALIGN_LEFT, text_padding=20) + self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT, text_padding=20) + self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT, text_padding=20) self._max_text_size = max_text_size self._min_text_size = min_text_size diff --git a/openpilot/system/ui/widgets/label.py b/openpilot/system/ui/widgets/label.py index a3e827321c..60fa3e25e0 100644 --- a/openpilot/system/ui/widgets/label.py +++ b/openpilot/system/ui/widgets/label.py @@ -4,10 +4,9 @@ from collections.abc import Callable from typing import Union import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR, FONT_SCALE, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.utils import GuiStyleContext from openpilot.system.ui.lib.wrap_text import wrap_text ICON_PADDING = 15 @@ -32,8 +31,8 @@ def gui_label( font_size: int = DEFAULT_TEXT_SIZE, color: rl.Color = DEFAULT_TEXT_COLOR, font_weight: FontWeight = FontWeight.NORMAL, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + alignment: int = TextAlignment.LEFT, + alignment_vertical: int = TextAlignmentVertical.MIDDLE, elide_right: bool = True ): font = gui_app.font(font_weight) @@ -57,16 +56,16 @@ def gui_label( # Calculate horizontal position based on alignment text_x = rect.x + { - rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, - rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, - rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, + TextAlignment.LEFT: 0, + TextAlignment.CENTER: (rect.width - text_size.x) / 2, + TextAlignment.RIGHT: rect.width - text_size.x, }.get(alignment, 0) # Calculate vertical position based on alignment text_y = rect.y + { - rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, + TextAlignmentVertical.TOP: 0, + TextAlignmentVertical.MIDDLE: (rect.height - text_size.y) / 2, + TextAlignmentVertical.BOTTOM: rect.height - text_size.y, }.get(alignment_vertical, 0) # Draw the text in the specified rectangle @@ -74,42 +73,14 @@ def gui_label( rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) -def gui_text_box( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, - font_weight: FontWeight = FontWeight.NORMAL, - line_scale: float = 1.0, -): - styles = [ - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, round(font_size * FONT_SCALE)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, round(font_size * FONT_SCALE * line_scale)), - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) - ] - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(font_weight)) - - with GuiStyleContext(styles): - rl.gui_label(rect, text) - - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) - - # Non-interactive text area. Can render an optional specified icon. class Label(Widget): def __init__(self, text: str | Callable[[], str], font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, - text_alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_CENTER, - text_alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + text_alignment: int = TextAlignment.CENTER, + text_alignment_vertical: int = TextAlignmentVertical.MIDDLE, text_padding: int = 0, text_color: rl.Color = DEFAULT_TEXT_COLOR, icon: Union[rl.Texture, None] = None, @@ -181,10 +152,10 @@ class Label(Widget): self._update_text(self._text) text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) - if self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: + if self._text_alignment_vertical == TextAlignmentVertical.MIDDLE: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, (self._rect.y + (self._rect.height - total_text_height) // 2)) - elif self._text_alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + elif self._text_alignment_vertical == TextAlignmentVertical.BOTTOM: total_text_height = sum(ts.y for ts in self._text_size) or self._font_size * FONT_SCALE text_pos = rl.Vector2(self._rect.x, self._rect.y + self._rect.height - total_text_height) else: @@ -193,10 +164,10 @@ class Label(Widget): if self._icon: icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 if len(self._text_wrapped) > 0: - if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._text_alignment == TextAlignment.LEFT: icon_x = self._rect.x + self._text_padding text_pos.x = self._rect.x + self._icon.width + ICON_PADDING - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._text_alignment == TextAlignment.CENTER: total_width = self._icon.width + ICON_PADDING + text_size.x icon_x = self._rect.x + (self._rect.width - total_width) / 2 text_pos.x = self._rect.x + self._icon.width + ICON_PADDING @@ -208,11 +179,11 @@ class Label(Widget): for text, text_size in zip(self._text_wrapped, self._text_size, strict=True): line_pos = rl.Vector2(text_pos.x, text_pos.y) - if self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._text_alignment == TextAlignment.LEFT: line_pos.x += self._text_padding - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._text_alignment == TextAlignment.CENTER: line_pos.x += (self._rect.width - text_size.x) // 2 - elif self._text_alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + elif self._text_alignment == TextAlignment.RIGHT: line_pos.x += self._rect.width - text_size.x - self._text_padding rl.draw_text_ex(self._font, text, line_pos, self._font_size, 0, self._text_color) @@ -221,7 +192,7 @@ class Label(Widget): class UnifiedLabel(Widget): """ - Unified label widget that combines functionality from gui_label, gui_text_box, and Label. + Unified label widget that combines functionality from gui_label and Label. Supports: - Text wrapping @@ -241,8 +212,8 @@ class UnifiedLabel(Widget): font_size: int = DEFAULT_TEXT_SIZE, font_weight: FontWeight = FontWeight.NORMAL, text_color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + alignment: int = TextAlignment.LEFT, + alignment_vertical: int = TextAlignmentVertical.TOP, text_padding: int = 0, max_width: int | None = None, elide: bool = True, @@ -561,9 +532,9 @@ class UnifiedLabel(Widget): total_visible_height += size.y * self._line_height # Calculate vertical alignment offset - if self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: + if self._alignment_vertical == TextAlignmentVertical.TOP: start_y = self._rect.y - elif self._alignment_vertical == rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: + elif self._alignment_vertical == TextAlignmentVertical.BOTTOM: start_y = self._rect.y + self._rect.height - total_visible_height else: # TEXT_ALIGN_MIDDLE start_y = self._rect.y + (self._rect.height - total_visible_height) / 2 @@ -640,11 +611,11 @@ class UnifiedLabel(Widget): def _render_line(self, line, size, current_y, x_offset=0.0): # Calculate horizontal position - if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_LEFT: + if self._alignment == TextAlignment.LEFT: line_x = self._rect.x + self._text_padding - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._alignment == TextAlignment.CENTER: line_x = self._rect.x + (self._rect.width - size.x) / 2 - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + elif self._alignment == TextAlignment.RIGHT: line_x = self._rect.x + self._rect.width - size.x - self._text_padding else: line_x = self._rect.x + self._text_padding @@ -662,9 +633,9 @@ class UnifiedLabel(Widget): def _render_line_shimmer(self, line, line_x, current_y): # Shimmer range based on widest line so sweep is even across all lines max_width = self.text_width - if self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: + if self._alignment == TextAlignment.RIGHT: shimmer_left = self._rect.x + self._rect.width - self._text_padding - max_width - elif self._alignment == rl.GuiTextAlignment.TEXT_ALIGN_CENTER: + elif self._alignment == TextAlignment.CENTER: shimmer_left = self._rect.x + (self._rect.width - max_width) / 2 else: shimmer_left = self._rect.x + self._text_padding diff --git a/openpilot/system/ui/widgets/list_view.py b/openpilot/system/ui/widgets/list_view.py index 705cb476de..d88473a491 100644 --- a/openpilot/system/ui/widgets/list_view.py +++ b/openpilot/system/ui/widgets/list_view.py @@ -3,7 +3,7 @@ import os import pyray as rl from collections.abc import Callable from abc import ABC -from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos, TextAlignment, TextAlignmentVertical from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.widgets import Widget @@ -139,8 +139,8 @@ class ButtonAction(ItemAction): if value_text: value_rect = rl.Rectangle(rect.x, rect.y, rect.width - BUTTON_WIDTH - TEXT_PADDING, rect.height) gui_label(value_rect, value_text, font_size=ITEM_TEXT_FONT_SIZE, color=ITEM_TEXT_VALUE_COLOR, - font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.NORMAL, alignment=TextAlignment.LEFT, + alignment_vertical=TextAlignmentVertical.MIDDLE) # TODO: just use the generic Widget click callbacks everywhere, no returning from render pressed = self._pressed @@ -168,8 +168,8 @@ class TextAction(ItemAction): def _render(self, rect: rl.Rectangle) -> bool: gui_label(self._rect, self.text, font_size=ITEM_TEXT_FONT_SIZE, color=self.color, - font_weight=FontWeight.NORMAL, alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE) + font_weight=FontWeight.NORMAL, alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.MIDDLE) return False def set_text(self, text: str | Callable[[], str]): diff --git a/openpilot/system/ui/widgets/network.py b/openpilot/system/ui/widgets/network.py index 4068d552b9..3aa6701943 100644 --- a/openpilot/system/ui/widgets/network.py +++ b/openpilot/system/ui/widgets/network.py @@ -3,7 +3,7 @@ from functools import partial from typing import cast import pyray as rl -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network, MeteredType, normalize_ssid @@ -51,7 +51,7 @@ class NavButton(Widget): def _render(self, _): color = rl.Color(74, 74, 74, 255) if self.is_pressed else rl.Color(57, 57, 57, 255) rl.draw_rectangle_rounded(self._rect, 0.6, 10, color) - gui_label(self.rect, self.text, font_size=60, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(self.rect, self.text, font_size=60, alignment=TextAlignment.CENTER) class NetworkUI(Widget): @@ -299,7 +299,7 @@ class WifiManagerUI(Widget): def _render(self, rect: rl.Rectangle): if not self._networks: - gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(rect, tr("Scanning Wi-Fi networks..."), 72, alignment=TextAlignment.CENTER) return if self.state == UIState.NEEDS_AUTH and self._state_network: @@ -373,7 +373,7 @@ class WifiManagerUI(Widget): if status_text: status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) - gui_label(status_text_rect, status_text, font_size=48, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + gui_label(status_text_rect, status_text, font_size=48, alignment=TextAlignment.CENTER) else: # If the network is saved, show the "Forget" button if self._wifi_manager.is_connection_saved(network.ssid): @@ -439,7 +439,7 @@ class WifiManagerUI(Widget): self._networks = networks for n in self._networks: self._networks_buttons[n.ssid] = Button(normalize_ssid(n.ssid), partial(self._networks_buttons_callback, n), font_size=55, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) + text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.TRANSPARENT_WHITE_TEXT) self._networks_buttons[n.ssid].set_touch_valid_callback(lambda: self.scroll_panel.is_touch_valid()) self._forget_networks_buttons[n.ssid] = Button(tr("Forget"), partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, font_size=45) diff --git a/openpilot/system/ui/widgets/option_dialog.py b/openpilot/system/ui/widgets/option_dialog.py index 206400a74f..816503c4a0 100644 --- a/openpilot/system/ui/widgets/option_dialog.py +++ b/openpilot/system/ui/widgets/option_dialog.py @@ -1,6 +1,6 @@ import pyray as rl from collections.abc import Callable -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment from openpilot.system.ui.lib.multilang import tr from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import Button, ButtonStyle @@ -29,7 +29,7 @@ class MultiOptionDialog(Widget): # Create scroller with option buttons self.option_buttons = [Button(option, click_callback=lambda opt=option: self._on_option_clicked(opt), font_weight=option_font_weight, - text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT, button_style=ButtonStyle.NORMAL, + text_alignment=TextAlignment.LEFT, button_style=ButtonStyle.NORMAL, text_padding=50, elide_right=True) for option in options] self.scroller = Scroller(self.option_buttons, spacing=LIST_ITEM_SPACING) diff --git a/openpilot/system/ui/widgets/slider.py b/openpilot/system/ui/widgets/slider.py index bf965954f2..7bdf056219 100644 --- a/openpilot/system/ui/widgets/slider.py +++ b/openpilot/system/ui/widgets/slider.py @@ -3,7 +3,7 @@ from collections.abc import Callable import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.application import gui_app, FontWeight, TextAlignment, TextAlignmentVertical from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.common.filter_simple import FirstOrderFilter, BounceFilter @@ -41,8 +41,8 @@ class SliderBase(Widget, abc.ABC): self._is_dragging_circle = False self._label = self._child(UnifiedLabel(title, font_size=36, font_weight=FontWeight.SEMI_BOLD, text_color=rl.WHITE, - alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT, - alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, line_height=0.9, shimmer=True)) + alignment=TextAlignment.RIGHT, + alignment_vertical=TextAlignmentVertical.MIDDLE, line_height=0.9, shimmer=True)) @abc.abstractmethod def _load_assets(self): From 4cdc16031ffd8bd44cb1d3f2fd2bfd49effd66ce Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Thu, 27 Aug 2026 11:21:56 -0700 Subject: [PATCH 08/53] log chestnut supply fault (#38711) * log chestnut INA supply fault * ci --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/modeld/modeld.py | 45 ++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 5 deletions(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fdf022fa63..fb4867b2a6 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -725,6 +725,7 @@ struct ChestnutState { pcieLtssm @7 :UInt8; supplyVoltage @8 :UInt16; # mV supplyCurrent @9 :Int16; # mA + supplyFault @10 :Bool; } struct RadarState @0x9a185389d6fdd05f { diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 0db66ed880..a795f366ce 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,6 +6,7 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device +import usb1 import struct import threading import time @@ -31,6 +32,7 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob @@ -79,6 +81,37 @@ class ChestnutState: self.valid = True self.sends = 0 self.metrics = {} + self._asm_usb = None + + def _close_asm_usb(self) -> None: + if self._asm_usb is not None: + self._asm_usb.close() + self._asm_usb = None + + def _open_asm_usb(self): + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: + return handle + context.close() + + def _read_ina(self) -> tuple[int, int, bool]: + if "AMD" in Device._opened_devices and self._asm_usb is None: + try: + raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) + return struct.unpack(' int: @@ -114,13 +147,15 @@ class ChestnutState: setattr(state, k, v) asm_valid = False + try: + # ASM runs on USB-C power, these still read without a gpu + state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() + asm_valid = True + except Exception: + pass if "AMD" in Device._opened_devices: try: - # ASM runs on USB-C power, these still read without a gpu - asm = Device["AMD"].iface.pci_dev.usb - state.pcieLtssm = asm.read(0xB450, 1)[0] - state.supplyVoltage, state.supplyCurrent = struct.unpack(' Date: Thu, 27 Aug 2026 11:38:53 -0700 Subject: [PATCH 09/53] bump raylib (#38712) --- uv.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/uv.lock b/uv.lock index 20fe78c1fb..89b82b5452 100644 --- a/uv.lock +++ b/uv.lock @@ -209,15 +209,15 @@ wheels = [ [[package]] name = "comma-deps-raylib" -version = "6.0.0.1.post98" +version = "6.0.0.1.post101" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/21/59509b2758e3612c63336df5534ea3b36e472e06c18a50f0b96c32e8e3e0/comma_deps_raylib-6.0.0.1.post98-py3-none-macosx_11_0_arm64.whl", hash = "sha256:838a6115f508ee4ca0e4c84975c0892d67c10f94b11dd2bc0e023ad65138d010", size = 2004199, upload-time = "2026-07-23T17:03:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f5/84e135ac611a6dfe8b95ff989097c200770907907afba78455d4c863981e/comma_deps_raylib-6.0.0.1.post98-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:9347fe18799c209dd1af746a44a94433bae69d4644de10e11dcc307aecd87fb1", size = 7203023, upload-time = "2026-07-23T17:03:21.525Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1b/ae0959dac622f7169de230ec98c6555870ae0ad4f61e7b8d32922fa960ed/comma_deps_raylib-6.0.0.1.post98-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:f9fd393fbaf1f16d7785be0b2252ef3b14a953c93d89296ed9a67bc7063de882", size = 5055448, upload-time = "2026-07-23T17:03:25.737Z" }, + { url = "https://files.pythonhosted.org/packages/ff/90/e289acd1725d71c792c33399422f1052c4b0c38aeb4222a866d30c4a2cad/comma_deps_raylib-6.0.0.1.post101-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa69d5093a92d7d2bfd2714a1afccca63b94d4c55fae88e61b80e8841de6a6cd", size = 1885392, upload-time = "2026-08-27T18:24:37.25Z" }, + { url = "https://files.pythonhosted.org/packages/a1/17/12977631f6d86d1daa4f67a310cfdf2783ba288f42d444d6a89b2297a0a4/comma_deps_raylib-6.0.0.1.post101-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:c93ff9b45df414620b3011280da77151764c027e2f95d384fe255615d78872cc", size = 21707644, upload-time = "2026-08-27T18:24:41.282Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ca/ef33aff790b37dfc925f94fbb3a86da19f67929c15b3725ddb18afb91e96/comma_deps_raylib-6.0.0.1.post101-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:8f2a1ffe5f60cac06170b144d6ce84925c91657e7ada3ae35bc5df6ccbe0b461", size = 20722616, upload-time = "2026-08-27T18:24:45.803Z" }, ] [[package]] From cbf750de20d29cd96c545da028a45c1135475c00 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:54:06 -0700 Subject: [PATCH 10/53] cabana: replace custom non-view Qt signals w/ plain observer (#38713) --- openpilot/tools/cabana/SConscript | 2 +- openpilot/tools/cabana/binaryview.cc | 5 +- openpilot/tools/cabana/binaryview.h | 1 + openpilot/tools/cabana/cabana.cc | 10 +-- openpilot/tools/cabana/cameraview.cc | 22 ++--- openpilot/tools/cabana/cameraview.h | 19 ++--- openpilot/tools/cabana/chart/chart.cc | 9 +- openpilot/tools/cabana/chart/chart.h | 1 + openpilot/tools/cabana/chart/chartswidget.cc | 17 ++-- openpilot/tools/cabana/chart/chartswidget.h | 1 + .../tools/cabana/chart/signalselector.cc | 1 - openpilot/tools/cabana/commands.cc | 22 ++--- openpilot/tools/cabana/commands.h | 28 +------ openpilot/tools/cabana/core/observable.h | 83 +++++++++++++++++++ openpilot/tools/cabana/dbc/dbcmanager.cc | 28 +++---- openpilot/tools/cabana/dbc/dbcmanager.h | 22 ++--- openpilot/tools/cabana/dbc/dbcqt.cc | 18 ---- openpilot/tools/cabana/dbc/dbcqt.h | 27 ------ openpilot/tools/cabana/detailwidget.cc | 11 ++- openpilot/tools/cabana/detailwidget.h | 1 + openpilot/tools/cabana/historylog.cc | 10 ++- openpilot/tools/cabana/historylog.h | 3 +- openpilot/tools/cabana/mainwin.cc | 69 ++++++++------- openpilot/tools/cabana/mainwin.h | 11 ++- openpilot/tools/cabana/messageswidget.cc | 12 +-- openpilot/tools/cabana/messageswidget.h | 3 +- openpilot/tools/cabana/settings.cc | 2 +- openpilot/tools/cabana/settings.h | 8 +- openpilot/tools/cabana/signalview.cc | 19 ++--- openpilot/tools/cabana/signalview.h | 2 + .../tools/cabana/streams/abstractstream.cc | 61 +++++++++----- .../tools/cabana/streams/abstractstream.h | 40 ++++----- .../tools/cabana/streams/devicestream.cc | 16 ++-- openpilot/tools/cabana/streams/devicestream.h | 3 +- openpilot/tools/cabana/streams/livestream.cc | 12 +-- openpilot/tools/cabana/streams/livestream.h | 4 +- openpilot/tools/cabana/streams/pandastream.cc | 4 +- openpilot/tools/cabana/streams/pandastream.h | 3 +- .../tools/cabana/streams/replaystream.cc | 40 +++++---- openpilot/tools/cabana/streams/replaystream.h | 12 ++- .../tools/cabana/streams/socketcanstream.cc | 4 +- .../tools/cabana/streams/socketcanstream.h | 3 +- openpilot/tools/cabana/tests/test_cabana.cc | 9 +- openpilot/tools/cabana/tools/routeinfo.cc | 2 +- openpilot/tools/cabana/utils/util.cc | 26 ++++++ openpilot/tools/cabana/utils/util.h | 13 ++- openpilot/tools/cabana/videowidget.cc | 22 ++--- openpilot/tools/cabana/videowidget.h | 1 + 48 files changed, 403 insertions(+), 339 deletions(-) create mode 100644 openpilot/tools/cabana/core/observable.h delete mode 100644 openpilot/tools/cabana/dbc/dbcqt.cc delete mode 100644 openpilot/tools/cabana/dbc/dbcqt.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 387129709e..bc366ab6fb 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'dbc/dbcqt.cc', + 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 5e919dc6a3..2f0167107c 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -1,5 +1,4 @@ #include "tools/cabana/binaryview.h" -#include "tools/cabana/dbc/dbcqt.h" #include @@ -36,8 +35,8 @@ BinaryView::BinaryView(QWidget *parent) : QTableView(parent) { setMouseTracking(true); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &BinaryView::refresh); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &BinaryView::refresh); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); addShortcuts(); setWhatsThis(R"( diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index c49067a1a2..0b0159b422 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -100,5 +100,6 @@ private: bool is_message_active = false; const cabana::Signal *resize_sig = nullptr; const cabana::Signal *hovered_sig = nullptr; + Connections connections_; friend class BinaryItemDelegate; }; diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 8b21143faf..e3a1850346 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -146,19 +146,19 @@ int main(int argc, char *argv[]) { AbstractStream *stream = nullptr; if (args.msgq) { - stream = new DeviceStream(&app); + stream = new DeviceStream(); } else if (!args.zmq.empty()) { - stream = new DeviceStream(&app, QString::fromStdString(args.zmq)); + stream = new DeviceStream(QString::fromStdString(args.zmq)); } else if (args.panda || !args.panda_serial.empty()) { try { - stream = new PandaStream(&app, {.serial = args.panda_serial}); + stream = new PandaStream({.serial = args.panda_serial}); } catch (std::exception &e) { fprintf(stderr, "%s\n", e.what()); return 0; } #ifdef __linux__ } else if (SocketCanStream::available() && !args.socketcan.empty()) { - stream = new SocketCanStream(&app, {.device = args.socketcan}); + stream = new SocketCanStream({.device = args.socketcan}); #endif } else { uint32_t replay_flags = REPLAY_FLAG_NONE; @@ -174,7 +174,7 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } if (!route.isEmpty()) { - auto replay_stream = std::make_unique(&app); + auto replay_stream = std::make_unique(); if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { return 0; } diff --git a/openpilot/tools/cabana/cameraview.cc b/openpilot/tools/cabana/cameraview.cc index 9bd6b9be19..b8c70ab1bf 100644 --- a/openpilot/tools/cabana/cameraview.cc +++ b/openpilot/tools/cabana/cameraview.cc @@ -9,13 +9,11 @@ #include #include "common/yuv.h" +#include "tools/cabana/utils/util.h" CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QWidget(parent) { setAttribute(Qt::WA_OpaquePaintEvent); - qRegisterMetaType>("availableStreams"); - QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); - QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); } @@ -38,10 +36,6 @@ void CameraWidget::stopVipcThread() { } } -void CameraWidget::availableStreamsUpdated(std::set streams) { - available_streams = streams; -} - void CameraWidget::paintEvent(QPaintEvent *event) { QPainter p(this); p.fillRect(rect(), bg); @@ -67,10 +61,6 @@ void CameraWidget::paintEvent(QPaintEvent *event) { p.drawImage(video_rect, rgb_frame); } -void CameraWidget::vipcFrameReceived() { - update(); -} - void CameraWidget::vipcThread() { VisionStreamType cur_stream = requested_stream_type; std::unique_ptr vipc_client; @@ -93,7 +83,11 @@ void CameraWidget::vipcThread() { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } - emit vipcAvailableStreamsUpdated(streams); + utils::runOnMainThread([this, alive = std::weak_ptr(alive_), streams]() { + if (alive.expired()) return; + available_streams = streams; + availableStreamsUpdated(streams); + }); if (!vipc_client->connect(false)) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); @@ -112,7 +106,9 @@ void CameraWidget::vipcThread() { std::lock_guard lk(frame_lock); rgb_frame.swap(rgb_back); } - emit vipcThreadFrameReceived(); + utils::runOnMainThread([this, alive = std::weak_ptr(alive_)]() { + if (!alive.expired()) update(); + }); } } } diff --git a/openpilot/tools/cabana/cameraview.h b/openpilot/tools/cabana/cameraview.h index 3b55dd9ed9..ac292a701c 100644 --- a/openpilot/tools/cabana/cameraview.h +++ b/openpilot/tools/cabana/cameraview.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,11 +12,10 @@ #include #include "openpilot/cereal/visionstream.h" +#include "tools/cabana/core/observable.h" #include "msgq/visionipc/visionipc_client.h" class CameraWidget : public QWidget { - Q_OBJECT - public: explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); ~CameraWidget(); @@ -23,16 +23,14 @@ public: VisionStreamType getStreamType() { return active_stream_type; } void stopVipcThread(); -signals: - void clicked(); - void vipcThreadFrameReceived(); - void vipcAvailableStreamsUpdated(std::set); + Observable<> clicked; + Observable> availableStreamsUpdated; // invoked on the main thread protected: void paintEvent(QPaintEvent *event) override; void showEvent(QShowEvent *event) override; void hideEvent(QHideEvent *event) override { stopVipcThread(); } - void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + void mouseReleaseEvent(QMouseEvent *event) override { clicked(); } void vipcThread(); void clearFrames(); @@ -47,10 +45,5 @@ protected: std::thread vipc_thread; std::atomic vipc_exit = false; std::mutex frame_lock; - -protected slots: - void vipcFrameReceived(); - void availableStreamsUpdated(std::set streams); + std::shared_ptr alive_ = std::make_shared(true); }; - -Q_DECLARE_METATYPE(std::set); diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index 8496c26994..beece12352 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/chart.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -37,10 +36,10 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par createToolButtons(); signal_value_font.setPointSize(9); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &ChartView::signalRemoved); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &ChartView::signalUpdated); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &ChartView::msgRemoved); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &ChartView::msgUpdated); + connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { signalRemoved(sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { signalUpdated(sig); })); + connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { msgRemoved(id); })); + connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { msgUpdated(id); })); } void ChartView::createToolButtons() { diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index b03623475a..d7227b6cf8 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -126,5 +126,6 @@ private: double tooltip_x = -1; QFont signal_value_font; ChartsWidget *charts_widget; + Connections connections_; friend class ChartsWidget; }; diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 3144c5132d..8aa095b65f 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/chartswidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -76,10 +75,10 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { redo_zoom_action = toolbar->addAction(utils::icon("arrow-clockwise"), tr("Redo Zoom"), [this]() { zoom_undo_stack.redo(); }); undo_zoom_action->setEnabled(false); redo_zoom_action->setEnabled(false); - zoom_undo_stack.setCallbacks({.index_changed = [this]() { + connections_.push_back(zoom_undo_stack.indexChanged.connect([this]() { undo_zoom_action->setEnabled(zoom_undo_stack.canUndo()); redo_zoom_action->setEnabled(zoom_undo_stack.canRedo()); - }}); + })); reset_zoom_action = toolbar->addWidget(reset_zoom_btn = new ToolButton("zoom-out", tr("Reset Zoom"))); reset_zoom_btn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); @@ -122,16 +121,16 @@ ChartsWidget::ChartsWidget(QWidget *parent) : QFrame(parent) { align_timer->setSingleShot(true); QObject::connect(align_timer, &QTimer::timeout, this, &ChartsWidget::alignCharts); QObject::connect(auto_scroll_timer, &QTimer::timeout, this, &ChartsWidget::doAutoScroll); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &ChartsWidget::removeAll); - QObject::connect(can, &AbstractStream::eventsMerged, this, &ChartsWidget::eventsMerged); - QObject::connect(can, &AbstractStream::msgsReceived, this, &ChartsWidget::updateState); - QObject::connect(can, &AbstractStream::seeking, this, &ChartsWidget::updateState); - QObject::connect(can, &AbstractStream::timeRangeChanged, this, &ChartsWidget::timeRangeChanged); + connections_.push_back(dbc()->fileChanged.connect([this]() { removeAll(); })); + connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &events) { eventsMerged(events); })); + connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); + connections_.push_back(can->seeking.connect([this](double) { updateState(); })); + connections_.push_back(can->timeRangeChanged.connect([this](const auto &range) { timeRangeChanged(range); })); QObject::connect(range_slider, &QSlider::valueChanged, this, &ChartsWidget::setMaxChartRange); QObject::connect(new_plot_btn, &QToolButton::clicked, this, &ChartsWidget::newChart); QObject::connect(remove_all_btn, &QToolButton::clicked, this, &ChartsWidget::removeAll); QObject::connect(reset_zoom_btn, &QToolButton::clicked, this, &ChartsWidget::zoomReset); - QObject::connect(&settings, &Settings::changed, this, &ChartsWidget::settingChanged); + connections_.push_back(settings.changed.connect([this]() { settingChanged(); })); QObject::connect(new_tab_btn, &QToolButton::clicked, this, &ChartsWidget::newTab); QObject::connect(this, &ChartsWidget::seriesChanged, this, &ChartsWidget::updateTabBar); QObject::connect(tabbar, &QTabBar::tabCloseRequested, this, &ChartsWidget::removeTab); diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index 8b3003dcd6..4c143242b2 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -123,6 +123,7 @@ private: QTimer *align_timer; int current_theme = 0; bool value_tip_visible_ = false; + Connections connections_; friend class ChartView; friend class ChartsContainer; }; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index 85832e796b..aab38f5372 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -1,5 +1,4 @@ #include "tools/cabana/chart/signalselector.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index b47bf90b0c..c6cdd9b1b7 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -28,22 +28,22 @@ void UndoStack::clear() { bool was_clean = isClean(); commands_.clear(); index_ = clean_index_ = 0; - if (callbacks_.index_changed) callbacks_.index_changed(); - if (!was_clean && callbacks_.clean_changed) callbacks_.clean_changed(true); + indexChanged(); + if (!was_clean) cleanChanged(true); } void UndoStack::setClean() { if (!isClean()) { clean_index_ = index_; - if (callbacks_.clean_changed) callbacks_.clean_changed(true); + cleanChanged(true); } } void UndoStack::setIndex(int index) { bool was_clean = isClean(); index_ = index; - if (callbacks_.index_changed) callbacks_.index_changed(); - if (isClean() != was_clean && callbacks_.clean_changed) callbacks_.clean_changed(isClean()); + indexChanged(); + if (isClean() != was_clean) cleanChanged(isClean()); } UndoStack *UndoStack::instance() { @@ -51,18 +51,6 @@ UndoStack *UndoStack::instance() { return &undo_stack; } -QtUndoNotifier::QtUndoNotifier(QObject *parent) : QObject(parent) { - UndoStack::instance()->setCallbacks({ - .index_changed = [this]() { emit indexChanged(); }, - .clean_changed = [this](bool clean) { emit cleanChanged(clean); }, - }); -} - -QtUndoNotifier *undoNotifier() { - static QtUndoNotifier notifier; - return ¬ifier; -} - // EditMsgCommand EditMsgCommand::EditMsgCommand(const MessageId &id, const std::string &name, int size, diff --git a/openpilot/tools/cabana/commands.h b/openpilot/tools/cabana/commands.h index 200a4f2f5b..1e40499515 100644 --- a/openpilot/tools/cabana/commands.h +++ b/openpilot/tools/cabana/commands.h @@ -1,13 +1,11 @@ #pragma once -#include #include #include #include #include -#include - +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" @@ -21,11 +19,6 @@ public: class UndoStack { public: - struct Callbacks { - std::function index_changed; - std::function clean_changed; - }; - void push(UndoCommand *cmd); // takes ownership and calls redo() void undo(); void redo(); @@ -36,31 +29,18 @@ public: bool canRedo() const { return index_ < (int)commands_.size(); } std::string undoText() const { return canUndo() ? commands_[index_ - 1]->text : ""; } std::string redoText() const { return canRedo() ? commands_[index_]->text : ""; } - void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } static UndoStack *instance(); + Observable<> indexChanged; + Observable cleanChanged; + private: void setIndex(int index); std::vector> commands_; int index_ = 0; int clean_index_ = 0; - Callbacks callbacks_; }; -// emits Qt signals for the global undo stack -class QtUndoNotifier : public QObject { - Q_OBJECT - -public: - explicit QtUndoNotifier(QObject *parent = nullptr); - -signals: - void indexChanged(); - void cleanChanged(bool clean); -}; - -QtUndoNotifier *undoNotifier(); - class EditMsgCommand : public UndoCommand { public: EditMsgCommand(const MessageId &id, const std::string &name, int size, const std::string &node, diff --git a/openpilot/tools/cabana/core/observable.h b/openpilot/tools/cabana/core/observable.h new file mode 100644 index 0000000000..0c992bf1da --- /dev/null +++ b/openpilot/tools/cabana/core/observable.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace observable_detail { +struct HandlerTable { + virtual ~HandlerTable() = default; + virtual void erase(int id) = 0; +}; +} // namespace observable_detail + +// disconnects on destruction; safe to outlive the Observable +class Connection { +public: + Connection() = default; + Connection(std::weak_ptr table, int id) : table_(std::move(table)), id_(id) {} + Connection(Connection &&other) noexcept { *this = std::move(other); } + Connection &operator=(Connection &&other) noexcept { + if (this != &other) { + disconnect(); + table_ = std::move(other.table_); + id_ = std::exchange(other.id_, -1); + } + return *this; + } + Connection(const Connection &) = delete; + Connection &operator=(const Connection &) = delete; + ~Connection() { disconnect(); } + + void disconnect() { + if (auto table = table_.lock()) table->erase(id_); + table_.reset(); + id_ = -1; + } + +private: + std::weak_ptr table_; + int id_ = -1; +}; + +using Connections = std::vector; + +// main thread only. handlers may disconnect (or destroy the Observable) while being invoked. +template +class Observable { +public: + using Handler = std::function; + + Observable() = default; + Observable(const Observable &) = delete; + Observable &operator=(const Observable &) = delete; + + [[nodiscard]] Connection connect(Handler handler) { + int id = table_->next_id++; + table_->handlers.emplace(id, std::make_shared(std::move(handler))); + return Connection(table_, id); + } + + void operator()(Args... args) const { + auto table = table_; + std::vector ids; + ids.reserve(table->handlers.size()); + for (const auto &[id, _] : table->handlers) ids.push_back(id); + for (int id : ids) { + auto it = table->handlers.find(id); + if (it == table->handlers.end()) continue; + auto handler = it->second; + (*handler)(args...); + } + } + +private: + struct Table : observable_detail::HandlerTable { + std::map> handlers; + int next_id = 0; + void erase(int id) override { handlers.erase(id); } + }; + std::shared_ptr table_ = std::make_shared
(); +}; diff --git a/openpilot/tools/cabana/dbc/dbcmanager.cc b/openpilot/tools/cabana/dbc/dbcmanager.cc index 7a95a4f809..551619ea82 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.cc +++ b/openpilot/tools/cabana/dbc/dbcmanager.cc @@ -17,7 +17,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &dbc_file_name return false; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); return true; } @@ -32,7 +32,7 @@ bool DBCManager::open(const SourceSet &sources, const std::string &name, const s return false; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); return true; } @@ -40,26 +40,26 @@ void DBCManager::close(const SourceSet &sources) { for (auto s : sources) { dbc_files[s] = nullptr; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::close(DBCFile *dbc_file) { for (auto &[_, f] : dbc_files) { if (f.get() == dbc_file) f = nullptr; } - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::closeAll() { dbc_files.clear(); - if (callbacks_.file_changed) callbacks_.file_changed(); + fileChanged(); } void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->addSignal(sig)) { - if (callbacks_.signal_added) callbacks_.signal_added(id, s); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + signalAdded(id, s); + maskUpdated(); } } } @@ -67,8 +67,8 @@ void DBCManager::addSignal(const MessageId &id, const cabana::Signal &sig) { void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, const cabana::Signal &sig) { if (auto m = msg(id)) { if (auto s = m->updateSignal(sig_name, sig)) { - if (callbacks_.signal_updated) callbacks_.signal_updated(s); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + signalUpdated(s); + maskUpdated(); } } } @@ -76,9 +76,9 @@ void DBCManager::updateSignal(const MessageId &id, const std::string &sig_name, void DBCManager::removeSignal(const MessageId &id, const std::string &sig_name) { if (auto m = msg(id)) { if (auto s = m->sig(sig_name)) { - if (callbacks_.signal_removed) callbacks_.signal_removed(s); + signalRemoved(s); m->removeSignal(sig_name); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + maskUpdated(); } } } @@ -87,15 +87,15 @@ void DBCManager::updateMsg(const MessageId &id, const std::string &name, uint32_ auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->updateMsg(id, name, size, node, comment); - if (callbacks_.msg_updated) callbacks_.msg_updated(id); + msgUpdated(id); } void DBCManager::removeMsg(const MessageId &id) { auto dbc_file = findDBCFile(id); assert(dbc_file); // This should be impossible dbc_file->removeMsg(id); - if (callbacks_.msg_removed) callbacks_.msg_removed(id); - if (callbacks_.mask_updated) callbacks_.mask_updated(); + msgRemoved(id); + maskUpdated(); } std::string DBCManager::newMsgName(const MessageId &id) { diff --git a/openpilot/tools/cabana/dbc/dbcmanager.h b/openpilot/tools/cabana/dbc/dbcmanager.h index 5a09fae03d..6bc86a8167 100644 --- a/openpilot/tools/cabana/dbc/dbcmanager.h +++ b/openpilot/tools/cabana/dbc/dbcmanager.h @@ -1,12 +1,12 @@ #pragma once -#include #include #include #include #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcfile.h" typedef std::set SourceSet; @@ -15,16 +15,6 @@ inline bool operator<(const std::shared_ptr &l, const std::shared_ptr signal_added; - std::function signal_removed; - std::function signal_updated; - std::function msg_updated; - std::function msg_removed; - std::function file_changed; - std::function mask_updated; - }; - DBCManager() = default; bool open(const SourceSet &sources, const std::string &dbc_file_name, std::string *error = nullptr); bool open(const SourceSet &sources, const std::string &name, const std::string &content, std::string *error = nullptr); @@ -54,11 +44,17 @@ public: DBCFile *findDBCFile(const uint8_t source); inline DBCFile *findDBCFile(const MessageId &id) { return findDBCFile(id.source); } std::set allDBCFiles(); - void setCallbacks(Callbacks callbacks) { callbacks_ = std::move(callbacks); } + + Observable signalAdded; + Observable signalRemoved; + Observable signalUpdated; + Observable msgUpdated; + Observable msgRemoved; + Observable<> fileChanged; + Observable<> maskUpdated; private: std::map> dbc_files; - Callbacks callbacks_; }; DBCManager *dbc(); diff --git a/openpilot/tools/cabana/dbc/dbcqt.cc b/openpilot/tools/cabana/dbc/dbcqt.cc deleted file mode 100644 index 4354caf3f5..0000000000 --- a/openpilot/tools/cabana/dbc/dbcqt.cc +++ /dev/null @@ -1,18 +0,0 @@ -#include "tools/cabana/dbc/dbcqt.h" - -QtDBCNotifier::QtDBCNotifier(QObject *parent) : QObject(parent) { - dbc()->setCallbacks({ - .signal_added = [this](MessageId id, const cabana::Signal *sig) { emit signalAdded(id, sig); }, - .signal_removed = [this](const cabana::Signal *sig) { emit signalRemoved(sig); }, - .signal_updated = [this](const cabana::Signal *sig) { emit signalUpdated(sig); }, - .msg_updated = [this](MessageId id) { emit msgUpdated(id); }, - .msg_removed = [this](MessageId id) { emit msgRemoved(id); }, - .file_changed = [this]() { emit DBCFileChanged(); }, - .mask_updated = [this]() { emit maskUpdated(); }, - }); -} - -QtDBCNotifier *dbcNotifier() { - static QtDBCNotifier notifier; - return ¬ifier; -} diff --git a/openpilot/tools/cabana/dbc/dbcqt.h b/openpilot/tools/cabana/dbc/dbcqt.h deleted file mode 100644 index b889f854ed..0000000000 --- a/openpilot/tools/cabana/dbc/dbcqt.h +++ /dev/null @@ -1,27 +0,0 @@ -#pragma once - -#include -#include - -#include "tools/cabana/dbc/dbcmanager.h" - -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -class QtDBCNotifier : public QObject { - Q_OBJECT - -public: - explicit QtDBCNotifier(QObject *parent = nullptr); - -signals: - void signalAdded(MessageId id, const cabana::Signal *sig); - void signalRemoved(const cabana::Signal *sig); - void signalUpdated(const cabana::Signal *sig); - void msgUpdated(MessageId id); - void msgRemoved(MessageId id); - void DBCFileChanged(); - void maskUpdated(); -}; - -QtDBCNotifier *dbcNotifier(); diff --git a/openpilot/tools/cabana/detailwidget.cc b/openpilot/tools/cabana/detailwidget.cc index 6b62f54959..d0cccb5992 100644 --- a/openpilot/tools/cabana/detailwidget.cc +++ b/openpilot/tools/cabana/detailwidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/detailwidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -56,9 +55,9 @@ DetailWidget::DetailWidget(ChartsWidget *charts, QWidget *parent) : charts(chart QObject::connect(signal_view, &SignalView::showChart, charts, &ChartsWidget::showChart); QObject::connect(signal_view, &SignalView::highlight, binary_view, &BinaryView::highlight); QObject::connect(tab_widget, &QTabWidget::currentChanged, [this]() { updateState(); }); - QObject::connect(can, &AbstractStream::msgsReceived, this, &DetailWidget::updateState); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &DetailWidget::refresh); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &DetailWidget::refresh); + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { refresh(); })); QObject::connect(tabbar, &QTabBar::customContextMenuRequested, this, &DetailWidget::showTabBarContextMenu); QObject::connect(tabbar, &QTabBar::currentChanged, [this](int index) { if (index != -1) { @@ -97,11 +96,11 @@ void DetailWidget::createToolBar() { layout()->addWidget(toolbar); connect(heatmap_live, &QAbstractButton::toggled, this, [this](bool on) { binary_view->setHeatmapLiveMode(on); }); - connect(can, &AbstractStream::timeRangeChanged, this, [=](const std::optional> &range) { + connections_.push_back(can->timeRangeChanged.connect([=](const std::optional> &range) { auto text = range ? QString("%1 - %2").arg(range->first, 0, 'f', 3).arg(range->second, 0, 'f', 3) : "All"; heatmap_all->setText(text); (range ? heatmap_all : heatmap_live)->setChecked(true); - }); + })); } void DetailWidget::showTabBarContextMenu(const QPoint &pt) { diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h index 0fe1535c7a..48d320c40f 100644 --- a/openpilot/tools/cabana/detailwidget.h +++ b/openpilot/tools/cabana/detailwidget.h @@ -57,6 +57,7 @@ private: SignalView *signal_view; ChartsWidget *charts; QSplitter *splitter; + Connections connections_; }; class CenterWidget : public QWidget { diff --git a/openpilot/tools/cabana/historylog.cc b/openpilot/tools/cabana/historylog.cc index 3fd569c648..26c0f4168f 100644 --- a/openpilot/tools/cabana/historylog.cc +++ b/openpilot/tools/cabana/historylog.cc @@ -1,5 +1,4 @@ #include "tools/cabana/historylog.h" -#include "tools/cabana/dbc/dbcqt.h" #include @@ -10,6 +9,12 @@ #include "tools/cabana/commands.h" #include "tools/cabana/utils/export.h" +HistoryLogModel::HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) { + connections_.push_back(can->seekedTo.connect([this](double) { reset(); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { reset(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { reset(); })); +} + QVariant HistoryLogModel::data(const QModelIndex &index, int role) const { const auto &m = messages[index.row()]; const int col = index.column(); @@ -207,9 +212,6 @@ LogsWidget::LogsWidget(QWidget *parent) : QFrame(parent) { QObject::connect(comp_box, SIGNAL(activated(int)), this, SLOT(filterChanged())); QObject::connect(value_edit, &QLineEdit::textEdited, this, &LogsWidget::filterChanged); QObject::connect(export_btn, &QToolButton::clicked, this, &LogsWidget::exportToCSV); - QObject::connect(can, &AbstractStream::seekedTo, model, &HistoryLogModel::reset); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &HistoryLogModel::reset); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &HistoryLogModel::reset); QObject::connect(model, &HistoryLogModel::modelReset, this, &LogsWidget::modelReset); QObject::connect(model, &HistoryLogModel::rowsInserted, [this]() { export_btn->setEnabled(true); }); } diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 1d3200b200..405fe60fbb 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -22,7 +22,7 @@ class HistoryLogModel : public QAbstractTableModel { Q_OBJECT public: - HistoryLogModel(QObject *parent) : QAbstractTableModel(parent) {} + HistoryLogModel(QObject *parent); void setMessage(const MessageId &message_id); void updateState(bool clear = false); void setFilter(int sig_idx, const QString &value, std::function cmp); @@ -54,6 +54,7 @@ public: std::deque messages; std::vector sigs; bool hex_mode = false; + Connections connections_; }; class LogsWidget : public QFrame { diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 953f573a0b..bcfe1dde41 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -1,5 +1,4 @@ #include "tools/cabana/mainwin.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -41,15 +40,13 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW restoreGeometry(utils::qbytes(settings.geometry)); restoreState(utils::qbytes(settings.window_state)); - // install handlers + // download handlers are called from download threads static auto static_main_win = this; - qRegisterMetaType("uint64_t"); - qRegisterMetaType("SourceSet"); installDownloadProgressHandler([](uint64_t cur, uint64_t total, bool success) { - emit static_main_win->updateProgressBar(cur, total, success); + utils::runOnMainThread([=]() { static_main_win->updateDownloadProgress(cur, total, success); }); }); installMessageHandler([](ReplyMsgType type, const std::string msg) { - emit static_main_win->showMessage(QString::fromStdString(msg), 2000); + utils::runOnMainThread([=]() { static_main_win->statusBar()->showMessage(QString::fromStdString(msg), 2000); }); }); setStyleSheet(QString(R"(QMainWindow::separator { @@ -57,11 +54,14 @@ MainWindow::MainWindow(AbstractStream *stream, const QString &dbc_file) : QMainW height: %1px; /* when horizontal */ })").arg(style()->pixelMetric(QStyle::PM_SplitterWidth))); - QObject::connect(this, &MainWindow::showMessage, statusBar(), &QStatusBar::showMessage); - QObject::connect(this, &MainWindow::updateProgressBar, this, &MainWindow::updateDownloadProgress); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &MainWindow::DBCFileChanged); - QObject::connect(undoNotifier(), &QtUndoNotifier::cleanChanged, this, &MainWindow::undoStackCleanChanged); - QObject::connect(&settings, &Settings::changed, this, &MainWindow::updateStatus); + connections_.push_back(dbc()->fileChanged.connect([this]() { DBCFileChanged(); })); + connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) { undoStackCleanChanged(clean); })); + connections_.push_back(settings.changed.connect([this]() { updateStatus(); })); + + // temporary pump for the non-Qt main thread queue until imgui owns the loop + auto *queue_timer = new QTimer(this); + QObject::connect(queue_timer, &QTimer::timeout, utils::drainMainThreadQueue); + queue_timer->start(10); QTimer::singleShot(0, this, [=]() { stream ? openStream(stream, dbc_file) : selectAndOpenStream(); }); show(); @@ -136,7 +136,7 @@ void MainWindow::createActions() { undo_act->setShortcuts(QKeySequence::Undo); redo_act = edit_menu->addAction(tr("&Redo"), []() { UndoStack::instance()->redo(); }); redo_act->setShortcuts(QKeySequence::Redo); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, this, &MainWindow::updateUndoRedoActions); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { updateUndoRedoActions(); })); updateUndoRedoActions(); // View Menu @@ -259,14 +259,14 @@ void MainWindow::selectAndOpenStream() { if (dlg.exec()) { openStream(dlg.stream(), dlg.dbcFile()); } else if (!can) { - openStream(new DummyStream(this)); + openStream(new DummyStream()); } } void MainWindow::closeStream() { - openStream(new DummyStream(this)); + openStream(new DummyStream()); if (dbc()->nonEmptyDBCCount() > 0) { - emit dbcNotifier()->DBCFileChanged(); + dbc()->fileChanged(); } statusBar()->showMessage(tr("stream closed")); } @@ -336,13 +336,19 @@ void MainWindow::loadFromClipboard(SourceSet s, bool close_all) { } } +// stream threads read the global `can` until its destructor joins them +MainWindow::~MainWindow() { + delete can; + can = nullptr; +} + void MainWindow::openStream(AbstractStream *stream, const QString &dbc_file) { - if (can) { - QObject::connect(can, &QObject::destroyed, this, [=]() { startStream(stream, dbc_file); }); - can->deleteLater(); - } else { - startStream(stream, dbc_file); - } + stream_connections_.clear(); + if (wait_dlg_) wait_dlg_->deleteLater(); + wait_dlg_ = nullptr; + delete can; + can = nullptr; + startStream(stream, dbc_file); } void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { @@ -350,8 +356,7 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { delete messages_widget; delete video_splitter; - can = stream; - can->setParent(this); // take ownership + can = stream; // take ownership can->start(); loadFile(dbc_file); @@ -373,18 +378,19 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { newFile(); } - QObject::connect(can, &AbstractStream::eventsMerged, this, &MainWindow::eventsMerged); + stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); })); if (has_stream) { - auto wait_dlg = new QProgressDialog( + wait_dlg_ = new QProgressDialog( can->liveStreaming() ? tr("Waiting for the live stream to start...") : tr("Loading segment data..."), tr("&Abort"), 0, 100, this); - wait_dlg->setWindowModality(Qt::WindowModal); - wait_dlg->setFixedSize(400, wait_dlg->sizeHint().height()); - QObject::connect(wait_dlg, &QProgressDialog::canceled, this, &MainWindow::close); - QObject::connect(can, &AbstractStream::eventsMerged, wait_dlg, &QProgressDialog::deleteLater); - QObject::connect(this, &MainWindow::updateProgressBar, wait_dlg, [=](uint64_t cur, uint64_t total, bool success) { - wait_dlg->setValue((int)((cur / (double)total) * 100)); + wait_dlg_->setWindowModality(Qt::WindowModal); + wait_dlg_->setFixedSize(400, wait_dlg_->sizeHint().height()); + QObject::connect(wait_dlg_, &QProgressDialog::canceled, this, &MainWindow::close); + wait_dlg_connection_ = can->eventsMerged.connect([this](const MessageEventsMap &) { + wait_dlg_->deleteLater(); + wait_dlg_ = nullptr; + wait_dlg_connection_.disconnect(); }); } } @@ -544,6 +550,7 @@ void MainWindow::remindSaveChanges() { } void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) { + if (wait_dlg_) wait_dlg_->setValue((int)((cur / (double)total) * 100)); if (success && cur < total) { progress_bar->setValue((cur / (double)total) * 100); progress_bar->setFormat(tr("Downloading %p% (%1)").arg(formattedDataSize(total).c_str())); diff --git a/openpilot/tools/cabana/mainwin.h b/openpilot/tools/cabana/mainwin.h index 279ea3b969..a57232ceb2 100644 --- a/openpilot/tools/cabana/mainwin.h +++ b/openpilot/tools/cabana/mainwin.h @@ -19,11 +19,14 @@ #include "tools/cabana/videowidget.h" #include "tools/cabana/tools/findsimilarbits.h" +class QProgressDialog; + class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(AbstractStream *stream, const QString &dbc_file); + ~MainWindow(); void toggleChartsDocking(); void showStatusMessage(const QString &msg, int timeout = 0) { statusBar()->showMessage(msg, timeout); } void loadFile(const QString &fn, SourceSet s = SOURCE_ALL); @@ -42,10 +45,6 @@ public slots: void saveAs(); void saveToClipboard(); -signals: - void showMessage(const QString &msg, int timeout); - void updateProgressBar(uint64_t cur, uint64_t total, bool success); - protected: void startStream(AbstractStream *stream, QString dbc_file); bool eventFilter(QObject *obj, QEvent *event) override; @@ -104,6 +103,10 @@ protected: QAction *redo_act = nullptr; QString car_fingerprint; std::vector default_state; + Connections connections_; + Connections stream_connections_; + Connection wait_dlg_connection_; + QProgressDialog *wait_dlg_ = nullptr; }; class HelpOverlay : public QWidget { diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index b07e3ea0bf..eea07b8711 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -1,5 +1,4 @@ #include "tools/cabana/messageswidget.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -43,9 +42,6 @@ MessagesWidget::MessagesWidget(QWidget *parent) : menu(new QMenu(this)), QWidget QObject::connect(menu, &QMenu::aboutToShow, this, &MessagesWidget::menuAboutToShow); QObject::connect(header, &MessageViewHeader::customContextMenuRequested, this, &MessagesWidget::headerContextMenuEvent); QObject::connect(view->horizontalScrollBar(), &QScrollBar::valueChanged, header, &MessageViewHeader::updateHeaderPositions); - QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, model, &MessageListModel::dbcModified); - QObject::connect(undoNotifier(), &QtUndoNotifier::indexChanged, model, &MessageListModel::dbcModified); QObject::connect(model, &MessageListModel::modelReset, [this]() { if (current_msg_id) { selectMessage(*current_msg_id); @@ -96,7 +92,7 @@ QWidget *MessagesWidget::createToolBar() { QObject::connect(suppress_add, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); QObject::connect(suppress_clear, &QPushButton::clicked, this, &MessagesWidget::suppressHighlighted); - QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, can, &AbstractStream::suppressDefinedSignals); + QObject::connect(suppress_defined_signals, &QCheckBox::stateChanged, this, [](int state) { can->suppressDefinedSignals(state); }); suppressHighlighted(); return toolbar; @@ -161,6 +157,12 @@ void MessagesWidget::setMultiLineBytes(bool multi) { // MessageListModel +MessageListModel::MessageListModel(QObject *parent) : QAbstractTableModel(parent) { + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool has_new_ids) { msgsReceived(msgs, has_new_ids); })); + connections_.push_back(dbc()->fileChanged.connect([this]() { dbcModified(); })); + connections_.push_back(UndoStack::instance()->indexChanged.connect([this]() { dbcModified(); })); +} + QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation == Qt::Horizontal && role == Qt::DisplayRole) { switch (section) { diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 0a9cd256d8..28cab25463 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -31,7 +31,7 @@ public: DATA, }; - MessageListModel(QObject *parent) : QAbstractTableModel(parent) {} + MessageListModel(QObject *parent); QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override { return Column::DATA + 1; } QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; @@ -63,6 +63,7 @@ private: int sort_column = 0; Qt::SortOrder sort_order = Qt::AscendingOrder; int sort_threshold_ = 0; + Connections connections_; }; class MessageView : public QTreeView { diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index 0c8136b84c..cbeded0d10 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -613,6 +613,6 @@ void SettingsDlg::save() { settings.log_livestream = log_livestream->isChecked(); settings.log_path = log_path->text().toStdString(); settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - emit settings.changed(); + settings.changed(); QDialog::accept(); } diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 7357ecf4fc..2c21fab419 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -9,11 +9,10 @@ #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/core/settings.h" -class Settings : public QObject, public CabanaSettingsState { - Q_OBJECT - +class Settings : public CabanaSettingsState { public: Settings(); void save(); @@ -24,8 +23,7 @@ public: std::vector window_state; std::vector message_header_state; -signals: - void changed(); + Observable<> changed; }; class SettingsDlg : public QDialog { diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index ebb5374140..250d05e3b2 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -1,5 +1,4 @@ #include "tools/cabana/signalview.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include @@ -26,12 +25,12 @@ static QString signalTypeToString(cabana::Signal::Type type) { } SignalModel::SignalModel(QObject *parent) : root(new Item), QAbstractItemModel(parent) { - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &SignalModel::refresh); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgUpdated, this, &SignalModel::handleMsgChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::msgRemoved, this, &SignalModel::handleMsgChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalModel::handleSignalAdded); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalModel::handleSignalUpdated); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalRemoved, this, &SignalModel::handleSignalRemoved); + connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); })); + connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); })); + connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); })); + connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); + connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); })); } void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) { @@ -472,11 +471,11 @@ SignalView::SignalView(ChartsWidget *charts, QWidget *parent) : charts(charts), QObject::connect(tree, &QTreeView::entered, [this](const QModelIndex &index) { emit highlight(model->getItem(index)->sig); }); QObject::connect(model, &QAbstractItemModel::modelReset, this, &SignalView::rowsChanged); QObject::connect(model, &QAbstractItemModel::rowsRemoved, this, &SignalView::rowsChanged); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalAdded, this, &SignalView::handleSignalAdded); - QObject::connect(dbcNotifier(), &QtDBCNotifier::signalUpdated, this, &SignalView::handleSignalUpdated); + connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); })); + connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); })); QObject::connect(tree->verticalScrollBar(), &QScrollBar::valueChanged, [this]() { updateState(); }); QObject::connect(tree->verticalScrollBar(), &QScrollBar::rangeChanged, [this]() { updateState(); }); - QObject::connect(can, &AbstractStream::msgsReceived, this, &SignalView::updateState); + connections_.push_back(can->msgsReceived.connect([this](const std::set *msgs, bool) { updateState(msgs); })); QObject::connect(tree->header(), &QHeaderView::sectionResized, [this](int logicalIndex, int oldSize, int newSize) { if (logicalIndex == 1) { value_column_width = newSize; diff --git a/openpilot/tools/cabana/signalview.h b/openpilot/tools/cabana/signalview.h index bccfbab0cc..e2a99ebbd1 100644 --- a/openpilot/tools/cabana/signalview.h +++ b/openpilot/tools/cabana/signalview.h @@ -62,6 +62,7 @@ private: MessageId msg_id; QString filter_str; std::unique_ptr root; + Connections connections_; friend class SignalView; friend class SignalItemDelegate; }; @@ -150,4 +151,5 @@ private: ChartsWidget *charts; QLabel *signal_count_lb; SignalItemDelegate *delegate; + Connections connections_; }; diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index c58a98084f..cc8b2f96ad 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,10 +1,8 @@ #include "tools/cabana/streams/abstractstream.h" -#include "tools/cabana/dbc/dbcqt.h" #include #include -#include #include "common/timing.h" #include "tools/cabana/settings.h" @@ -12,15 +10,41 @@ static const int EVENT_NEXT_BUFFER_SIZE = 6 * 1024 * 1024; // 6MB AbstractStream *can = nullptr; -AbstractStream::AbstractStream(QObject *parent) : QObject(parent) { - assert(parent != nullptr); +AbstractStream::AbstractStream() { event_buffer_ = std::make_unique(EVENT_NEXT_BUFFER_SIZE); - QObject::connect(this, &AbstractStream::privateUpdateLastMsgsSignal, this, &AbstractStream::updateLastMessages, Qt::QueuedConnection); - QObject::connect(this, &AbstractStream::seekedTo, this, &AbstractStream::updateLastMsgsTo); - QObject::connect(this, &AbstractStream::seeking, this, [this](double sec) { current_sec_ = sec; }); - QObject::connect(dbcNotifier(), &QtDBCNotifier::DBCFileChanged, this, &AbstractStream::updateMasks); - QObject::connect(dbcNotifier(), &QtDBCNotifier::maskUpdated, this, &AbstractStream::updateMasks); + // connected first so the stream state is updated before any widget handlers run + connections_.push_back(seekedTo.connect([this](double sec) { updateLastMsgsTo(sec); })); + connections_.push_back(seeking.connect([this](double sec) { current_sec_ = sec; })); + connections_.push_back(dbc()->fileChanged.connect([this]() { updateMasks(); })); + connections_.push_back(dbc()->maskUpdated.connect([this]() { updateMasks(); })); +} + +void AbstractStream::postToMainThread(std::function fn) { + utils::runOnMainThread([alive = std::weak_ptr(alive_), fn = std::move(fn)]() { + if (!alive.expired()) fn(); + }); +} + +void AbstractStream::postToMainThreadAndWait(std::function fn) { + assert(!utils::isMainThread()); + std::unique_lock lock(mutex_); + if (exiting_) return; + auto done = std::make_shared(false); + postToMainThread([this, alive = std::weak_ptr(alive_), done, fn = std::move(fn)]() { + fn(); + if (alive.expired()) return; // fn deleted the stream, the waiter was released by cancelWaits() + std::lock_guard lk(mutex_); + *done = true; + wait_cv_.notify_all(); + }); + wait_cv_.wait(lock, [&]() { return *done || exiting_; }); +} + +void AbstractStream::cancelWaits() { + std::lock_guard lk(mutex_); + exiting_ = true; + wait_cv_.notify_all(); } void AbstractStream::updateMasks() { @@ -97,9 +121,8 @@ void AbstractStream::updateLastMessages() { if (sources.size() != prev_src_size) { updateMasks(); - emit sourcesUpdated(sources); } - emit msgsReceived(&msgs, prev_msg_size != last_msgs.size()); + msgsReceived(&msgs, prev_msg_size != last_msgs.size()); } void AbstractStream::setTimeRange(const std::optional> &range) { @@ -107,7 +130,7 @@ void AbstractStream::setTimeRange(const std::optional> if (time_range_ && (current_sec_ < time_range_->first || current_sec_ >= time_range_->second)) { seekTo(time_range_->first); } - emit timeRangeChanged(time_range_); + timeRangeChanged(time_range_); } void AbstractStream::updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size) { @@ -175,16 +198,16 @@ void AbstractStream::updateLastMsgsTo(double sec) { std::any_of(messages_.cbegin(), messages_.cend(), [this](const auto &m) { return !last_msgs.count(m.first); }); last_msgs = messages_; - emit msgsReceived(nullptr, id_changed); + msgsReceived(nullptr, id_changed); std::lock_guard lk(mutex_); seek_finished_ = true; - seek_finished_cv_.notify_one(); + wait_cv_.notify_all(); } void AbstractStream::waitForSeekFinshed() { std::unique_lock lock(mutex_); - seek_finished_cv_.wait(lock, [this]() { return seek_finished_; }); + wait_cv_.wait(lock, [this]() { return seek_finished_ || exiting_; }); seek_finished_ = false; } @@ -218,16 +241,16 @@ void AbstractStream::mergeEvents(const std::vector &events) { } auto pos = std::upper_bound(all_events_.cbegin(), all_events_.cend(), events.front()->mono_time, CompareCanEvent()); all_events_.insert(pos, events.cbegin(), events.cend()); - emit eventsMerged(msg_events); + eventsMerged(msg_events); } } std::pair AbstractStream::eventsInRange(const MessageId &id, std::optional> time_range) const { - const auto &events = can->events(id); + const auto &events = this->events(id); if (!time_range) return {events.begin(), events.end()}; - auto first = std::lower_bound(events.begin(), events.end(), can->toMonoTime(time_range->first), CompareCanEvent()); - auto last = std::upper_bound(first, events.end(), can->toMonoTime(time_range->second), CompareCanEvent()); + auto first = std::lower_bound(events.begin(), events.end(), toMonoTime(time_range->first), CompareCanEvent()); + auto last = std::upper_bound(first, events.end(), toMonoTime(time_range->second), CompareCanEvent()); return {first, last}; } diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 82cb899937..17d212041a 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -14,16 +15,15 @@ #include "openpilot/cereal/messaging/messaging.h" #include "tools/cabana/core/can_data.h" +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/utils/util.h" #include "tools/replay/util.h" -class AbstractStream : public QObject { - Q_OBJECT - +class AbstractStream { public: - AbstractStream(QObject *parent); - virtual ~AbstractStream() {} + AbstractStream(); + virtual ~AbstractStream() = default; virtual void start() = 0; virtual bool liveStreaming() const { return true; } virtual void seekTo(double ts) {} @@ -56,21 +56,22 @@ public: void clearSuppressed(); void suppressDefinedSignals(bool suppress); -signals: - void paused(); - void resume(); - void seeking(double sec); - void seekedTo(double sec); - void timeRangeChanged(const std::optional> &range); - void eventsMerged(const MessageEventsMap &events_map); - void msgsReceived(const std::set *new_msgs, bool has_new_ids); - void sourcesUpdated(const SourceSet &s); - void privateUpdateLastMsgsSignal(); + // invoked on the main thread + Observable<> paused; + Observable<> resume; + Observable seeking; + Observable seekedTo; + Observable> &> timeRangeChanged; + Observable eventsMerged; + Observable *, bool> msgsReceived; -public: SourceSet sources; protected: + void postToMainThread(std::function fn); // dropped if the stream is destroyed first + void postToMainThreadAndWait(std::function fn); + void cancelWaits(); // call before joining threads, the main thread isn't pumping events during destruction + void requestUpdateLastMessages() { postToMainThread([this]() { updateLastMessages(); }); } void mergeEvents(const std::vector &events); const CanEvent *newEvent(uint64_t mono_time, const cereal::CanData::Reader &c); void updateEvent(const MessageId &id, double sec, const uint8_t *data, uint8_t size); @@ -87,11 +88,14 @@ private: MessageEventsMap events_; std::unordered_map last_msgs; std::unique_ptr event_buffer_; + std::shared_ptr alive_ = std::make_shared(true); + Connections connections_; // Members accessed in multiple threads. (mutex protected) std::mutex mutex_; - std::condition_variable seek_finished_cv_; + std::condition_variable wait_cv_; bool seek_finished_ = false; + bool exiting_ = false; std::set new_msgs_; std::unordered_map messages_; std::unordered_map> masks_; @@ -108,9 +112,7 @@ signals: }; class DummyStream : public AbstractStream { - Q_OBJECT public: - DummyStream(QObject *parent) : AbstractStream(parent) {} std::string routeName() const override { return "No Stream"; } void start() override {} }; diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3bd51c079d..3b4b54707c 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -23,7 +23,7 @@ // DeviceStream -DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { +DeviceStream::DeviceStream(QString address) : zmq_address(address) { } DeviceStream::~DeviceStream() { @@ -61,8 +61,8 @@ void DeviceStream::start() { // fails, the child writes errno and the parent aborts stream start. int err_pipe[2] = {-1, -1}; if (::pipe(err_pipe) != 0) { - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); return; } @@ -79,8 +79,8 @@ void DeviceStream::start() { ::close(err_pipe[1]); if (pid < 0) { ::close(err_pipe[0]); - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); return; } @@ -91,8 +91,8 @@ void DeviceStream::start() { // Child failed to exec; reap and surface the error. int status = 0; ::waitpid(pid, &status, 0); - QMessageBox::warning(nullptr, tr("Error"), - tr("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + QMessageBox::warning(nullptr, "Error", + QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); return; } @@ -144,5 +144,5 @@ OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(p AbstractStream *OpenDeviceWidget::open() { QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); bool msgq = group->checkedId() == 0; - return new DeviceStream(qApp, msgq ? "" : ip); + return new DeviceStream(msgq ? "" : ip); } diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 0e6951c92c..1aa8ce9bff 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -5,9 +5,8 @@ #include class DeviceStream : public LiveStream { - Q_OBJECT public: - DeviceStream(QObject *parent, QString address = {}); + DeviceStream(QString address = {}); ~DeviceStream(); inline std::string routeName() const override { return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString()); diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 019a67e8f2..05a6d44c40 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -38,7 +38,7 @@ struct LiveStream::Logger { uint64_t start_ts; }; -LiveStream::LiveStream(QObject *parent) : AbstractStream(parent) { +LiveStream::LiveStream() { if (settings.log_livestream) { logger = std::make_unique(); } @@ -65,9 +65,9 @@ void LiveStream::stop() { void LiveStream::updateThread() { while (!exit_) { std::this_thread::sleep_for(std::chrono::milliseconds(1000 / fps_)); - // coalesce: skip the emit if the main thread hasn't processed the previous one yet. + // coalesce: skip the request if the main thread hasn't processed the previous one yet. if (!update_pending_.exchange(true)) { - emit privateUpdateLastMsgsSignal(); + requestUpdateLastMessages(); } } } @@ -89,7 +89,7 @@ void LiveStream::handleEvent(kj::ArrayPtr data) { } } -// called on the main thread by the queued privateUpdateLastMsgsSignal connection +// called on the main thread via requestUpdateLastMessages() void LiveStream::updateLastMessages() { update_pending_ = false; fps_ = settings.fps; @@ -142,10 +142,10 @@ void LiveStream::seekTo(double sec) { first_update_ts = nanos_since_boot(); current_event_ts = first_event_ts = std::min(sec * 1e9 + begin_event_ts, lastest_event_ts); post_last_event = (first_event_ts == lastest_event_ts); - emit seekedTo((current_event_ts - begin_event_ts) / 1e9); + seekedTo((current_event_ts - begin_event_ts) / 1e9); } void LiveStream::pause(bool pause) { paused_ = pause; - emit(pause ? paused() : resume()); + pause ? paused() : resume(); } diff --git a/openpilot/tools/cabana/streams/livestream.h b/openpilot/tools/cabana/streams/livestream.h index 5d65b1743f..22587335b0 100644 --- a/openpilot/tools/cabana/streams/livestream.h +++ b/openpilot/tools/cabana/streams/livestream.h @@ -9,10 +9,8 @@ #include "tools/cabana/streams/abstractstream.h" class LiveStream : public AbstractStream { - Q_OBJECT - public: - LiveStream(QObject *parent); + LiveStream(); virtual ~LiveStream(); void start() override; void stop(); diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 7ccb18a756..06c8e6e19c 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -10,7 +10,7 @@ #include #include -PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { +PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) { if (!connect()) { throw std::runtime_error("Failed to connect to panda"); } @@ -181,7 +181,7 @@ void OpenPandaWidget::buildConfigForm() { AbstractStream *OpenPandaWidget::open() { try { - return new PandaStream(qApp, config); + return new PandaStream(config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); return nullptr; diff --git a/openpilot/tools/cabana/streams/pandastream.h b/openpilot/tools/cabana/streams/pandastream.h index f8847f65e5..d764613fe1 100644 --- a/openpilot/tools/cabana/streams/pandastream.h +++ b/openpilot/tools/cabana/streams/pandastream.h @@ -24,9 +24,8 @@ struct PandaStreamConfig { }; class PandaStream : public LiveStream { - Q_OBJECT public: - PandaStream(QObject *parent, PandaStreamConfig config_ = {}); + PandaStream(PandaStreamConfig config_ = {}); ~PandaStream() { stop(); } inline std::string routeName() const override { return "Panda: " + config.serial; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 6d54369aef..68f4fb3056 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -12,17 +12,21 @@ #include "common/util.h" #include "tools/cabana/streams/routes.h" -ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) { +ReplayStream::ReplayStream() { unsetenv("ZMQ"); setenv("COMMA_CACHE", "/tmp/comma_download_cache", 1); op_prefix = std::make_unique(); - QObject::connect(&settings, &Settings::changed, this, [this]() { + settings_connection_ = settings.changed.connect([this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); } +ReplayStream::~ReplayStream() { + cancelWaits(); +} + void ReplayStream::mergeSegments() { auto event_data = replay->getEventData(); for (const auto &[n, seg] : event_data->segments) { @@ -51,14 +55,14 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter([this](const Event *event) { return eventFilter(event); }); - // Forward replay callbacks to corresponding Qt signals. - replay->onSeeking = [this](double sec) { emit seeking(sec); }; + // replay callbacks arrive on replay threads + replay->onSeeking = [this](double sec) { postToMainThread([this, sec]() { seeking(sec); }); }; replay->onSeekedTo = [this](double sec) { - emit seekedTo(sec); + postToMainThread([this, sec]() { seekedTo(sec); }); waitForSeekFinshed(); }; - replay->onQLogLoaded = [this](std::shared_ptr qlog) { emit qLogLoaded(qlog); }; - replay->onSegmentsMerged = [this]() { QMetaObject::invokeMethod(this, &ReplayStream::mergeSegments, Qt::BlockingQueuedConnection); }; + replay->onQLogLoaded = [this](std::shared_ptr qlog) { postToMainThread([this, qlog]() { qLogLoaded(qlog); }); }; + replay->onSegmentsMerged = [this]() { postToMainThreadAndWait([this]() { mergeSegments(); }); }; bool success = replay->load(); if (!success) { @@ -70,18 +74,18 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d "python3 openpilot/tools/lib/auth.py\n\n" "This will grant access to routes from your comma account."; } else { - message = tr("Access Denied. You do not have permission to access route:\n\n%1\n\n" - "This is likely a private route.").arg(QString::fromStdString(route)); + message = QString("Access Denied. You do not have permission to access route:\n\n%1\n\n" + "This is likely a private route.").arg(QString::fromStdString(route)); } - QMessageBox::warning(nullptr, tr("Access Denied"), message); + QMessageBox::warning(nullptr, "Access Denied", message); } else if (replay->lastRouteError() == RouteLoadError::NetworkError) { - QMessageBox::warning(nullptr, tr("Network Error"), - tr("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Network Error", + QString("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); } else if (replay->lastRouteError() == RouteLoadError::FileNotFound) { - QMessageBox::warning(nullptr, tr("Route Not Found"), - tr("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Route Not Found", + QString("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); } else { - QMessageBox::warning(nullptr, tr("Route Load Failed"), tr("Failed to load route: '%1'").arg(QString::fromStdString(route))); + QMessageBox::warning(nullptr, "Route Load Failed", QString("Failed to load route: '%1'").arg(QString::fromStdString(route))); } } return success; @@ -102,7 +106,7 @@ bool ReplayStream::eventFilter(const Event *event) { double ts = millis_since_boot(); if ((ts - prev_update_ts) > (1000.0 / settings.fps)) { - emit privateUpdateLastMsgsSignal(); + requestUpdateLastMessages(); prev_update_ts = ts; } return true; @@ -110,7 +114,7 @@ bool ReplayStream::eventFilter(const Event *event) { void ReplayStream::pause(bool pause) { replay->pause(pause); - emit(pause ? paused() : resume()); + pause ? paused() : resume(); } @@ -161,7 +165,7 @@ AbstractStream *OpenReplayWidget::open() { if (!is_valid_format) { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { - auto replay_stream = std::make_unique(qApp); + auto replay_stream = std::make_unique(); uint32_t flags = REPLAY_FLAG_NONE; if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; diff --git a/openpilot/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h index eecd345715..6f49502ba3 100644 --- a/openpilot/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -10,13 +10,10 @@ #include "tools/cabana/streams/abstractstream.h" #include "tools/replay/replay.h" -Q_DECLARE_METATYPE(std::shared_ptr); - class ReplayStream : public AbstractStream { - Q_OBJECT - public: - ReplayStream(QObject *parent); + ReplayStream(); + ~ReplayStream(); void start() override { replay->start(); } bool loadRoute(const std::string &route, const std::string &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE, bool auto_source = false); bool eventFilter(const Event *event); @@ -36,12 +33,13 @@ public: inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; -signals: - void qLogLoaded(std::shared_ptr qlog); + // invoked on the main thread + Observable> qLogLoaded; private: void mergeSegments(); std::unique_ptr replay = nullptr; + Connection settings_connection_; std::set processed_segments; std::unique_ptr op_prefix; }; diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index b616e7f242..b10f2cab90 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -16,7 +16,7 @@ #include #include -SocketCanStream::SocketCanStream(QObject *parent, SocketCanStreamConfig config_) : config(config_), LiveStream(parent) { +SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) { if (!available()) { throw std::runtime_error("SocketCAN not available"); } @@ -140,7 +140,7 @@ void OpenSocketCanWidget::refreshDevices() { AbstractStream *OpenSocketCanWidget::open() { try { - return new SocketCanStream(qApp, config); + return new SocketCanStream(config); } catch (std::exception &e) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); return nullptr; diff --git a/openpilot/tools/cabana/streams/socketcanstream.h b/openpilot/tools/cabana/streams/socketcanstream.h index 3c5cd184f7..78e856d506 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.h +++ b/openpilot/tools/cabana/streams/socketcanstream.h @@ -9,9 +9,8 @@ struct SocketCanStreamConfig { }; class SocketCanStream : public LiveStream { - Q_OBJECT public: - SocketCanStream(QObject *parent, SocketCanStreamConfig config_ = {}); + SocketCanStream(SocketCanStreamConfig config_ = {}); ~SocketCanStream(); static bool available(); diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index 53be1b0afa..c2ebc2e6b0 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -164,11 +164,10 @@ void test_dbc_manager() { int files_changed = 0; int signals_added = 0; int masks_updated = 0; - manager.setCallbacks({ - .signal_added = [&](MessageId, const cabana::Signal *) { ++signals_added; }, - .file_changed = [&]() { ++files_changed; }, - .mask_updated = [&]() { ++masks_updated; }, - }); + Connections connections; + connections.push_back(manager.signalAdded.connect([&](MessageId, const cabana::Signal *) { ++signals_added; })); + connections.push_back(manager.fileChanged.connect([&]() { ++files_changed; })); + connections.push_back(manager.maskUpdated.connect([&]() { ++masks_updated; })); std::string error; REQUIRE(manager.open(SOURCE_ALL, "test", "BO_ 160 message: 8 XXX\n", &error)); diff --git a/openpilot/tools/cabana/tools/routeinfo.cc b/openpilot/tools/cabana/tools/routeinfo.cc index dc272e3d12..1037d4a206 100644 --- a/openpilot/tools/cabana/tools/routeinfo.cc +++ b/openpilot/tools/cabana/tools/routeinfo.cc @@ -6,7 +6,7 @@ #include "tools/cabana/streams/replaystream.h" RouteInfoDlg::RouteInfoDlg(QWidget *parent) : QDialog(parent) { - auto *replay = qobject_cast(can)->getReplay(); + auto *replay = dynamic_cast(can)->getReplay(); setWindowTitle(tr("Route: %1").arg(QString::fromStdString(replay->route().name()))); auto *table = new QTableWidget(replay->route().segments().size(), 7, this); diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 87a0f89427..9a26e5f977 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -11,7 +11,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -23,6 +25,30 @@ #include #include "common/util.h" +static const std::thread::id main_thread_id = std::this_thread::get_id(); +static std::mutex main_thread_queue_mutex; +static std::vector> main_thread_queue; + +bool utils::isMainThread() { return std::this_thread::get_id() == main_thread_id; } + +void utils::runOnMainThread(std::function fn) { + if (isMainThread()) { + fn(); + } else { + std::lock_guard lk(main_thread_queue_mutex); + main_thread_queue.push_back(std::move(fn)); + } +} + +void utils::drainMainThreadQueue() { + std::vector> fns; + { + std::lock_guard lk(main_thread_queue_mutex); + fns.swap(main_thread_queue); + } + for (auto &fn : fns) fn(); +} + // SegmentTree void SegmentTree::build(const std::vector &arr) { diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index 5a3c62d118..cbcb6d14ee 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -20,9 +21,14 @@ #include #include +#include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +// needed by QVariant::fromValue() in the Qt views; goes away with QVariant +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + inline QColor toQColor(const CabanaColor &color) { return QColor(color.r, color.g, color.b, color.a); } @@ -132,6 +138,10 @@ public: namespace utils { +bool isMainThread(); +// inline on the main thread, queued until drainMainThreadQueue() otherwise +void runOnMainThread(std::function fn); +void drainMainThreadQueue(); QPixmap icon(const QString &id); std::string homePath(); std::filesystem::path configPath(); @@ -175,7 +185,7 @@ public: const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); setIconSize({metric, metric}); theme = settings.theme; - connect(&settings, &Settings::changed, this, &ToolButton::updateIcon); + settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); } void setIcon(const QString &icon) { icon_str = icon; @@ -184,6 +194,7 @@ public: private: void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + Connection settings_connection_; QString icon_str; int theme; }; diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index bd61573658..b1e84586a8 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -27,7 +27,7 @@ static const QColor timeline_colors[] = { }; static Replay *getReplay() { - auto stream = qobject_cast(can); + auto stream = dynamic_cast(can); return stream ? stream->getReplay() : nullptr; } @@ -42,11 +42,11 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { createPlaybackController(); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); - QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); - QObject::connect(can, &AbstractStream::msgsReceived, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::seeking, this, &VideoWidget::updateState); - QObject::connect(can, &AbstractStream::timeRangeChanged, this, &VideoWidget::timeRangeChanged); + connections_.push_back(can->paused.connect([this]() { updatePlayBtnState(); })); + connections_.push_back(can->resume.connect([this]() { updatePlayBtnState(); })); + connections_.push_back(can->msgsReceived.connect([this](const std::set *, bool) { updateState(); })); + connections_.push_back(can->seeking.connect([this](double) { updateState(); })); + connections_.push_back(can->timeRangeChanged.connect([this](const auto &) { timeRangeChanged(); })); updatePlayBtnState(); setWhatsThis(tr(R"( @@ -157,14 +157,14 @@ QWidget *VideoWidget::createCameraWidget() { slider->setTimeRange(can->minSeconds(), can->maxSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); - QObject::connect(can, &AbstractStream::paused, cam_widget, qOverload<>(&StreamCameraView::update)); - QObject::connect(can, &AbstractStream::eventsMerged, this, [this]() { slider->update(); }); - QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); - QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); + connections_.push_back(can->paused.connect([this]() { cam_widget->update(); })); + connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { slider->update(); })); + connections_.push_back(cam_widget->clicked.connect([]() { can->pause(!can->isPaused()); })); + connections_.push_back(cam_widget->availableStreamsUpdated.connect([this](std::set streams) { vipcAvailableStreamsUpdated(streams); })); QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); }); - QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, cam_widget, &StreamCameraView::parseQLog, Qt::QueuedConnection); + connections_.push_back(static_cast(can)->qLogLoaded.connect([this](std::shared_ptr qlog) { cam_widget->parseQLog(qlog); })); slider->installEventFilter(this); return w; } diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index 09f7a9931b..1b6d81406e 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -64,6 +64,7 @@ protected: void timeRangeChanged(); void updateState(); void updatePlayBtnState(); + Connections connections_; QWidget *createCameraWidget(); void createPlaybackController(); void createSpeedDropdown(QToolBar *toolbar); From 7cc48b5bc94e197bfce1ee88d865d16488d8c286 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Thu, 27 Aug 2026 22:00:24 -0700 Subject: [PATCH 11/53] cabana: move RoutesDialog out of streams/ (#38716) --- openpilot/tools/cabana/SConscript | 2 +- .../cabana/{streams/routes.cc => routesdialog.cc} | 12 ++++++------ .../cabana/{streams/routes.h => routesdialog.h} | 0 openpilot/tools/cabana/streams/replaystream.cc | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename openpilot/tools/cabana/{streams/routes.cc => routesdialog.cc} (94%) rename openpilot/tools/cabana/{streams/routes.h => routesdialog.h} (100%) diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index bc366ab6fb..9c34de661d 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', diff --git a/openpilot/tools/cabana/streams/routes.cc b/openpilot/tools/cabana/routesdialog.cc similarity index 94% rename from openpilot/tools/cabana/streams/routes.cc rename to openpilot/tools/cabana/routesdialog.cc index b6f98da533..35e23a6eee 100644 --- a/openpilot/tools/cabana/streams/routes.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -1,4 +1,4 @@ -#include "tools/cabana/streams/routes.h" +#include "tools/cabana/routesdialog.h" #include #include @@ -6,7 +6,6 @@ #include #include -#include #include #include #include @@ -14,6 +13,7 @@ #include #include "json11/json11.hpp" +#include "tools/cabana/utils/util.h" #include "tools/replay/py_downloader.h" namespace { @@ -113,9 +113,9 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { // Fetch devices std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { if (!alive.expired()) parseDeviceList(r, response.first, response.second); - }, Qt::QueuedConnection); + }); }).detach(); } @@ -156,9 +156,9 @@ void RoutesDialog::fetchRoutes() { int request_id = ++fetch_id_; std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - QMetaObject::invokeMethod(qApp, [this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); - }, Qt::QueuedConnection); + }); }).detach(); } diff --git a/openpilot/tools/cabana/streams/routes.h b/openpilot/tools/cabana/routesdialog.h similarity index 100% rename from openpilot/tools/cabana/streams/routes.h rename to openpilot/tools/cabana/routesdialog.h diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 68f4fb3056..56e4bfa084 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -10,7 +10,7 @@ #include "common/timing.h" #include "common/util.h" -#include "tools/cabana/streams/routes.h" +#include "tools/cabana/routesdialog.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); From 9b9e3ea6048a72948922d0e58dcdbbcd876cd658 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:25:11 -0700 Subject: [PATCH 12/53] cabana: string helpers in utils return std::string (#38720) --- openpilot/tools/cabana/SConscript | 3 +- openpilot/tools/cabana/binaryview.cc | 2 +- openpilot/tools/cabana/chart/chartswidget.cc | 2 +- openpilot/tools/cabana/messageswidget.cc | 6 +- openpilot/tools/cabana/signalview.cc | 4 +- openpilot/tools/cabana/tests/test_cabana.cc | 58 ++++++++++++++++++++ openpilot/tools/cabana/utils/strings.cc | 57 +++++++++++++++++++ openpilot/tools/cabana/utils/strings.h | 33 +++++++++++ openpilot/tools/cabana/utils/util.cc | 42 -------------- openpilot/tools/cabana/utils/util.h | 15 +---- openpilot/tools/cabana/videowidget.cc | 2 +- 11 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 openpilot/tools/cabana/utils/strings.cc create mode 100644 openpilot/tools/cabana/utils/strings.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 9c34de661d..662b3952bb 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -100,7 +100,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"] cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/elidedlabel.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] @@ -119,6 +119,7 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_model', 'dbc/dbc.cc'), dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), + dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), ] dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 2f0167107c..160aead6fd 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -385,7 +385,7 @@ QVariant BinaryViewModel::headerData(int section, Qt::Orientation orientation, i QVariant BinaryViewModel::data(const QModelIndex &index, int role) const { auto item = (const BinaryViewModel::Item *)index.internalPointer(); - return role == Qt::ToolTipRole && item && !item->sigs.empty() ? signalToolTip(item->sigs.back()) : QVariant(); + return role == Qt::ToolTipRole && item && !item->sigs.empty() ? QString::fromStdString(utils::signalToolTip(item->sigs.back())) : QVariant(); } // BinaryItemDelegate diff --git a/openpilot/tools/cabana/chart/chartswidget.cc b/openpilot/tools/cabana/chart/chartswidget.cc index 8aa095b65f..2e3e86bab0 100644 --- a/openpilot/tools/cabana/chart/chartswidget.cc +++ b/openpilot/tools/cabana/chart/chartswidget.cc @@ -245,7 +245,7 @@ void ChartsWidget::setIsDocked(bool docked) { void ChartsWidget::updateToolBar() { title_label->setText(tr("Charts: %1").arg(charts.size())); columns_action->setText(tr("Columns: %1").arg(column_count)); - range_lb->setText(utils::formatSeconds(max_chart_range)); + range_lb->setText(QString::fromStdString(utils::formatSeconds(max_chart_range))); bool is_zoomed = can->timeRange().has_value(); range_lb_action->setVisible(!is_zoomed); diff --git a/openpilot/tools/cabana/messageswidget.cc b/openpilot/tools/cabana/messageswidget.cc index eea07b8711..2325bb3ae4 100644 --- a/openpilot/tools/cabana/messageswidget.cc +++ b/openpilot/tools/cabana/messageswidget.cc @@ -195,7 +195,7 @@ QVariant MessageListModel::data(const QModelIndex &index, int role) const { switch (index.column()) { case Column::NAME: return item.name; case Column::SOURCE: return item.id.source != INVALID_SOURCE ? QString::number(item.id.source) : NA; - case Column::ADDRESS: return toHexString(item.id.address); + case Column::ADDRESS: return QString::fromStdString(utils::toHexString(item.id.address)); case Column::NODE: return item.node; case Column::FREQ: return item.id.source != INVALID_SOURCE ? getFreq(can->lastMessage(item.id).freq) : NA; case Column::COUNT: return item.id.source != INVALID_SOURCE ? QString::number(can->lastMessage(item.id).count) : NA; @@ -288,7 +288,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, item.id.source); break; case Column::ADDRESS: - match = toHexString(item.id.address).contains(txt, Qt::CaseInsensitive); + match = QString::fromStdString(utils::toHexString(item.id.address)).contains(txt, Qt::CaseInsensitive); match = match || parseRange(txt, item.id.address, 16); break; case Column::NODE: @@ -301,7 +301,7 @@ bool MessageListModel::match(const MessageListModel::Item &item) { match = parseRange(txt, data.count); break; case Column::DATA: - match = utils::toHex(data.dat).contains(txt, Qt::CaseInsensitive); + match = QString::fromStdString(utils::toHex(data.dat)).contains(txt, Qt::CaseInsensitive); break; } } diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 250d05e3b2..3a6d4dab25 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -153,7 +153,7 @@ QVariant SignalModel::data(const QModelIndex &index, int role) const { if (item->type == Item::Endian) return item->sig->is_little_endian ? Qt::Checked : Qt::Unchecked; if (item->type == Item::Signed) return item->sig->is_signed ? Qt::Checked : Qt::Unchecked; } else if (role == Qt::ToolTipRole && item->type == Item::Sig) { - return (index.column() == 0) ? signalToolTip(item->sig) : QString(); + return (index.column() == 0) ? QString::fromStdString(utils::signalToolTip(item->sig)) : QString(); } } return {}; @@ -570,7 +570,7 @@ void SignalView::signalHovered(const cabana::Signal *sig) { void SignalView::updateToolBar() { signal_count_lb->setText(tr("Signals: %1").arg(model->rowCount())); - sparkline_label->setText(utils::formatSeconds(settings.sparkline_range)); + sparkline_label->setText(QString::fromStdString(utils::formatSeconds(settings.sparkline_range))); } void SignalView::setSparklineRange(int value) { diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index c2ebc2e6b0..e3b8377e02 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -1,10 +1,13 @@ +#include +#include #include #include #include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/utils/strings.h" const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -185,7 +188,62 @@ void test_dbc_manager() { REQUIRE(manager.msg({.source = 0, .address = 160})->sig("speed") != nullptr); } +void test_format_seconds() { + REQUIRE(utils::formatSeconds(0) == "00:00"); + REQUIRE(utils::formatSeconds(59.4) == "00:59"); + REQUIRE(utils::formatSeconds(-1) == "00:00"); + REQUIRE(utils::formatSeconds(61.234, true) == "01:01.234"); + REQUIRE(utils::formatSeconds(3599.9) == "59:59"); + REQUIRE(utils::formatSeconds(3601) == "01:00:01"); + REQUIRE(utils::formatSeconds(3601.5, true) == "01:00:01.500"); + + const char *tz = getenv("TZ"); + const bool had_tz = tz != nullptr; + const std::string saved_tz = had_tz ? tz : ""; + setenv("TZ", "UTC", 1); + tzset(); + REQUIRE(utils::formatSeconds(0, false, true) == "1970-01-01 00:00:00"); + REQUIRE(utils::formatSeconds(1700000000.123, true, true) == "2023-11-14 22:13:20.123"); + if (had_tz) { + setenv("TZ", saved_tz.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); +} + +void test_to_hex() { + REQUIRE(utils::toHex({}) == ""); + REQUIRE(utils::toHex({0x00, 0x0f, 0xab, 0xff}) == "000FABFF"); + REQUIRE(utils::toHex({0x01, 0x02, 0x03}, ' ') == "01 02 03"); + + REQUIRE(utils::toHexString(0) == "0x00"); + REQUIRE(utils::toHexString(0xf) == "0x0F"); + REQUIRE(utils::toHexString(0x1ab) == "0x1AB"); + REQUIRE(utils::toHexString(0x1fffffff) == "0x1FFFFFFF"); +} + +void test_signal_tooltip() { + cabana::Signal sig{}; + sig.name = "speed"; + sig.start_bit = 3; + sig.size = 12; + sig.msb = 14; + sig.lsb = 3; + sig.is_little_endian = true; + sig.is_signed = false; + REQUIRE(utils::signalToolTip(&sig) == R"( + speed
+ Start Bit: 3 Size: 12
+ MSB: 14 LSB: 3
+ Little Endian: Y Signed: N
+ )"); +} + void test_cabana_core() { + test_format_seconds(); + test_to_hex(); + test_signal_tooltip(); test_generate_dbc(); test_comment_order(); test_preserve_original_header(); diff --git a/openpilot/tools/cabana/utils/strings.cc b/openpilot/tools/cabana/utils/strings.cc new file mode 100644 index 0000000000..5590b6078e --- /dev/null +++ b/openpilot/tools/cabana/utils/strings.cc @@ -0,0 +1,57 @@ +#include "tools/cabana/utils/strings.h" + +#include +#include +#include +#include + +#include "tools/cabana/dbc/dbc.h" + +namespace utils { + +std::string formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { + char out[80] = {}; + if (absolute_time) { + const auto ms_total = static_cast(std::llround(sec * 1000.0)); + const std::time_t secs = static_cast(ms_total / 1000); + int millis = static_cast(ms_total % 1000); + if (millis < 0) millis = -millis; + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64] = {}; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + if (!include_milliseconds) return buf; + snprintf(out, sizeof(out), "%s.%03d", buf, millis); + return out; + } + + // Relative duration (not wall-clock). + const bool show_hours = sec > 60 * 60; + int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); + const int hours = total_ms / (3600 * 1000); + const int minutes = (total_ms / (60 * 1000)) % 60; + const int seconds = (total_ms / 1000) % 60; + const int millis = total_ms % 1000; + if (show_hours && include_milliseconds) { + snprintf(out, sizeof(out), "%02d:%02d:%02d.%03d", hours, minutes, seconds, millis); + } else if (show_hours) { + snprintf(out, sizeof(out), "%02d:%02d:%02d", hours, minutes, seconds); + } else if (include_milliseconds) { + snprintf(out, sizeof(out), "%02d:%02d.%03d", minutes, seconds, millis); + } else { + snprintf(out, sizeof(out), "%02d:%02d", minutes, seconds); + } + return out; +} + +std::string signalToolTip(const cabana::Signal *sig) { + std::ostringstream s; + s << "\n " << sig->name << "
\n" + << " Start Bit: " << sig->start_bit << " Size: " << sig->size << "
\n" + << " MSB: " << sig->msb << " LSB: " << sig->lsb << "
\n" + << " Little Endian: " << (sig->is_little_endian ? "Y" : "N") + << " Signed: " << (sig->is_signed ? "Y" : "N") << "
\n "; + return s.str(); +} + +} // namespace utils diff --git a/openpilot/tools/cabana/utils/strings.h b/openpilot/tools/cabana/utils/strings.h new file mode 100644 index 0000000000..f6581c3164 --- /dev/null +++ b/openpilot/tools/cabana/utils/strings.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace cabana { class Signal; } + +namespace utils { + +std::string formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); +std::string signalToolTip(const cabana::Signal *sig); + +inline std::string toHex(const std::vector &dat, char separator = '\0') { + static const char digits[] = "0123456789ABCDEF"; + std::string hex; + hex.reserve(dat.size() * (separator ? 3 : 2)); + for (size_t i = 0; i < dat.size(); ++i) { + if (separator && i) hex += separator; + hex += digits[dat[i] >> 4]; + hex += digits[dat[i] & 0xf]; + } + return hex; +} + +inline std::string toHexString(int value) { + char buf[16] = {}; + snprintf(buf, sizeof(buf), "0x%02X", value); + return buf; +} + +} // namespace utils diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 9a26e5f977..9a44ef5054 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -442,37 +441,6 @@ void setTheme(int theme) { } } -QString formatSeconds(double sec, bool include_milliseconds, bool absolute_time) { - if (absolute_time) { - const auto ms_total = static_cast(std::llround(sec * 1000.0)); - const std::time_t secs = static_cast(ms_total / 1000); - int millis = static_cast(ms_total % 1000); - if (millis < 0) millis = -millis; - std::tm tm{}; - localtime_r(&secs, &tm); - char buf[64]; - std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - if (include_milliseconds) { - return QString::asprintf("%s.%03d", buf, millis); - } - return QString::fromUtf8(buf); - } - - // Relative duration (not wall-clock). - const bool show_hours = sec > 60 * 60; - int total_ms = static_cast(std::llround(std::max(0.0, sec) * 1000.0)); - const int hours = total_ms / (3600 * 1000); - const int minutes = (total_ms / (60 * 1000)) % 60; - const int seconds = (total_ms / 1000) % 60; - const int millis = total_ms % 1000; - if (show_hours) { - return include_milliseconds ? QString::asprintf("%02d:%02d:%02d.%03d", hours, minutes, seconds, millis) - : QString::asprintf("%02d:%02d:%02d", hours, minutes, seconds); - } - return include_milliseconds ? QString::asprintf("%02d:%02d.%03d", minutes, seconds, millis) - : QString::asprintf("%02d:%02d", minutes, seconds); -} - } // namespace utils int num_decimals(double num) { @@ -481,16 +449,6 @@ int num_decimals(double num) { return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; } -QString signalToolTip(const cabana::Signal *sig) { - return QObject::tr(R"( - %1
- Start Bit: %2 Size: %3
- MSB: %4 LSB: %5
- Little Endian: %6 Signed: %7
- )").arg(QString::fromStdString(sig->name)).arg(sig->start_bit).arg(sig->size).arg(sig->msb).arg(sig->lsb) - .arg(sig->is_little_endian ? "Y" : "N").arg(sig->is_signed ? "Y" : "N"); -} - void sigTermHandler(int s) { std::signal(s, SIG_DFL); qApp->quit(); diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index cbcb6d14ee..f2435143c1 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -24,6 +24,7 @@ #include "tools/cabana/core/observable.h" #include "tools/cabana/dbc/dbc.h" #include "tools/cabana/settings.h" +#include "tools/cabana/utils/strings.h" // needed by QVariant::fromValue() in the Qt views; goes away with QVariant Q_DECLARE_METATYPE(MessageId) @@ -149,22 +150,10 @@ bool getClipboardText(std::string *text); // false if no clipboard tool is avai bool setClipboardText(const std::string &text); bool isDarkTheme(); void setTheme(int theme); -QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { auto size = (r.size() - text.size()) / 2; p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); } -inline QString toHex(const std::vector &dat, char separator = '\0') { - static const char digits[] = "0123456789ABCDEF"; - QString hex; - hex.reserve(dat.size() * (separator ? 3 : 2)); - for (size_t i = 0; i < dat.size(); ++i) { - if (separator && i) hex += QLatin1Char(separator); - hex += QLatin1Char(digits[dat[i] >> 4]); - hex += QLatin1Char(digits[dat[i] & 0xf]); - } - return hex; -} // boundary conversions for the remaining Qt byte-array based state APIs template @@ -225,7 +214,5 @@ private: }; int num_decimals(double num); -QString signalToolTip(const cabana::Signal *sig); -inline QString toHexString(int value) { return QString("0x%1").arg(QString::number(value, 16).toUpper(), 2, '0'); } void initApp(int argc, char *argv[], bool disable_hidpi = true); QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index b1e84586a8..23f60f29f1 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -204,7 +204,7 @@ void VideoWidget::timeRangeChanged() { QString VideoWidget::formatTime(double sec, bool include_milliseconds) { if (settings.absolute_time) sec += std::chrono::duration(can->beginDateTime().time_since_epoch()).count(); - return utils::formatSeconds(sec, include_milliseconds, settings.absolute_time); + return QString::fromStdString(utils::formatSeconds(sec, include_milliseconds, settings.absolute_time)); } void VideoWidget::updateState() { From 30f358eb59c191cb6ca98b4ee79e17e5b9b82b63 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 07:25:34 -0700 Subject: [PATCH 13/53] cabana: use std::string in RoutesDialog API results (#38717) --- openpilot/tools/cabana/routesdialog.cc | 24 ++++++++++--------- openpilot/tools/cabana/routesdialog.h | 6 ++--- .../tools/cabana/streams/replaystream.cc | 2 +- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/openpilot/tools/cabana/routesdialog.cc b/openpilot/tools/cabana/routesdialog.cc index 35e23a6eee..9891104c9f 100644 --- a/openpilot/tools/cabana/routesdialog.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -58,13 +58,13 @@ int64_t parseIsoToUnixMs(const std::string &s) { return static_cast(secs) * 1000 + millis; } -QString formatUnixMs(int64_t ms) { +std::string formatUnixMs(int64_t ms) { time_t secs = static_cast(ms / 1000); std::tm tm{}; localtime_r(&secs, &tm); char buf[64]; std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - return QString::fromUtf8(buf); + return buf; } } // namespace @@ -113,17 +113,18 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { // Fetch devices std::thread([this, alive = std::weak_ptr(alive_)]() { std::string result = PyDownloader::getDevices(); - utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result)]() { + auto response = checkApiResponse(result); + utils::runOnMainThread([this, alive, r = std::move(result), response]() { if (!alive.expired()) parseDeviceList(r, response.first, response.second); }); }).detach(); } -void RoutesDialog::parseDeviceList(const QString &json, bool success, int error_code) { +void RoutesDialog::parseDeviceList(const std::string &json, bool success, int error_code) { if (success) { device_list_->clear(); std::string err; - auto doc = json11::Json::parse(json.toStdString(), err); + auto doc = json11::Json::parse(json, err); if (err.empty() && doc.is_array()) { for (const auto &device : doc.array_items()) { QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); @@ -156,16 +157,17 @@ void RoutesDialog::fetchRoutes() { int request_id = ++fetch_id_; std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - utils::runOnMainThread([this, alive, r = QString::fromStdString(result), response = checkApiResponse(result), request_id]() { + auto response = checkApiResponse(result); + utils::runOnMainThread([this, alive, r = std::move(result), response, request_id]() { if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); }); }).detach(); } -void RoutesDialog::parseRouteList(const QString &json, bool success, int error_code) { +void RoutesDialog::parseRouteList(const std::string &json, bool success, int error_code) { if (success) { std::string err; - auto doc = json11::Json::parse(json.toStdString(), err); + auto doc = json11::Json::parse(json, err); if (err.empty() && doc.is_array()) { for (const auto &route : doc.array_items()) { int64_t from_ms = 0, to_ms = 0; @@ -177,7 +179,7 @@ void RoutesDialog::parseRouteList(const QString &json, bool success, int error_c to_ms = static_cast(route["end_time_utc_millis"].number_value()); } const int mins = static_cast((to_ms - from_ms) / 60000); - auto item = new QListWidgetItem(QString("%1 %2min").arg(formatUnixMs(from_ms)).arg(mins)); + auto item = new QListWidgetItem(QString::fromStdString(formatUnixMs(from_ms) + " " + std::to_string(mins) + "min")); item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); route_list_->addItem(item); } @@ -190,7 +192,7 @@ void RoutesDialog::parseRouteList(const QString &json, bool success, int error_c route_list_->setEmptyText(tr("No items")); } -QString RoutesDialog::route() { +std::string RoutesDialog::route() { auto current_item = route_list_->currentItem(); - return current_item ? current_item->data(Qt::UserRole).toString() : ""; + return current_item ? current_item->data(Qt::UserRole).toString().toStdString() : ""; } diff --git a/openpilot/tools/cabana/routesdialog.h b/openpilot/tools/cabana/routesdialog.h index 6ed145603f..44d7f068f9 100644 --- a/openpilot/tools/cabana/routesdialog.h +++ b/openpilot/tools/cabana/routesdialog.h @@ -12,11 +12,11 @@ class RoutesDialog : public QDialog { Q_OBJECT public: RoutesDialog(QWidget *parent); - QString route(); + std::string route(); protected: - void parseDeviceList(const QString &json, bool success, int error_code); - void parseRouteList(const QString &json, bool success, int error_code); + void parseDeviceList(const std::string &json, bool success, int error_code); + void parseRouteList(const std::string &json, bool success, int error_code); void fetchRoutes(); QComboBox *device_list_; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 56e4bfa084..3524c9e600 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -148,7 +148,7 @@ OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(p QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { RoutesDialog route_dlg(this); if (route_dlg.exec()) { - route_edit->setText(route_dlg.route()); + route_edit->setText(QString::fromStdString(route_dlg.route())); } }); } From 131e473f37f3f2762469f98ef11d82a238e42f6a Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:36:26 -0700 Subject: [PATCH 14/53] cabana: move stream open widgets into streamselector (#38715) --- .../tools/cabana/streams/abstractstream.h | 10 - .../tools/cabana/streams/devicestream.cc | 34 +-- openpilot/tools/cabana/streams/devicestream.h | 12 - openpilot/tools/cabana/streams/pandastream.cc | 116 -------- openpilot/tools/cabana/streams/pandastream.h | 22 -- .../tools/cabana/streams/replaystream.cc | 68 ----- openpilot/tools/cabana/streams/replaystream.h | 14 - .../tools/cabana/streams/socketcanstream.cc | 55 ---- .../tools/cabana/streams/socketcanstream.h | 16 -- openpilot/tools/cabana/streamselector.cc | 261 +++++++++++++++++- openpilot/tools/cabana/streamselector.h | 75 +++++ 11 files changed, 335 insertions(+), 348 deletions(-) diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 17d212041a..4f7c0dc3e7 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -101,16 +101,6 @@ private: std::unordered_map> masks_; }; -class AbstractOpenStreamWidget : public QWidget { - Q_OBJECT -public: - AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} - virtual AbstractStream *open() = 0; - -signals: - void enableOpenButton(bool); -}; - class DummyStream : public AbstractStream { public: std::string routeName() const override { return "No Stream"; } diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3b4b54707c..6d580cc912 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -14,12 +14,8 @@ #include "openpilot/cereal/services.h" -#include -#include +#include #include -#include - -#include "tools/cabana/utils/util.h" // DeviceStream @@ -118,31 +114,3 @@ void DeviceStream::streamThread() { handleEvent(kj::ArrayPtr((capnp::word*)msg->getData(), msg->getSize() / sizeof(capnp::word))); } } - -// OpenDeviceWidget - -OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QRadioButton *msgq = new QRadioButton(tr("MSGQ")); - QRadioButton *zmq = new QRadioButton(tr("ZMQ")); - ip_address = new QLineEdit(this); - ip_address->setPlaceholderText(tr("Enter device Ip Address")); - ip_address->setValidator(new IpAddressValidator(this)); - - group = new QButtonGroup(this); - group->addButton(msgq, 0); - group->addButton(zmq, 1); - - QFormLayout *form_layout = new QFormLayout(this); - form_layout->addRow(msgq); - form_layout->addRow(zmq, ip_address); - QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { - ip_address->setEnabled(button == zmq && checked); - }); - zmq->setChecked(true); -} - -AbstractStream *OpenDeviceWidget::open() { - QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); - bool msgq = group->checkedId() == 0; - return new DeviceStream(msgq ? "" : ip); -} diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 1aa8ce9bff..71205e5a80 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -19,15 +19,3 @@ protected: pid_t bridge_pid = -1; const QString zmq_address; }; - -class OpenDeviceWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenDeviceWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *ip_address; - QButtonGroup *group; -}; diff --git a/openpilot/tools/cabana/streams/pandastream.cc b/openpilot/tools/cabana/streams/pandastream.cc index 06c8e6e19c..0e72443c7a 100644 --- a/openpilot/tools/cabana/streams/pandastream.cc +++ b/openpilot/tools/cabana/streams/pandastream.cc @@ -4,12 +4,6 @@ #include #include -#include -#include -#include -#include -#include - PandaStream::PandaStream(PandaStreamConfig config_) : config(config_) { if (!connect()) { throw std::runtime_error("Failed to connect to panda"); @@ -77,113 +71,3 @@ void PandaStream::streamThread() { panda->send_heartbeat(false); } } - -// OpenPandaWidget - -OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - form_layout = new QFormLayout(this); - if (can && dynamic_cast(can) != nullptr) { - form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName())))); - form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); - QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); - return; - } - - QHBoxLayout *serial_layout = new QHBoxLayout(); - serial_layout->addWidget(serial_edit = new QComboBox()); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); - serial_layout->addWidget(refresh); - form_layout->addRow(tr("Serial"), serial_layout); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials); - QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm); - - // Populate serials - refreshSerials(); - buildConfigForm(); -} - -void OpenPandaWidget::refreshSerials() { - serial_edit->clear(); - for (auto serial : Panda::list()) { - serial_edit->addItem(QString::fromStdString(serial)); - } -} - -void OpenPandaWidget::buildConfigForm() { - for (int i = form_layout->rowCount() - 1; i > 0; --i) { - form_layout->removeRow(i); - } - - QString serial = serial_edit->currentText(); - bool has_fd = false; - bool has_panda = !serial.isEmpty(); - if (has_panda) { - try { - Panda panda(serial.toStdString()); - has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); - } catch (const std::exception& e) { - fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); - has_panda = false; - } - } - - if (has_panda) { - config.serial = serial.toStdString(); - config.bus_config.resize(3); - for (int i = 0; i < config.bus_config.size(); i++) { - QHBoxLayout *bus_layout = new QHBoxLayout; - - // CAN Speed - bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):"))); - QComboBox *can_speed = new QComboBox; - for (int j = 0; j < std::size(speeds); j++) { - can_speed->addItem(QString::number(speeds[j])); - - if (data_speeds[j] == config.bus_config[i].can_speed_kbps) { - can_speed->setCurrentIndex(j); - } - } - QObject::connect(can_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];}); - bus_layout->addWidget(can_speed); - - // CAN-FD Speed - if (has_fd) { - QCheckBox *enable_fd = new QCheckBox("CAN-FD"); - bus_layout->addWidget(enable_fd); - bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):"))); - QComboBox *data_speed = new QComboBox; - for (int j = 0; j < std::size(data_speeds); j++) { - data_speed->addItem(QString::number(data_speeds[j])); - - if (data_speeds[j] == config.bus_config[i].data_speed_kbps) { - data_speed->setCurrentIndex(j); - } - } - - data_speed->setEnabled(false); - bus_layout->addWidget(data_speed); - - QObject::connect(data_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];}); - QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled); - QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;}); - } - - form_layout->addRow(tr("Bus %1:").arg(i), bus_layout); - } - } else { - config.serial = ""; - form_layout->addWidget(new QLabel(tr("No panda found"))); - } -} - -AbstractStream *OpenPandaWidget::open() { - try { - return new PandaStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); - return nullptr; - } -} diff --git a/openpilot/tools/cabana/streams/pandastream.h b/openpilot/tools/cabana/streams/pandastream.h index d764613fe1..760b5c7b15 100644 --- a/openpilot/tools/cabana/streams/pandastream.h +++ b/openpilot/tools/cabana/streams/pandastream.h @@ -3,15 +3,9 @@ #include #include -#include -#include - #include "tools/cabana/streams/livestream.h" #include "tools/cabana/panda.h" -const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; -const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; - struct BusConfig { int can_speed_kbps = 500; int data_speed_kbps = 2000; @@ -38,19 +32,3 @@ protected: std::unique_ptr panda; PandaStreamConfig config = {}; }; - -class OpenPandaWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenPandaWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshSerials(); - void buildConfigForm(); - - QComboBox *serial_edit; - QFormLayout *form_layout; - PandaStreamConfig config = {}; -}; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 3524c9e600..e9df400599 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,16 +1,9 @@ #include "tools/cabana/streams/replaystream.h" -#include - -#include -#include -#include #include -#include #include "common/timing.h" #include "common/util.h" -#include "tools/cabana/routesdialog.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); @@ -116,64 +109,3 @@ void ReplayStream::pause(bool pause) { replay->pause(pause); pause ? paused() : resume(); } - - -// OpenReplayWidget - -OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QGridLayout *grid_layout = new QGridLayout(this); - grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); - grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); - route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route")); - auto browse_remote_btn = new QPushButton(tr("Remote route..."), this); - grid_layout->addWidget(browse_remote_btn, 0, 2); - auto browse_local_btn = new QPushButton(tr("Local route..."), this); - grid_layout->addWidget(browse_local_btn, 0, 3); - - QHBoxLayout *camera_layout = new QHBoxLayout(); - for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) - camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); - cameras[0]->setChecked(true); - camera_layout->addStretch(1); - grid_layout->addItem(camera_layout, 1, 1); - - setMinimumWidth(550); - QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { - QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); - if (!dir.isEmpty()) { - route_edit->setText(dir); - settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); - } - }); - QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { - RoutesDialog route_dlg(this); - if (route_dlg.exec()) { - route_edit->setText(QString::fromStdString(route_dlg.route())); - } - }); -} - -AbstractStream *OpenReplayWidget::open() { - QString route = route_edit->text(); - QString data_dir; - if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { - data_dir = route.mid(0, idx + 1); - route = route.mid(idx + 1); - } - - bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0; - if (!is_valid_format) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); - } else { - auto replay_stream = std::make_unique(); - uint32_t flags = REPLAY_FLAG_NONE; - if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; - if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; - if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; - - if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { - return replay_stream.release(); - } - } - return nullptr; -} diff --git a/openpilot/tools/cabana/streams/replaystream.h b/openpilot/tools/cabana/streams/replaystream.h index 6f49502ba3..aad0ac6361 100644 --- a/openpilot/tools/cabana/streams/replaystream.h +++ b/openpilot/tools/cabana/streams/replaystream.h @@ -1,10 +1,8 @@ #pragma once -#include #include #include #include -#include #include "common/prefix.h" #include "tools/cabana/streams/abstractstream.h" @@ -43,15 +41,3 @@ private: std::set processed_segments; std::unique_ptr op_prefix; }; - -class OpenReplayWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenReplayWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - QLineEdit *route_edit; - std::vector cameras; -}; diff --git a/openpilot/tools/cabana/streams/socketcanstream.cc b/openpilot/tools/cabana/streams/socketcanstream.cc index b10f2cab90..f16ed36d9d 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.cc +++ b/openpilot/tools/cabana/streams/socketcanstream.cc @@ -8,13 +8,6 @@ #include #include -#include -#include - -#include -#include -#include -#include SocketCanStream::SocketCanStream(SocketCanStreamConfig config_) : config(config_) { if (!available()) { @@ -98,51 +91,3 @@ void SocketCanStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); } } - -OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addStretch(1); - - QFormLayout *form_layout = new QFormLayout(); - - QHBoxLayout *device_layout = new QHBoxLayout(); - device_edit = new QComboBox(); - device_edit->setFixedWidth(300); - device_layout->addWidget(device_edit); - - QPushButton *refresh = new QPushButton(tr("Refresh")); - refresh->setFixedWidth(100); - device_layout->addWidget(refresh); - form_layout->addRow(tr("Device"), device_layout); - main_layout->addLayout(form_layout); - - main_layout->addStretch(1); - - QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); - QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); }); - - // Populate devices - refreshDevices(); -} - -void OpenSocketCanWidget::refreshDevices() { - device_edit->clear(); - // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) - std::error_code ec; - for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { - std::ifstream type_file(entry.path() / "type"); - int type = 0; - if (type_file >> type && type == 280) { - device_edit->addItem(QString::fromStdString(entry.path().filename().string())); - } - } -} - -AbstractStream *OpenSocketCanWidget::open() { - try { - return new SocketCanStream(config); - } catch (std::exception &e) { - QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); - return nullptr; - } -} diff --git a/openpilot/tools/cabana/streams/socketcanstream.h b/openpilot/tools/cabana/streams/socketcanstream.h index 78e856d506..c44a498221 100644 --- a/openpilot/tools/cabana/streams/socketcanstream.h +++ b/openpilot/tools/cabana/streams/socketcanstream.h @@ -1,7 +1,5 @@ #pragma once -#include - #include "tools/cabana/streams/livestream.h" struct SocketCanStreamConfig { @@ -25,17 +23,3 @@ protected: SocketCanStreamConfig config = {}; int sock_fd = -1; }; - -class OpenSocketCanWidget : public AbstractOpenStreamWidget { - Q_OBJECT - -public: - OpenSocketCanWidget(QWidget *parent = nullptr); - AbstractStream *open() override; - -private: - void refreshDevices(); - - QComboBox *device_edit; - SocketCanStreamConfig config = {}; -}; diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 7e8adc568d..dae022be75 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -1,18 +1,275 @@ #include "tools/cabana/streamselector.h" #include +#include #include +#include #include +#include #include +#include +#include #include "tools/cabana/streams/devicestream.h" -#include "tools/cabana/streams/pandastream.h" #include "tools/cabana/streams/replaystream.h" +#include "tools/cabana/routesdialog.h" + +// OpenReplayWidget + +OpenReplayWidget::OpenReplayWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QGridLayout *grid_layout = new QGridLayout(this); + grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); + grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); + route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route")); + auto browse_remote_btn = new QPushButton(tr("Remote route..."), this); + grid_layout->addWidget(browse_remote_btn, 0, 2); + auto browse_local_btn = new QPushButton(tr("Local route..."), this); + grid_layout->addWidget(browse_local_btn, 0, 3); + + QHBoxLayout *camera_layout = new QHBoxLayout(); + for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")}) + camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this))); + cameras[0]->setChecked(true); + camera_layout->addStretch(1); + grid_layout->addItem(camera_layout, 1, 1); + + setMinimumWidth(550); + QObject::connect(browse_local_btn, &QPushButton::clicked, [=]() { + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), QString::fromStdString(settings.last_route_dir)); + if (!dir.isEmpty()) { + route_edit->setText(dir); + settings.last_route_dir = std::filesystem::absolute(dir.toStdString()).parent_path().string(); + } + }); + QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() { + RoutesDialog route_dlg(this); + if (route_dlg.exec()) { + route_edit->setText(QString::fromStdString(route_dlg.route())); + } + }); +} + +AbstractStream *OpenReplayWidget::open() { + QString route = route_edit->text(); + QString data_dir; + if (int idx = route.lastIndexOf('/'); idx != -1 && util::file_exists(route.toStdString())) { + data_dir = route.mid(0, idx + 1); + route = route.mid(idx + 1); + } + + bool is_valid_format = Route::parseRoute(route.toStdString()).str.size() > 0; + if (!is_valid_format) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); + } else { + auto replay_stream = std::make_unique(); + uint32_t flags = REPLAY_FLAG_NONE; + if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; + if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; + if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; + + if (replay_stream->loadRoute(route.toStdString(), data_dir.toStdString(), flags)) { + return replay_stream.release(); + } + } + return nullptr; +} + +// OpenPandaWidget + +static const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; +static const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; + +OpenPandaWidget::OpenPandaWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + form_layout = new QFormLayout(this); + if (can && dynamic_cast(can) != nullptr) { + form_layout->addWidget(new QLabel(tr("Already connected to %1.").arg(QString::fromStdString(can->routeName())))); + form_layout->addWidget(new QLabel("Close the current connection via [File menu -> Close Stream] before connecting to another Panda.")); + QTimer::singleShot(0, [this]() { emit enableOpenButton(false); }); + return; + } + + QHBoxLayout *serial_layout = new QHBoxLayout(); + serial_layout->addWidget(serial_edit = new QComboBox()); + + QPushButton *refresh = new QPushButton(tr("Refresh")); + refresh->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Preferred); + serial_layout->addWidget(refresh); + form_layout->addRow(tr("Serial"), serial_layout); + + QObject::connect(refresh, &QPushButton::clicked, this, &OpenPandaWidget::refreshSerials); + QObject::connect(serial_edit, &QComboBox::currentTextChanged, this, &OpenPandaWidget::buildConfigForm); + + // Populate serials + refreshSerials(); + buildConfigForm(); +} + +void OpenPandaWidget::refreshSerials() { + serial_edit->clear(); + for (auto serial : Panda::list()) { + serial_edit->addItem(QString::fromStdString(serial)); + } +} + +void OpenPandaWidget::buildConfigForm() { + for (int i = form_layout->rowCount() - 1; i > 0; --i) { + form_layout->removeRow(i); + } + + QString serial = serial_edit->currentText(); + bool has_fd = false; + bool has_panda = !serial.isEmpty(); + if (has_panda) { + try { + Panda panda(serial.toStdString()); + has_fd = (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA) || (panda.hw_type == cereal::PandaState::PandaType::RED_PANDA_V2); + } catch (const std::exception& e) { + fprintf(stderr, "failed to open panda %s\n", serial.toUtf8().constData()); + has_panda = false; + } + } + + if (has_panda) { + config.serial = serial.toStdString(); + config.bus_config.resize(3); + for (int i = 0; i < config.bus_config.size(); i++) { + QHBoxLayout *bus_layout = new QHBoxLayout; + + // CAN Speed + bus_layout->addWidget(new QLabel(tr("CAN Speed (kbps):"))); + QComboBox *can_speed = new QComboBox; + for (int j = 0; j < std::size(speeds); j++) { + can_speed->addItem(QString::number(speeds[j])); + + if (data_speeds[j] == config.bus_config[i].can_speed_kbps) { + can_speed->setCurrentIndex(j); + } + } + QObject::connect(can_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].can_speed_kbps = speeds[index];}); + bus_layout->addWidget(can_speed); + + // CAN-FD Speed + if (has_fd) { + QCheckBox *enable_fd = new QCheckBox("CAN-FD"); + bus_layout->addWidget(enable_fd); + bus_layout->addWidget(new QLabel(tr("Data Speed (kbps):"))); + QComboBox *data_speed = new QComboBox; + for (int j = 0; j < std::size(data_speeds); j++) { + data_speed->addItem(QString::number(data_speeds[j])); + + if (data_speeds[j] == config.bus_config[i].data_speed_kbps) { + data_speed->setCurrentIndex(j); + } + } + + data_speed->setEnabled(false); + bus_layout->addWidget(data_speed); + + QObject::connect(data_speed, qOverload(&QComboBox::currentIndexChanged), [=](int index) {config.bus_config[i].data_speed_kbps = data_speeds[index];}); + QObject::connect(enable_fd, &QCheckBox::stateChanged, data_speed, &QComboBox::setEnabled); + QObject::connect(enable_fd, &QCheckBox::stateChanged, [=](int state) {config.bus_config[i].can_fd = (bool)state;}); + } + + form_layout->addRow(tr("Bus %1:").arg(i), bus_layout); + } + } else { + config.serial = ""; + form_layout->addWidget(new QLabel(tr("No panda found"))); + } +} + +AbstractStream *OpenPandaWidget::open() { + try { + return new PandaStream(config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); + return nullptr; + } +} + +// OpenDeviceWidget + +OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QRadioButton *msgq = new QRadioButton(tr("MSGQ")); + QRadioButton *zmq = new QRadioButton(tr("ZMQ")); + ip_address = new QLineEdit(this); + ip_address->setPlaceholderText(tr("Enter device Ip Address")); + ip_address->setValidator(new IpAddressValidator(this)); + + group = new QButtonGroup(this); + group->addButton(msgq, 0); + group->addButton(zmq, 1); + + QFormLayout *form_layout = new QFormLayout(this); + form_layout->addRow(msgq); + form_layout->addRow(zmq, ip_address); + QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { + ip_address->setEnabled(button == zmq && checked); + }); + zmq->setChecked(true); +} + +AbstractStream *OpenDeviceWidget::open() { + QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + bool msgq = group->checkedId() == 0; + return new DeviceStream(msgq ? "" : ip); +} + #ifdef __linux__ -#include "tools/cabana/streams/socketcanstream.h" +// OpenSocketCanWidget + +OpenSocketCanWidget::OpenSocketCanWidget(QWidget *parent) : AbstractOpenStreamWidget(parent) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addStretch(1); + + QFormLayout *form_layout = new QFormLayout(); + + QHBoxLayout *device_layout = new QHBoxLayout(); + device_edit = new QComboBox(); + device_edit->setFixedWidth(300); + device_layout->addWidget(device_edit); + + QPushButton *refresh = new QPushButton(tr("Refresh")); + refresh->setFixedWidth(100); + device_layout->addWidget(refresh); + form_layout->addRow(tr("Device"), device_layout); + main_layout->addLayout(form_layout); + + main_layout->addStretch(1); + + QObject::connect(refresh, &QPushButton::clicked, this, &OpenSocketCanWidget::refreshDevices); + QObject::connect(device_edit, &QComboBox::currentTextChanged, this, [=]{ config.device = device_edit->currentText().toStdString(); }); + + // Populate devices + refreshDevices(); +} + +void OpenSocketCanWidget::refreshDevices() { + device_edit->clear(); + // Scan /sys/class/net/ for CAN interfaces (type 280 = ARPHRD_CAN) + std::error_code ec; + for (const auto &entry : std::filesystem::directory_iterator("/sys/class/net", ec)) { + std::ifstream type_file(entry.path() / "type"); + int type = 0; + if (type_file >> type && type == 280) { + device_edit->addItem(QString::fromStdString(entry.path().filename().string())); + } + } +} + +AbstractStream *OpenSocketCanWidget::open() { + try { + return new SocketCanStream(config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to SocketCAN device: '%1'").arg(e.what())); + return nullptr; + } +} #endif +// StreamSelector + StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Open stream")); QVBoxLayout *layout = new QVBoxLayout(this); diff --git a/openpilot/tools/cabana/streamselector.h b/openpilot/tools/cabana/streamselector.h index 0919195e4e..1210806a3b 100644 --- a/openpilot/tools/cabana/streamselector.h +++ b/openpilot/tools/cabana/streamselector.h @@ -1,11 +1,86 @@ #pragma once +#include + +#include +#include +#include #include #include +#include #include #include #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/streams/pandastream.h" +#ifdef __linux__ +#include "tools/cabana/streams/socketcanstream.h" +#endif + +class AbstractOpenStreamWidget : public QWidget { + Q_OBJECT +public: + AbstractOpenStreamWidget(QWidget *parent = nullptr) : QWidget(parent) {} + virtual AbstractStream *open() = 0; + +signals: + void enableOpenButton(bool); +}; + +class OpenReplayWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenReplayWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + QLineEdit *route_edit; + std::vector cameras; +}; + +class OpenPandaWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenPandaWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + void refreshSerials(); + void buildConfigForm(); + + QComboBox *serial_edit; + QFormLayout *form_layout; + PandaStreamConfig config = {}; +}; + +class OpenDeviceWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenDeviceWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + QLineEdit *ip_address; + QButtonGroup *group; +}; + +#ifdef __linux__ +// no Q_OBJECT: moc does not define __linux__ and would otherwise skip this class +class OpenSocketCanWidget : public AbstractOpenStreamWidget { +public: + OpenSocketCanWidget(QWidget *parent = nullptr); + AbstractStream *open() override; + +private: + void refreshDevices(); + + QComboBox *device_edit; + SocketCanStreamConfig config = {}; +}; +#endif class StreamSelector : public QDialog { Q_OBJECT From 0f9c753e6ec5c7af9ea37f9020ab3cc66bed5db1 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:46:45 -0700 Subject: [PATCH 15/53] cabana: remove Qt from livestream (#38722) --- openpilot/tools/cabana/streams/livestream.cc | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 05a6d44c40..71b5d9a1ec 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -21,12 +21,9 @@ struct LiveStream::Logger { localtime_r(&start_time, &local_time); std::ostringstream date; date << std::put_time(&local_time, "%Y-%m-%d--%H-%M-%S"); - QString dir = QString("%1/%2--%3") - .arg(QString::fromStdString(settings.log_path)) - .arg(QString::fromStdString(date.str())) - .arg(n); - util::create_directories(dir.toStdString(), 0755); - fs.reset(new std::ofstream((dir + "/rlog").toStdString(), std::ios::binary | std::ios::out)); + std::string dir = settings.log_path + "/" + date.str() + "--" + std::to_string(n); + util::create_directories(dir, 0755); + fs.reset(new std::ofstream(dir + "/rlog", std::ios::binary | std::ios::out)); } auto bytes = data.asBytes(); From 6e0f4f46306d6c569d6a52a4bbbea6537354d898 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:55:28 -0700 Subject: [PATCH 16/53] cabana: split SettingsDialog out of settings (#38719) cabana: split SettingsDialog out of settings.{h,cc} --- openpilot/tools/cabana/SConscript | 2 +- openpilot/tools/cabana/mainwin.cc | 3 +- openpilot/tools/cabana/settings.cc | 89 ---------------------- openpilot/tools/cabana/settings.h | 20 ----- openpilot/tools/cabana/settingsdialog.cc | 94 ++++++++++++++++++++++++ openpilot/tools/cabana/settingsdialog.h | 21 ++++++ openpilot/tools/cabana/utils/util.h | 5 ++ 7 files changed, 123 insertions(+), 111 deletions(-) create mode 100644 openpilot/tools/cabana/settingsdialog.cc create mode 100644 openpilot/tools/cabana/settingsdialog.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 662b3952bb..75e67d99e3 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -102,7 +102,7 @@ cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc' 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'panda.cc', + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] if arch != "Darwin": cabana_srcs += ['streams/socketcanstream.cc'] diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index bcfe1dde41..3ae5896a2d 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -19,6 +19,7 @@ #include "json11/json11.hpp" #include "tools/cabana/commands.h" +#include "tools/cabana/settingsdialog.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" @@ -617,7 +618,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { } void MainWindow::setOption() { - SettingsDlg dlg(this); + SettingsDialog dlg(this); dlg.exec(); } diff --git a/openpilot/tools/cabana/settings.cc b/openpilot/tools/cabana/settings.cc index cbeded0d10..bacbbd5330 100644 --- a/openpilot/tools/cabana/settings.cc +++ b/openpilot/tools/cabana/settings.cc @@ -24,19 +24,11 @@ #include #endif -#include -#include -#include -#include -#include #include #include "json11/json11.hpp" #include "tools/cabana/utils/util.h" -const int MIN_CACHE_MINIUTES = 30; -const int MAX_CACHE_MINIUTES = 120; - Settings settings; namespace { @@ -535,84 +527,3 @@ void Settings::save() { settingsOp(stored_settings.values, [](auto &s, const char *key, const auto &value) { writeSetting(s, key, value); }); saveSettings(stored_settings.values); } - -// SettingsDlg - -SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { - setWindowTitle(tr("Settings")); - QVBoxLayout *main_layout = new QVBoxLayout(this); - QGroupBox *groupbox = new QGroupBox("General"); - QFormLayout *form_layout = new QFormLayout(groupbox); - - form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); - theme->setToolTip(tr("You may need to restart cabana after changes theme")); - theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); - theme->setCurrentIndex(settings.theme); - - form_layout->addRow("FPS", fps = new QSpinBox(this)); - fps->setRange(10, 100); - fps->setSingleStep(10); - fps->setValue(settings.fps); - - form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); - cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); - cached_minutes->setSingleStep(1); - cached_minutes->setValue(settings.max_cached_minutes); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("New Signal Settings"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); - drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); - drag_direction->setCurrentIndex(settings.drag_direction); - main_layout->addWidget(groupbox); - - groupbox = new QGroupBox("Chart"); - form_layout = new QFormLayout(groupbox); - form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); - chart_height->setRange(100, 500); - chart_height->setSingleStep(10); - chart_height->setValue(settings.chart_height); - main_layout->addWidget(groupbox); - - log_livestream = new QGroupBox(tr("Enable live stream logging"), this); - log_livestream->setCheckable(true); - log_livestream->setChecked(settings.log_livestream); - QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); - path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); - log_path->setReadOnly(true); - auto browse_btn = new QPushButton(tr("B&rowse...")); - path_layout->addWidget(browse_btn); - main_layout->addWidget(log_livestream); - - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - main_layout->addWidget(buttonBox); - setFixedSize(400, sizeHint().height()); - - QObject::connect(browse_btn, &QPushButton::clicked, [this]() { - QString fn = QFileDialog::getExistingDirectory( - this, tr("Log File Location"), - QString::fromStdString(utils::homePath()), - QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); - if (!fn.isEmpty()) { - log_path->setText(fn); - } - }); - QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); - QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDlg::save); -} - -void SettingsDlg::save() { - if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { - // set theme before emit changed - utils::setTheme(settings.theme); - } - settings.fps = fps->value(); - settings.max_cached_minutes = cached_minutes->value(); - settings.chart_height = chart_height->value(); - settings.log_livestream = log_livestream->isChecked(); - settings.log_path = log_path->text().toStdString(); - settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); - settings.changed(); - QDialog::accept(); -} diff --git a/openpilot/tools/cabana/settings.h b/openpilot/tools/cabana/settings.h index 2c21fab419..52353dc667 100644 --- a/openpilot/tools/cabana/settings.h +++ b/openpilot/tools/cabana/settings.h @@ -3,12 +3,6 @@ #include #include -#include -#include -#include -#include -#include - #include "tools/cabana/core/observable.h" #include "tools/cabana/core/settings.h" @@ -26,18 +20,4 @@ public: Observable<> changed; }; -class SettingsDlg : public QDialog { -public: - SettingsDlg(QWidget *parent); - void save(); - QSpinBox *fps; - QSpinBox *cached_minutes; - QSpinBox *chart_height; - QComboBox *chart_series_type; - QComboBox *theme; - QGroupBox *log_livestream; - QLineEdit *log_path; - QComboBox *drag_direction; -}; - extern Settings settings; diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc new file mode 100644 index 0000000000..3a49de5746 --- /dev/null +++ b/openpilot/tools/cabana/settingsdialog.cc @@ -0,0 +1,94 @@ +#include "tools/cabana/settingsdialog.h" + +#include + +#include +#include +#include +#include +#include + +#include "tools/cabana/settings.h" +#include "tools/cabana/utils/util.h" + +const int MIN_CACHE_MINIUTES = 30; +const int MAX_CACHE_MINIUTES = 120; + +SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Settings")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGroupBox *groupbox = new QGroupBox("General"); + QFormLayout *form_layout = new QFormLayout(groupbox); + + form_layout->addRow(tr("Color Theme"), theme = new QComboBox(this)); + theme->setToolTip(tr("You may need to restart cabana after changes theme")); + theme->addItems({tr("Automatic"), tr("Light"), tr("Dark")}); + theme->setCurrentIndex(settings.theme); + + form_layout->addRow("FPS", fps = new QSpinBox(this)); + fps->setRange(10, 100); + fps->setSingleStep(10); + fps->setValue(settings.fps); + + form_layout->addRow(tr("Max Cached Minutes"), cached_minutes = new QSpinBox(this)); + cached_minutes->setRange(MIN_CACHE_MINIUTES, MAX_CACHE_MINIUTES); + cached_minutes->setSingleStep(1); + cached_minutes->setValue(settings.max_cached_minutes); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("New Signal Settings"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Drag Direction"), drag_direction = new QComboBox(this)); + drag_direction->addItems({tr("MSB First"), tr("LSB First"), tr("Always Little Endian"), tr("Always Big Endian")}); + drag_direction->setCurrentIndex(settings.drag_direction); + main_layout->addWidget(groupbox); + + groupbox = new QGroupBox("Chart"); + form_layout = new QFormLayout(groupbox); + form_layout->addRow(tr("Chart Height"), chart_height = new QSpinBox(this)); + chart_height->setRange(100, 500); + chart_height->setSingleStep(10); + chart_height->setValue(settings.chart_height); + main_layout->addWidget(groupbox); + + log_livestream = new QGroupBox(tr("Enable live stream logging"), this); + log_livestream->setCheckable(true); + log_livestream->setChecked(settings.log_livestream); + QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); + path_layout->addWidget(log_path = new QLineEdit(QString::fromStdString(settings.log_path), this)); + log_path->setReadOnly(true); + auto browse_btn = new QPushButton(tr("B&rowse...")); + path_layout->addWidget(browse_btn); + main_layout->addWidget(log_livestream); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + main_layout->addWidget(buttonBox); + setFixedSize(400, sizeHint().height()); + + QObject::connect(browse_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getExistingDirectory( + this, tr("Log File Location"), + QString::fromStdString(utils::homePath()), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!fn.isEmpty()) { + log_path->setText(fn); + } + }); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::save); +} + +void SettingsDialog::save() { + if (std::exchange(settings.theme, theme->currentIndex()) != settings.theme) { + // set theme before emit changed + utils::setTheme(settings.theme); + } + settings.fps = fps->value(); + settings.max_cached_minutes = cached_minutes->value(); + settings.chart_height = chart_height->value(); + settings.log_livestream = log_livestream->isChecked(); + settings.log_path = log_path->text().toStdString(); + settings.drag_direction = (Settings::DragDirection)drag_direction->currentIndex(); + settings.changed(); + QDialog::accept(); +} diff --git a/openpilot/tools/cabana/settingsdialog.h b/openpilot/tools/cabana/settingsdialog.h new file mode 100644 index 0000000000..c5c723c1cb --- /dev/null +++ b/openpilot/tools/cabana/settingsdialog.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include +#include +#include +#include + +class SettingsDialog : public QDialog { +public: + SettingsDialog(QWidget *parent); + void save(); + QSpinBox *fps; + QSpinBox *cached_minutes; + QSpinBox *chart_height; + QComboBox *chart_series_type; + QComboBox *theme; + QGroupBox *log_livestream; + QLineEdit *log_path; + QComboBox *drag_direction; +}; diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index f2435143c1..77a634fc9e 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -12,9 +12,14 @@ #include #include +#include +#include #include #include +#include +#include #include +#include #include #include #include From 46f612224cf6953a845ddc6ed7aadaa9f2128e6b Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:57:47 -0700 Subject: [PATCH 17/53] cabana: split comma API route fetching out of RoutesDialog (#38721) --- openpilot/tools/cabana/SConscript | 5 +- openpilot/tools/cabana/routes.cc | 115 ++++++++++++++++++ openpilot/tools/cabana/routes.h | 42 +++++++ openpilot/tools/cabana/routesdialog.cc | 126 +++----------------- openpilot/tools/cabana/routesdialog.h | 7 +- openpilot/tools/cabana/tests/test_cabana.cc | 62 ++++++++++ 6 files changed, 246 insertions(+), 111 deletions(-) create mode 100644 openpilot/tools/cabana/routes.cc create mode 100644 openpilot/tools/cabana/routes.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index 75e67d99e3..f7db3f23b7 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -99,7 +99,7 @@ cabana_env.Command(assets, "assets/assets.qrc", f"rcc $SOURCES -o $TARGET") cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"])) cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', - 'routesdialog.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', + 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', @@ -120,8 +120,9 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), + dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), ] - dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects) + dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) output_json_file = 'openpilot/tools/cabana/dbc/car_fingerprint_to_dbc.json' generate_dbc = cabana_env.Command('#' + output_json_file, diff --git a/openpilot/tools/cabana/routes.cc b/openpilot/tools/cabana/routes.cc new file mode 100644 index 0000000000..50a1eacf8d --- /dev/null +++ b/openpilot/tools/cabana/routes.cc @@ -0,0 +1,115 @@ +#include "tools/cabana/routes.h" + +#include +#include +#include +#include + +#include "json11/json11.hpp" +#include "tools/replay/py_downloader.h" + +namespace routes { + +std::pair checkApiResponse(const std::string &result) { + if (result.empty()) return {false, 500}; + std::string err; + auto doc = json11::Json::parse(result, err); + if (!err.empty()) return {false, 500}; + if (doc.is_object() && doc["error"].is_string()) { + return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; + } + return {true, 0}; +} + +int64_t nowUnixMs() { + return std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()) + .count(); +} + +int64_t parseIsoToUnixMs(const std::string &s) { + std::string bytes = s; + if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); + int millis = 0; + auto dot = bytes.find('.'); + if (dot != std::string::npos) { + std::string frac = bytes.substr(dot + 1); + bytes = bytes.substr(0, dot); + while (frac.size() < 3) frac.push_back('0'); + millis = std::atoi(frac.substr(0, 3).c_str()); + } + std::tm tm{}; + const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); + if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); + if (!ret) return 0; + time_t secs = timegm(&tm); + if (secs == static_cast(-1)) return 0; + return static_cast(secs) * 1000 + millis; +} + +std::string formatUnixMs(int64_t ms) { + time_t secs = static_cast(ms / 1000); + std::tm tm{}; + localtime_r(&secs, &tm); + char buf[64]; + std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); + return buf; +} + +std::vector parseDevices(const std::string &json) { + std::vector devices; + std::string err; + auto doc = json11::Json::parse(json, err); + if (err.empty() && doc.is_array()) { + for (const auto &device : doc.array_items()) { + devices.push_back({device["dongle_id"].string_value()}); + } + } + return devices; +} + +std::vector parseRoutes(const std::string &json, bool preserved) { + std::vector items; + std::string err; + auto doc = json11::Json::parse(json, err); + if (err.empty() && doc.is_array()) { + for (const auto &route : doc.array_items()) { + RouteInfo info; + info.name = route["fullname"].string_value(); + if (preserved) { + info.start_ms = parseIsoToUnixMs(route["start_time"].string_value()); + info.end_ms = parseIsoToUnixMs(route["end_time"].string_value()); + } else { + info.start_ms = static_cast(route["start_time_utc_millis"].number_value()); + info.end_ms = static_cast(route["end_time_utc_millis"].number_value()); + } + items.push_back(std::move(info)); + } + } + return items; +} + +void fetchDevices(DevicesCallback callback) { + std::thread([callback = std::move(callback)]() { + std::string result = PyDownloader::getDevices(); + auto [success, error_code] = checkApiResponse(result); + callback(success ? parseDevices(result) : std::vector{}, success, error_code); + }).detach(); +} + +void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback) { + const bool preserved = period_days == -1; + int64_t start_ms = 0, end_ms = 0; + if (!preserved) { + end_ms = nowUnixMs(); + start_ms = end_ms - static_cast(period_days) * 24LL * 60LL * 60LL * 1000LL; + } + + std::thread([dongle_id, start_ms, end_ms, preserved, callback = std::move(callback)]() { + std::string result = PyDownloader::getDeviceRoutes(dongle_id, start_ms, end_ms, preserved); + auto [success, error_code] = checkApiResponse(result); + callback(success ? parseRoutes(result, preserved) : std::vector{}, success, error_code); + }).detach(); +} + +} // namespace routes diff --git a/openpilot/tools/cabana/routes.h b/openpilot/tools/cabana/routes.h new file mode 100644 index 0000000000..ef6fc8dee0 --- /dev/null +++ b/openpilot/tools/cabana/routes.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace routes { + +struct DeviceInfo { + std::string dongle_id; +}; + +struct RouteInfo { + std::string name; + int64_t start_ms = 0; + int64_t end_ms = 0; +}; + +using DevicesCallback = std::function devices, bool success, int error_code)>; +using RoutesCallback = std::function routes, bool success, int error_code)>; + +// Parse a PyDownloader JSON response into (success, error_code). +std::pair checkApiResponse(const std::string &result); + +int64_t nowUnixMs(); +// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. +int64_t parseIsoToUnixMs(const std::string &s); +// Local time, "%Y-%m-%d %H:%M:%S". +std::string formatUnixMs(int64_t ms); + +std::vector parseDevices(const std::string &json); +// preserved routes report ISO-8601 timestamps instead of unix millis +std::vector parseRoutes(const std::string &json, bool preserved); + +// Both fetch on a detached thread and invoke the callback from that thread. +void fetchDevices(DevicesCallback callback); +// period_days of -1 requests preserved routes +void fetchRoutes(const std::string &dongle_id, int period_days, RoutesCallback callback); + +} // namespace routes diff --git a/openpilot/tools/cabana/routesdialog.cc b/openpilot/tools/cabana/routesdialog.cc index 9891104c9f..33b773edb9 100644 --- a/openpilot/tools/cabana/routesdialog.cc +++ b/openpilot/tools/cabana/routesdialog.cc @@ -1,9 +1,6 @@ #include "tools/cabana/routesdialog.h" -#include -#include #include -#include #include #include @@ -12,62 +9,7 @@ #include #include -#include "json11/json11.hpp" #include "tools/cabana/utils/util.h" -#include "tools/replay/py_downloader.h" - -namespace { - -// Parse a PyDownloader JSON response into (success, error_code). -std::pair checkApiResponse(const std::string &result) { - if (result.empty()) return {false, 500}; - std::string err; - auto doc = json11::Json::parse(result, err); - if (!err.empty()) return {false, 500}; - if (doc.is_object() && doc["error"].is_string()) { - return {false, doc["error"].string_value() == "unauthorized" ? 401 : 500}; - } - return {true, 0}; -} - -int64_t nowUnixMs() { - return std::chrono::duration_cast( - std::chrono::system_clock::now().time_since_epoch()) - .count(); -} - -// Parse ISO-8601 (with optional fractional seconds / Z) to unix ms. Returns 0 on failure. -int64_t parseIsoToUnixMs(const std::string &s) { - std::string bytes = s; - if (!bytes.empty() && (bytes.back() == 'Z' || bytes.back() == 'z')) bytes.pop_back(); - int millis = 0; - auto dot = bytes.find('.'); - if (dot != std::string::npos) { - std::string frac = bytes.substr(dot + 1); - bytes = bytes.substr(0, dot); - while (frac.size() < 3) frac.push_back('0'); - millis = std::atoi(frac.substr(0, 3).c_str()); - } - std::tm tm{}; - const char *ret = strptime(bytes.c_str(), "%Y-%m-%dT%H:%M:%S", &tm); - if (!ret) ret = strptime(bytes.c_str(), "%Y-%m-%d %H:%M:%S", &tm); - if (!ret) return 0; - tm.tm_isdst = -1; - time_t secs = timegm(&tm); - if (secs == static_cast(-1)) return 0; - return static_cast(secs) * 1000 + millis; -} - -std::string formatUnixMs(int64_t ms) { - time_t secs = static_cast(ms / 1000); - std::tm tm{}; - localtime_r(&secs, &tm); - char buf[64]; - std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm); - return buf; -} - -} // namespace // The RouteListWidget class extends QListWidget to display a custom message when empty class RouteListWidget : public QListWidget { @@ -110,26 +52,19 @@ RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) { connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject); - // Fetch devices - std::thread([this, alive = std::weak_ptr(alive_)]() { - std::string result = PyDownloader::getDevices(); - auto response = checkApiResponse(result); - utils::runOnMainThread([this, alive, r = std::move(result), response]() { - if (!alive.expired()) parseDeviceList(r, response.first, response.second); + routes::fetchDevices([this, alive = std::weak_ptr(alive_)](std::vector devices, bool success, int error_code) { + utils::runOnMainThread([this, alive, devices = std::move(devices), success, error_code]() { + if (!alive.expired()) setDeviceList(devices, success, error_code); }); - }).detach(); + }); } -void RoutesDialog::parseDeviceList(const std::string &json, bool success, int error_code) { +void RoutesDialog::setDeviceList(const std::vector &devices, bool success, int error_code) { if (success) { device_list_->clear(); - std::string err; - auto doc = json11::Json::parse(json, err); - if (err.empty() && doc.is_array()) { - for (const auto &device : doc.array_items()) { - QString dongle_id = QString::fromStdString(device["dongle_id"].string_value()); - device_list_->addItem(dongle_id, dongle_id); - } + for (const auto &device : devices) { + QString dongle_id = QString::fromStdString(device.dongle_id); + device_list_->addItem(dongle_id, dongle_id); } } else { QMessageBox::warning(this, tr("Error"), error_code == 401 ? tr("Unauthorized. Authenticate with openpilot/tools/lib/auth.py") : tr("Network error")); @@ -144,45 +79,22 @@ void RoutesDialog::fetchRoutes() { route_list_->clear(); route_list_->setEmptyText(tr("Loading...")); - std::string did = device_list_->currentText().toStdString(); - int period = period_selector_->currentData().toInt(); - - bool preserved = (period == -1); - int64_t start_ms = 0, end_ms = 0; - if (!preserved) { - end_ms = nowUnixMs(); - start_ms = end_ms - static_cast(period) * 24LL * 60LL * 60LL * 1000LL; - } - int request_id = ++fetch_id_; - std::thread([this, alive = std::weak_ptr(alive_), did, start_ms, end_ms, preserved, request_id]() { - std::string result = PyDownloader::getDeviceRoutes(did, start_ms, end_ms, preserved); - auto response = checkApiResponse(result); - utils::runOnMainThread([this, alive, r = std::move(result), response, request_id]() { - if (!alive.expired() && fetch_id_ == request_id) parseRouteList(r, response.first, response.second); + auto on_routes = [this, alive = std::weak_ptr(alive_), request_id](std::vector list, bool success, int) { + utils::runOnMainThread([this, alive, list = std::move(list), success, request_id]() { + if (!alive.expired() && fetch_id_ == request_id) setRouteList(list, success); }); - }).detach(); + }; + routes::fetchRoutes(device_list_->currentText().toStdString(), period_selector_->currentData().toInt(), std::move(on_routes)); } -void RoutesDialog::parseRouteList(const std::string &json, bool success, int error_code) { +void RoutesDialog::setRouteList(const std::vector &list, bool success) { if (success) { - std::string err; - auto doc = json11::Json::parse(json, err); - if (err.empty() && doc.is_array()) { - for (const auto &route : doc.array_items()) { - int64_t from_ms = 0, to_ms = 0; - if (period_selector_->currentData().toInt() == -1) { - from_ms = parseIsoToUnixMs(route["start_time"].string_value()); - to_ms = parseIsoToUnixMs(route["end_time"].string_value()); - } else { - from_ms = static_cast(route["start_time_utc_millis"].number_value()); - to_ms = static_cast(route["end_time_utc_millis"].number_value()); - } - const int mins = static_cast((to_ms - from_ms) / 60000); - auto item = new QListWidgetItem(QString::fromStdString(formatUnixMs(from_ms) + " " + std::to_string(mins) + "min")); - item->setData(Qt::UserRole, QString::fromStdString(route["fullname"].string_value())); - route_list_->addItem(item); - } + for (const auto &route : list) { + const int mins = static_cast((route.end_ms - route.start_ms) / 60000); + auto item = new QListWidgetItem(QString::fromStdString(routes::formatUnixMs(route.start_ms) + " " + std::to_string(mins) + "min")); + item->setData(Qt::UserRole, QString::fromStdString(route.name)); + route_list_->addItem(item); } if (route_list_->count() > 0) route_list_->setCurrentRow(0); } else { diff --git a/openpilot/tools/cabana/routesdialog.h b/openpilot/tools/cabana/routesdialog.h index 44d7f068f9..4983f1b558 100644 --- a/openpilot/tools/cabana/routesdialog.h +++ b/openpilot/tools/cabana/routesdialog.h @@ -2,10 +2,13 @@ #include #include +#include #include #include +#include "tools/cabana/routes.h" + class RouteListWidget; class RoutesDialog : public QDialog { @@ -15,8 +18,8 @@ public: std::string route(); protected: - void parseDeviceList(const std::string &json, bool success, int error_code); - void parseRouteList(const std::string &json, bool success, int error_code); + void setDeviceList(const std::vector &devices, bool success, int error_code); + void setRouteList(const std::vector &list, bool success); void fetchRoutes(); QComboBox *device_list_; diff --git a/openpilot/tools/cabana/tests/test_cabana.cc b/openpilot/tools/cabana/tests/test_cabana.cc index e3b8377e02..5c5fce9864 100644 --- a/openpilot/tools/cabana/tests/test_cabana.cc +++ b/openpilot/tools/cabana/tests/test_cabana.cc @@ -7,6 +7,7 @@ #include "common/tests/native_test.h" #include "tools/cabana/dbc/dbcfile.h" #include "tools/cabana/dbc/dbcmanager.h" +#include "tools/cabana/routes.h" #include "tools/cabana/utils/strings.h" const std::string TEST_RLOG_URL = "https://commadataci.blob.core.windows.net/openpilotci/0c94aa1e1296d7c6/2021-05-05--19-48-37/0/rlog.bz2"; @@ -240,6 +241,64 @@ void test_signal_tooltip() { )"); } +void test_route_timestamps() { + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05Z") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02 03:04:05") == 1704164645000); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123Z") == 1704164645123); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.4Z") == 1704164645400); + REQUIRE(routes::parseIsoToUnixMs("2024-01-02T03:04:05.123456Z") == 1704164645123); + REQUIRE(routes::parseIsoToUnixMs("") == 0); + REQUIRE(routes::parseIsoToUnixMs("not a timestamp") == 0); + + // formatUnixMs is local time + const char *tz = getenv("TZ"); + const std::string prev_tz = tz ? tz : ""; + setenv("TZ", "UTC", 1); + tzset(); + REQUIRE(routes::formatUnixMs(1704164645123) == "2024-01-02 03:04:05"); + if (tz) { + setenv("TZ", prev_tz.c_str(), 1); + } else { + unsetenv("TZ"); + } + tzset(); +} + +void test_route_api_response() { + REQUIRE(routes::checkApiResponse("") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse("not json") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse(R"({"error": "unauthorized"})") == std::make_pair(false, 401)); + REQUIRE(routes::checkApiResponse(R"({"error": "server error"})") == std::make_pair(false, 500)); + REQUIRE(routes::checkApiResponse("[]") == std::make_pair(true, 0)); + REQUIRE(routes::checkApiResponse(R"({"dongle_id": "aaaa"})") == std::make_pair(true, 0)); +} + +void test_route_json() { + auto devices = routes::parseDevices(R"([{"dongle_id": "aaaa"}, {"dongle_id": "bbbb"}])"); + REQUIRE(devices.size() == 2); + REQUIRE(devices[0].dongle_id == "aaaa"); + REQUIRE(devices[1].dongle_id == "bbbb"); + REQUIRE(routes::parseDevices("not json").empty()); + REQUIRE(routes::parseDevices(R"({"error": "unauthorized"})").empty()); + + auto list = routes::parseRoutes( + R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time_utc_millis": 1704164645000, "end_time_utc_millis": 1704165245000}])", false); + REQUIRE(list.size() == 1); + REQUIRE(list[0].name == "aaaa|2024-01-02--03-04-05"); + REQUIRE(list[0].start_ms == 1704164645000); + REQUIRE(list[0].end_ms == 1704165245000); + + // preserved routes report ISO-8601 timestamps + auto preserved = routes::parseRoutes( + R"([{"fullname": "aaaa|2024-01-02--03-04-05", "start_time": "2024-01-02T03:04:05Z", "end_time": "2024-01-02T03:14:05Z"}])", true); + REQUIRE(preserved.size() == 1); + REQUIRE(preserved[0].start_ms == 1704164645000); + REQUIRE(preserved[0].end_ms == 1704165245000); + + REQUIRE(routes::parseRoutes("not json", false).empty()); +} + void test_cabana_core() { test_format_seconds(); test_to_hex(); @@ -251,6 +310,9 @@ void test_cabana_core() { test_parse_dbc(); test_parse_opendbc(); test_dbc_manager(); + test_route_timestamps(); + test_route_api_response(); + test_route_json(); } int main() { From 5419f57b3a63f581dde6b0485624f0a2f698d878 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:18:10 -0700 Subject: [PATCH 18/53] cabana: de-QT streams (#38718) --- openpilot/tools/cabana/cabana.cc | 3 ++- openpilot/tools/cabana/mainwin.cc | 3 +++ .../tools/cabana/streams/abstractstream.h | 1 + .../tools/cabana/streams/devicestream.cc | 26 +++++++------------ openpilot/tools/cabana/streams/devicestream.h | 7 ++--- .../tools/cabana/streams/replaystream.cc | 18 ++++++------- openpilot/tools/cabana/streamselector.cc | 5 +++- openpilot/tools/cabana/utils/util.cc | 22 ++++++++++++---- openpilot/tools/cabana/utils/util.h | 1 + 9 files changed, 50 insertions(+), 36 deletions(-) diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index e3a1850346..6cc6046bec 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -148,7 +148,7 @@ int main(int argc, char *argv[]) { if (args.msgq) { stream = new DeviceStream(); } else if (!args.zmq.empty()) { - stream = new DeviceStream(QString::fromStdString(args.zmq)); + stream = new DeviceStream(args.zmq); } else if (args.panda || !args.panda_serial.empty()) { try { stream = new PandaStream({.serial = args.panda_serial}); @@ -175,6 +175,7 @@ int main(int argc, char *argv[]) { } if (!route.isEmpty()) { auto replay_stream = std::make_unique(); + Connection err = replay_stream->error.connect([](const std::string &msg) { fprintf(stderr, "%s\n", msg.c_str()); }); if (!replay_stream->loadRoute(route.toStdString(), args.data_dir, replay_flags, args.auto_source)) { return 0; } diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 3ae5896a2d..939aade269 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -358,6 +358,9 @@ void MainWindow::startStream(AbstractStream *stream, QString dbc_file) { delete video_splitter; can = stream; // take ownership + stream_connections_.push_back(can->error.connect([this](const std::string &msg) { + QMessageBox::warning(this, tr("Error"), QString::fromStdString(msg)); + })); can->start(); loadFile(dbc_file); diff --git a/openpilot/tools/cabana/streams/abstractstream.h b/openpilot/tools/cabana/streams/abstractstream.h index 4f7c0dc3e7..b992b086fd 100644 --- a/openpilot/tools/cabana/streams/abstractstream.h +++ b/openpilot/tools/cabana/streams/abstractstream.h @@ -64,6 +64,7 @@ public: Observable> &> timeRangeChanged; Observable eventsMerged; Observable *, bool> msgsReceived; + Observable error; SourceSet sources; diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 6d580cc912..3a338c7f59 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -9,17 +9,16 @@ #include #include #include +#include #include #include #include "openpilot/cereal/services.h" - -#include -#include +#include "tools/cabana/utils/util.h" // DeviceStream -DeviceStream::DeviceStream(QString address) : zmq_address(address) { +DeviceStream::DeviceStream(std::string address) : zmq_address(std::move(address)) { } DeviceStream::~DeviceStream() { @@ -46,19 +45,16 @@ void DeviceStream::stopBridge() { } void DeviceStream::start() { - if (!zmq_address.isEmpty()) { + if (!zmq_address.empty()) { stopBridge(); - const std::string path = (std::filesystem::path(QCoreApplication::applicationDirPath().toStdString()) / - "../../cereal/messaging/bridge").lexically_normal().string(); - const std::string addr = zmq_address.toStdString(); + const std::string path = (executableDir() / "../../cereal/messaging/bridge").lexically_normal().string(); const char *can_filter = "/\"can/\""; // Self-pipe: write end is CLOEXEC so it closes on successful exec. If exec // fails, the child writes errno and the parent aborts stream start. int err_pipe[2] = {-1, -1}; if (::pipe(err_pipe) != 0) { - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + error(std::string("Failed to start bridge: ") + strerror(errno)); return; } @@ -66,7 +62,7 @@ void DeviceStream::start() { if (pid == 0) { ::close(err_pipe[0]); ::fcntl(err_pipe[1], F_SETFD, FD_CLOEXEC); - execl(path.c_str(), path.c_str(), addr.c_str(), can_filter, static_cast(nullptr)); + execl(path.c_str(), path.c_str(), zmq_address.c_str(), can_filter, static_cast(nullptr)); const int err = errno; (void)!::write(err_pipe[1], &err, sizeof(err)); _exit(127); @@ -75,8 +71,7 @@ void DeviceStream::start() { ::close(err_pipe[1]); if (pid < 0) { ::close(err_pipe[0]); - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(errno)))); + error(std::string("Failed to start bridge: ") + strerror(errno)); return; } @@ -87,8 +82,7 @@ void DeviceStream::start() { // Child failed to exec; reap and surface the error. int status = 0; ::waitpid(pid, &status, 0); - QMessageBox::warning(nullptr, "Error", - QString("Failed to start bridge: %1").arg(QString::fromLocal8Bit(strerror(exec_errno)))); + error(std::string("Failed to start bridge: ") + strerror(exec_errno)); return; } @@ -99,7 +93,7 @@ void DeviceStream::start() { } void DeviceStream::streamThread() { - zmq_address.isEmpty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); + zmq_address.empty() ? unsetenv("ZMQ") : setenv("ZMQ", "1", 1); std::unique_ptr context(Context::create()); std::unique_ptr sock(SubSocket::create(context.get(), "can", "127.0.0.1", false, true, services.at("can").queue_size)); diff --git a/openpilot/tools/cabana/streams/devicestream.h b/openpilot/tools/cabana/streams/devicestream.h index 71205e5a80..3770d952aa 100644 --- a/openpilot/tools/cabana/streams/devicestream.h +++ b/openpilot/tools/cabana/streams/devicestream.h @@ -2,14 +2,15 @@ #include "tools/cabana/streams/livestream.h" +#include #include class DeviceStream : public LiveStream { public: - DeviceStream(QString address = {}); + DeviceStream(std::string address = {}); ~DeviceStream(); inline std::string routeName() const override { - return "Live Streaming From " + (zmq_address.isEmpty() ? std::string("127.0.0.1") : zmq_address.toStdString()); + return "Live Streaming From " + (zmq_address.empty() ? std::string("127.0.0.1") : zmq_address); } protected: @@ -17,5 +18,5 @@ protected: void streamThread() override; void stopBridge(); pid_t bridge_pid = -1; - const QString zmq_address; + const std::string zmq_address; }; diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index e9df400599..81ed34e8db 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -1,6 +1,6 @@ #include "tools/cabana/streams/replaystream.h" -#include +#include #include "common/timing.h" #include "common/util.h" @@ -59,27 +59,25 @@ bool ReplayStream::loadRoute(const std::string &route, const std::string &data_d bool success = replay->load(); if (!success) { + std::string message; if (replay->lastRouteError() == RouteLoadError::Unauthorized) { auto auth_content = util::read_file(util::getenv("HOME") + "/.comma/auth.json"); - QString message; if (auth_content.empty()) { message = "Authentication Required. Please run the following command to authenticate:\n\n" "python3 openpilot/tools/lib/auth.py\n\n" "This will grant access to routes from your comma account."; } else { - message = QString("Access Denied. You do not have permission to access route:\n\n%1\n\n" - "This is likely a private route.").arg(QString::fromStdString(route)); + message = "Access Denied. You do not have permission to access route:\n\n" + route + "\n\n" + "This is likely a private route."; } - QMessageBox::warning(nullptr, "Access Denied", message); } else if (replay->lastRouteError() == RouteLoadError::NetworkError) { - QMessageBox::warning(nullptr, "Network Error", - QString("Unable to load the route:\n\n %1.\n\nPlease check your network connection and try again.").arg(QString::fromStdString(route))); + message = "Unable to load the route:\n\n " + route + ".\n\nPlease check your network connection and try again."; } else if (replay->lastRouteError() == RouteLoadError::FileNotFound) { - QMessageBox::warning(nullptr, "Route Not Found", - QString("The specified route could not be found:\n\n %1.\n\nPlease check the route name and try again.").arg(QString::fromStdString(route))); + message = "The specified route could not be found:\n\n " + route + ".\n\nPlease check the route name and try again."; } else { - QMessageBox::warning(nullptr, "Route Load Failed", QString("Failed to load route: '%1'").arg(QString::fromStdString(route))); + message = "Failed to load route: '" + route + "'"; } + error(message); } return success; } diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index dae022be75..8c95bf584b 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -63,6 +63,9 @@ AbstractStream *OpenReplayWidget::open() { QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); } else { auto replay_stream = std::make_unique(); + Connection err = replay_stream->error.connect([](const std::string &msg) { + QMessageBox::warning(nullptr, tr("Error"), QString::fromStdString(msg)); + }); uint32_t flags = REPLAY_FLAG_NONE; if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_CABIN_CAMERA; if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_WIDE_ROAD; @@ -211,7 +214,7 @@ OpenDeviceWidget::OpenDeviceWidget(QWidget *parent) : AbstractOpenStreamWidget(p } AbstractStream *OpenDeviceWidget::open() { - QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + std::string ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text().toStdString(); bool msgq = group->checkedId() == 0; return new DeviceStream(msgq ? "" : ip); } diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index 9a44ef5054..d5cd77a8c8 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -16,6 +16,9 @@ #include #include #include +#ifdef __APPLE__ +#include +#endif #include #include @@ -454,27 +457,36 @@ void sigTermHandler(int s) { qApp->quit(); } +std::filesystem::path executableDir() { +#ifdef __APPLE__ + char buf[PATH_MAX]; + uint32_t size = sizeof(buf); + if (_NSGetExecutablePath(buf, &size) != 0) return {}; + std::error_code ec; + auto path = std::filesystem::canonical(buf, ec); + return (ec ? std::filesystem::path(buf) : path).parent_path(); +#else + return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); +#endif +} + void initApp(int argc, char *argv[], bool disable_hidpi) { // setup signal handlers to exit gracefully std::signal(SIGINT, sigTermHandler); std::signal(SIGTERM, sigTermHandler); - std::filesystem::path app_dir; #ifdef __APPLE__ // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering QApplication tmp(argc, argv); - app_dir = QCoreApplication::applicationDirPath().toStdString(); if (disable_hidpi) { qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); } -#else - app_dir = std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif qputenv("QT_DBL_CLICK_DIST", "150"); // ensure the current dir matches the exectuable's directory std::error_code ec; - std::filesystem::current_path(app_dir, ec); + std::filesystem::current_path(executableDir(), ec); } // embedded at build time from the bootstrap_icons package (see SConscript) diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index 77a634fc9e..e0d13208a6 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -219,5 +219,6 @@ private: }; int num_decimals(double num); +std::filesystem::path executableDir(); void initApp(int argc, char *argv[], bool disable_hidpi = true); QPixmap bootstrapPixmap(const QString &id); From 633d17cd128690e00b5a24dfd6747141c8d34a2e Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:30:13 -0700 Subject: [PATCH 19/53] ui: fix install update button overflow (#38696) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 0f12004828..32fbc2b3a7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -184,7 +184,7 @@ class CheckUpdateButton(BigButton): class InstallUpdateButton(BigButton): def __init__(self): - super().__init__("install update", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) + super().__init__("install now", "", gui_app.texture("icons_mici/settings/device/reboot.png", 64, 70)) self.set_visible(lambda: ui_state.is_offroad() and ui_state.params.get_bool("UpdateAvailable")) def _update_state(self): From 5645370f845ce70449fdf87ebfa544a2fbc36101 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:37:09 -0700 Subject: [PATCH 20/53] cabana: split utils/util into Qt-free util and qtutil (#38723) --- openpilot/tools/cabana/SConscript | 4 +- openpilot/tools/cabana/binaryview.cc | 1 + openpilot/tools/cabana/binaryview.h | 1 + openpilot/tools/cabana/cabana.cc | 10 +- openpilot/tools/cabana/chart/chart.cc | 2 +- openpilot/tools/cabana/chart/chart.h | 1 + openpilot/tools/cabana/chart/chartswidget.h | 1 + .../tools/cabana/chart/signalselector.cc | 1 + openpilot/tools/cabana/chart/sparkline.cc | 1 + openpilot/tools/cabana/chart/tiplabel.cc | 2 +- openpilot/tools/cabana/commands.cc | 1 + openpilot/tools/cabana/detailwidget.h | 2 + openpilot/tools/cabana/historylog.h | 1 + openpilot/tools/cabana/mainwin.cc | 1 + openpilot/tools/cabana/messageswidget.h | 2 + openpilot/tools/cabana/settingsdialog.cc | 2 +- openpilot/tools/cabana/signalview.cc | 3 +- .../tools/cabana/streams/abstractstream.cc | 1 + .../tools/cabana/streams/devicestream.cc | 1 + openpilot/tools/cabana/streams/livestream.cc | 1 + .../tools/cabana/streams/replaystream.cc | 1 + openpilot/tools/cabana/streamselector.cc | 1 + openpilot/tools/cabana/tools/findsignal.cc | 2 + openpilot/tools/cabana/tools/findsignal.h | 5 + openpilot/tools/cabana/utils/qtutil.cc | 236 ++++++++++ openpilot/tools/cabana/utils/qtutil.h | 139 ++++++ openpilot/tools/cabana/utils/util.cc | 410 +++++------------- openpilot/tools/cabana/utils/util.h | 212 +++------ openpilot/tools/cabana/videowidget.cc | 1 + openpilot/tools/cabana/videowidget.h | 2 +- 30 files changed, 566 insertions(+), 482 deletions(-) create mode 100644 openpilot/tools/cabana/utils/qtutil.cc create mode 100644 openpilot/tools/cabana/utils/qtutil.h diff --git a/openpilot/tools/cabana/SConscript b/openpilot/tools/cabana/SConscript index f7db3f23b7..63b725a759 100644 --- a/openpilot/tools/cabana/SConscript +++ b/openpilot/tools/cabana/SConscript @@ -100,7 +100,7 @@ cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, "assets/assets.o"] cabana_srcs = ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'routesdialog.cc', 'routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', - 'utils/export.cc', 'utils/util.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', + 'utils/export.cc', 'utils/util.cc', 'utils/qtutil.cc', 'utils/strings.cc', 'utils/elidedlabel.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'settingsdialog.cc', 'panda.cc', 'cameraview.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc', 'tools/routeinfo.cc'] @@ -120,6 +120,8 @@ if GetOption('extras'): dbc_core_test_env.Object('tests/dbc_core_file', 'dbc/dbcfile.cc'), dbc_core_test_env.Object('tests/dbc_core_manager', 'dbc/dbcmanager.cc'), dbc_core_test_env.Object('tests/dbc_core_strings', 'utils/strings.cc'), + dbc_core_test_env.Object('tests/dbc_core_util', 'utils/util.cc'), + dbc_core_test_env.Object('tests/dbc_core_icons', bootstrap_icons_src), dbc_core_test_env.Object('tests/dbc_core_routes', 'routes.cc'), ] dbc_core_test_env.Program('tests/test_dbc_core', dbc_core_test_objects, LIBS=[replay_lib, common]) diff --git a/openpilot/tools/cabana/binaryview.cc b/openpilot/tools/cabana/binaryview.cc index 160aead6fd..20fd2ec1ec 100644 --- a/openpilot/tools/cabana/binaryview.cc +++ b/openpilot/tools/cabana/binaryview.cc @@ -13,6 +13,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/utils/qtutil.h" // BinaryView diff --git a/openpilot/tools/cabana/binaryview.h b/openpilot/tools/cabana/binaryview.h index 0b0159b422..c918ee01e7 100644 --- a/openpilot/tools/cabana/binaryview.h +++ b/openpilot/tools/cabana/binaryview.h @@ -4,6 +4,7 @@ #include #include +#include #include #include diff --git a/openpilot/tools/cabana/cabana.cc b/openpilot/tools/cabana/cabana.cc index 6cc6046bec..b18484c556 100644 --- a/openpilot/tools/cabana/cabana.cc +++ b/openpilot/tools/cabana/cabana.cc @@ -12,6 +12,7 @@ #ifdef __linux__ #include "tools/cabana/streams/socketcanstream.h" #endif +#include "tools/cabana/utils/qtutil.h" namespace { @@ -134,7 +135,14 @@ int main(int argc, char *argv[]) { app.setApplicationDisplayName("Cabana"); //app.setWindowIcon(QIcon(":cabana-icon.png")); // TODO: do this in imgui - UnixSignalHandler signalHandler; + // Marshal exit onto the GUI thread (qApp methods are not thread-safe). + UnixSignalHandler signalHandler([]() { + QMetaObject::invokeMethod(qApp, []() { + printf("\nexiting...\n"); + qApp->closeAllWindows(); + qApp->exit(); + }, Qt::QueuedConnection); + }); utils::setTheme(settings.theme); CabanaArgs args; diff --git a/openpilot/tools/cabana/chart/chart.cc b/openpilot/tools/cabana/chart/chart.cc index beece12352..478ff3ce9d 100644 --- a/openpilot/tools/cabana/chart/chart.cc +++ b/openpilot/tools/cabana/chart/chart.cc @@ -248,7 +248,7 @@ void ChartView::updateSeries(const cabana::Signal *sig, const MessageEventsMap * } if (!can->liveStreaming()) { - s.segment_tree.build(s.vals); + s.segment_tree.build(s.vals.size(), [&vals = s.vals](int i) { return vals[i].y(); }); } } } diff --git a/openpilot/tools/cabana/chart/chart.h b/openpilot/tools/cabana/chart/chart.h index d7227b6cf8..3188e1f401 100644 --- a/openpilot/tools/cabana/chart/chart.h +++ b/openpilot/tools/cabana/chart/chart.h @@ -10,6 +10,7 @@ #include "tools/cabana/chart/tiplabel.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" enum class SeriesType { Line = 0, diff --git a/openpilot/tools/cabana/chart/chartswidget.h b/openpilot/tools/cabana/chart/chartswidget.h index 4c143242b2..17e18a90d2 100644 --- a/openpilot/tools/cabana/chart/chartswidget.h +++ b/openpilot/tools/cabana/chart/chartswidget.h @@ -13,6 +13,7 @@ #include "tools/cabana/commands.h" #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" const int CHART_MIN_WIDTH = 300; diff --git a/openpilot/tools/cabana/chart/signalselector.cc b/openpilot/tools/cabana/chart/signalselector.cc index aab38f5372..90825dc402 100644 --- a/openpilot/tools/cabana/chart/signalselector.cc +++ b/openpilot/tools/cabana/chart/signalselector.cc @@ -8,6 +8,7 @@ #include #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" SignalSelector::SignalSelector(QString title, QWidget *parent) : QDialog(parent) { setWindowTitle(title); diff --git a/openpilot/tools/cabana/chart/sparkline.cc b/openpilot/tools/cabana/chart/sparkline.cc index f5bef0fc2e..587a35956d 100644 --- a/openpilot/tools/cabana/chart/sparkline.cc +++ b/openpilot/tools/cabana/chart/sparkline.cc @@ -3,6 +3,7 @@ #include #include #include +#include "tools/cabana/utils/qtutil.h" void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { if (first == last || size.isEmpty()) { diff --git a/openpilot/tools/cabana/chart/tiplabel.cc b/openpilot/tools/cabana/chart/tiplabel.cc index 233fa80373..c250ebf877 100644 --- a/openpilot/tools/cabana/chart/tiplabel.cc +++ b/openpilot/tools/cabana/chart/tiplabel.cc @@ -7,7 +7,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_ShowWithoutActivating); diff --git a/openpilot/tools/cabana/commands.cc b/openpilot/tools/cabana/commands.cc index c6cdd9b1b7..40d1c5ba18 100644 --- a/openpilot/tools/cabana/commands.cc +++ b/openpilot/tools/cabana/commands.cc @@ -1,5 +1,6 @@ #include "tools/cabana/commands.h" +#include #include // UndoStack diff --git a/openpilot/tools/cabana/detailwidget.h b/openpilot/tools/cabana/detailwidget.h index 48d320c40f..c003548da4 100644 --- a/openpilot/tools/cabana/detailwidget.h +++ b/openpilot/tools/cabana/detailwidget.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -11,6 +12,7 @@ #include "tools/cabana/historylog.h" #include "tools/cabana/signalview.h" #include "tools/cabana/utils/elidedlabel.h" +#include "tools/cabana/utils/qtutil.h" class EditMessageDialog : public QDialog { public: diff --git a/openpilot/tools/cabana/historylog.h b/openpilot/tools/cabana/historylog.h index 405fe60fbb..1b75e3b8a9 100644 --- a/openpilot/tools/cabana/historylog.h +++ b/openpilot/tools/cabana/historylog.h @@ -10,6 +10,7 @@ #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class HeaderView : public QHeaderView { public: diff --git a/openpilot/tools/cabana/mainwin.cc b/openpilot/tools/cabana/mainwin.cc index 939aade269..2c19df530c 100644 --- a/openpilot/tools/cabana/mainwin.cc +++ b/openpilot/tools/cabana/mainwin.cc @@ -23,6 +23,7 @@ #include "tools/cabana/streamselector.h" #include "tools/cabana/tools/findsignal.h" #include "tools/cabana/utils/export.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/py_downloader.h" #include "tools/replay/util.h" diff --git a/openpilot/tools/cabana/messageswidget.h b/openpilot/tools/cabana/messageswidget.h index 28cab25463..7c9fc0253f 100644 --- a/openpilot/tools/cabana/messageswidget.h +++ b/openpilot/tools/cabana/messageswidget.h @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include "tools/cabana/dbc/dbcmanager.h" #include "tools/cabana/streams/abstractstream.h" +#include "tools/cabana/utils/qtutil.h" class MessageListModel : public QAbstractTableModel { Q_OBJECT diff --git a/openpilot/tools/cabana/settingsdialog.cc b/openpilot/tools/cabana/settingsdialog.cc index 3a49de5746..5dd8b7617c 100644 --- a/openpilot/tools/cabana/settingsdialog.cc +++ b/openpilot/tools/cabana/settingsdialog.cc @@ -9,7 +9,7 @@ #include #include "tools/cabana/settings.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_CACHE_MINIUTES = 30; const int MAX_CACHE_MINIUTES = 120; diff --git a/openpilot/tools/cabana/signalview.cc b/openpilot/tools/cabana/signalview.cc index 3a6d4dab25..de80ce2a3f 100644 --- a/openpilot/tools/cabana/signalview.cc +++ b/openpilot/tools/cabana/signalview.cc @@ -11,10 +11,11 @@ #include #include #include +#include #include #include "tools/cabana/commands.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" // SignalModel diff --git a/openpilot/tools/cabana/streams/abstractstream.cc b/openpilot/tools/cabana/streams/abstractstream.cc index cc8b2f96ad..7be579269d 100644 --- a/openpilot/tools/cabana/streams/abstractstream.cc +++ b/openpilot/tools/cabana/streams/abstractstream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/abstractstream.h" +#include #include #include diff --git a/openpilot/tools/cabana/streams/devicestream.cc b/openpilot/tools/cabana/streams/devicestream.cc index 3a338c7f59..986ca13558 100644 --- a/openpilot/tools/cabana/streams/devicestream.cc +++ b/openpilot/tools/cabana/streams/devicestream.cc @@ -1,5 +1,6 @@ #include "tools/cabana/streams/devicestream.h" +#include #include #include #include diff --git a/openpilot/tools/cabana/streams/livestream.cc b/openpilot/tools/cabana/streams/livestream.cc index 71b5d9a1ec..ec71a6794d 100644 --- a/openpilot/tools/cabana/streams/livestream.cc +++ b/openpilot/tools/cabana/streams/livestream.cc @@ -9,6 +9,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" struct LiveStream::Logger { Logger() : start_ts(seconds_since_epoch()), segment_num(-1) {} diff --git a/openpilot/tools/cabana/streams/replaystream.cc b/openpilot/tools/cabana/streams/replaystream.cc index 81ed34e8db..944cb10fd9 100644 --- a/openpilot/tools/cabana/streams/replaystream.cc +++ b/openpilot/tools/cabana/streams/replaystream.cc @@ -4,6 +4,7 @@ #include "common/timing.h" #include "common/util.h" +#include "tools/cabana/settings.h" ReplayStream::ReplayStream() { unsetenv("ZMQ"); diff --git a/openpilot/tools/cabana/streamselector.cc b/openpilot/tools/cabana/streamselector.cc index 8c95bf584b..31532737f3 100644 --- a/openpilot/tools/cabana/streamselector.cc +++ b/openpilot/tools/cabana/streamselector.cc @@ -14,6 +14,7 @@ #include "tools/cabana/streams/devicestream.h" #include "tools/cabana/streams/replaystream.h" #include "tools/cabana/routesdialog.h" +#include "tools/cabana/utils/qtutil.h" // OpenReplayWidget diff --git a/openpilot/tools/cabana/tools/findsignal.cc b/openpilot/tools/cabana/tools/findsignal.cc index 4511af7c01..2e533741cc 100644 --- a/openpilot/tools/cabana/tools/findsignal.cc +++ b/openpilot/tools/cabana/tools/findsignal.cc @@ -10,6 +10,8 @@ #include #include +#include "tools/cabana/utils/qtutil.h" + // FindSignalModel QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/openpilot/tools/cabana/tools/findsignal.h b/openpilot/tools/cabana/tools/findsignal.h index 239a08c9c4..b7cae73b49 100644 --- a/openpilot/tools/cabana/tools/findsignal.h +++ b/openpilot/tools/cabana/tools/findsignal.h @@ -7,8 +7,13 @@ #include #include +#include +#include +#include #include +#include #include +#include #include #include "tools/cabana/commands.h" diff --git a/openpilot/tools/cabana/utils/qtutil.cc b/openpilot/tools/cabana/utils/qtutil.cc new file mode 100644 index 0000000000..040c90c624 --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.cc @@ -0,0 +1,236 @@ +#include "tools/cabana/utils/qtutil.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// MessageBytesDelegate + +MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) + : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { + fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); + byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); + for (int i = 0; i < 256; ++i) { + hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); + hex_text_table[i].prepare({}, fixed_font); + } + h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; + v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; +} + +QSize MessageBytesDelegate::sizeForBytes(int n) const { + int rows = multiple_lines ? std::max(1, n / 8) : 1; + return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; +} + +QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { + auto data = index.data(BytesRole); + return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); +} + +void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); + } + + QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); + QColor highlighted_color = option.palette.color(QPalette::HighlightedText); + auto text_color = index.data(Qt::ForegroundRole).value(); + bool inactive = text_color.isValid(); + if (!inactive) { + text_color = option.palette.color(QPalette::Text); + } + auto data = index.data(BytesRole); + if (!data.isValid()) { + painter->setFont(option.font); + painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); + painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); + return; + } + + // Paint hex column + const auto &bytes = *static_cast *>(data.value()); + const auto &colors = *static_cast *>(index.data(ColorsRole).value()); + + painter->setFont(fixed_font); + const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); + const QPoint pt = item_rect.topLeft(); + for (int i = 0; i < bytes.size(); ++i) { + int row = !multiple_lines ? 0 : i / 8; + int column = !multiple_lines ? i : i % 8; + QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); + + if (!inactive && i < colors.size() && colors[i].alpha() > 0) { + if (option.state & QStyle::State_Selected) { + painter->setPen(option.palette.color(QPalette::Text)); + painter->fillRect(r, option.palette.color(QPalette::Window)); + } + painter->fillRect(r, toQColor(colors[i])); + } else { + painter->setPen(text_pen); + } + utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); + } +} + +// TabBar + +int TabBar::addTab(const QString &text) { + int index = QTabBar::addTab(text); + QToolButton *btn = new ToolButton("x", tr("Close Tab")); + int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); + int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); + btn->setFixedSize({width, height}); + setTabButton(index, QTabBar::RightSide, btn); + QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); + return index; +} + +void TabBar::closeTabClicked() { + QObject *object = sender(); + for (int i = 0; i < count(); ++i) { + if (tabButton(i, QTabBar::RightSide) == object) { + emit tabCloseRequested(i); + break; + } + } +} + +// validators + +static QValidator::State toQtState(ValidState s) { + switch (s) { + case ValidState::Acceptable: return QValidator::Acceptable; + case ValidState::Intermediate: return QValidator::Intermediate; + default: return QValidator::Invalid; + } +} + +QValidator::State NameValidator::validate(QString &input, int &pos) const { + std::string s = input.toStdString(); + auto state = validateName(s); + input = QString::fromStdString(s); + return toQtState(state); +} + +QValidator::State NodeValidator::validate(QString &input, int &pos) const { + return toQtState(validateNodes(input.toStdString())); +} + +QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { + return toQtState(validateNonWhitespace(input.toStdString())); +} + +QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { + return toQtState(validateIpAddress(input.toStdString())); +} + +QValidator::State DoubleValidator::validate(QString &input, int &pos) const { + return toQtState(validateDouble(input.toLatin1().toStdString())); +} + +namespace utils { + +bool isDarkTheme() { + QColor windowColor = QApplication::palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + +QPixmap icon(const QString &id) { + bool dark_theme = isDarkTheme(); + + QPixmap pm; + QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); + if (!QPixmapCache::find(key, &pm)) { + pm = bootstrapPixmap(id); + if (dark_theme) { + QPainter p(&pm); + p.setCompositionMode(QPainter::CompositionMode_SourceIn); + p.fillRect(pm.rect(), QColor("#bbbbbb")); + } + QPixmapCache::insert(key, pm); + } + return pm; +} + +void setTheme(int theme) { + auto style = QApplication::style(); + if (!style) return; + + static int prev_theme = 0; + if (theme != prev_theme) { + prev_theme = theme; + QPalette new_palette; + if (theme == DARK_THEME) { + new_palette.setColor(QPalette::Window, toQColor(DarkTheme::window)); + new_palette.setColor(QPalette::WindowText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Base, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::AlternateBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipBase, toQColor(DarkTheme::base)); + new_palette.setColor(QPalette::ToolTipText, toQColor(DarkTheme::tooltip_text)); + new_palette.setColor(QPalette::Text, toQColor(DarkTheme::text)); + new_palette.setColor(QPalette::Button, toQColor(DarkTheme::button)); + new_palette.setColor(QPalette::ButtonText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::Highlight, toQColor(DarkTheme::highlight)); + new_palette.setColor(QPalette::HighlightedText, toQColor(DarkTheme::window_text)); + new_palette.setColor(QPalette::BrightText, toQColor(DarkTheme::bright_text)); + new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::WindowText, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Disabled, QPalette::Text, toQColor(DarkTheme::disabled_text)); + new_palette.setColor(QPalette::Light, toQColor(DarkTheme::light)); + new_palette.setColor(QPalette::Dark, toQColor(DarkTheme::dark)); + } else { + new_palette = style->standardPalette(); + } + qApp->setPalette(new_palette); + style->polish(qApp); + for (auto w : QApplication::allWidgets()) { + w->setPalette(new_palette); + } + } +} + +} // namespace utils + +void sigTermHandler(int s) { + std::signal(s, SIG_DFL); + qApp->quit(); +} + +void initApp(int argc, char *argv[], bool disable_hidpi) { + // setup signal handlers to exit gracefully + std::signal(SIGINT, sigTermHandler); + std::signal(SIGTERM, sigTermHandler); + +#ifdef __APPLE__ + // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering + QApplication tmp(argc, argv); + if (disable_hidpi) { + qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); + } +#endif + + qputenv("QT_DBL_CLICK_DIST", "150"); + // ensure the current dir matches the exectuable's directory + std::error_code ec; + std::filesystem::current_path(executableDir(), ec); +} + +QPixmap bootstrapPixmap(const QString &id) { + QPixmap pixmap; + const std::string svg = utils::bootstrapSvg(id.toStdString()); + if (!svg.empty()) { + pixmap.loadFromData((const uchar *)svg.data(), svg.size(), "svg"); + } + return pixmap; +} diff --git a/openpilot/tools/cabana/utils/qtutil.h b/openpilot/tools/cabana/utils/qtutil.h new file mode 100644 index 0000000000..db0417ec9a --- /dev/null +++ b/openpilot/tools/cabana/utils/qtutil.h @@ -0,0 +1,139 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/core/observable.h" +#include "tools/cabana/dbc/dbc.h" +#include "tools/cabana/settings.h" +#include "tools/cabana/utils/strings.h" +#include "tools/cabana/utils/util.h" + +// needed by QVariant::fromValue() in the Qt views; goes away with QVariant +Q_DECLARE_METATYPE(MessageId) +Q_DECLARE_METATYPE(ValueDescription) + +inline QColor toQColor(const CabanaColor &color) { + return QColor(color.r, color.g, color.b, color.a); +} + +class LogSlider : public QSlider { + Q_OBJECT + +public: + LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : scale(factor), QSlider(orientation, parent) {} + + void setRange(double min, double max) { + scale.setRange(min, max); + QSlider::setRange(min, max); + setValue(QSlider::value()); + } + int value() const { return scale.value(QSlider::value(), minimum(), maximum()); } + void setValue(int v) { QSlider::setValue(scale.position(v, minimum(), maximum())); } + +private: + LogScale scale; +}; + +enum { + ColorsRole = Qt::UserRole + 1, + BytesRole = Qt::UserRole + 2 +}; + +class MessageBytesDelegate : public QStyledItemDelegate { + Q_OBJECT +public: + MessageBytesDelegate(QObject *parent, bool multiple_lines = false); + void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; + bool multipleLines() const { return multiple_lines; } + void setMultipleLines(bool v) { multiple_lines = v; } + QSize sizeForBytes(int n) const; + +private: + std::array hex_text_table; + QFontMetrics font_metrics; + QFont fixed_font; + QSize byte_size = {}; + bool multiple_lines = false; + int h_margin, v_margin; +}; + +// QValidator wrappers around the std::string validators in util.h +#define CABANA_VALIDATOR(Name) \ + class Name : public QValidator { \ + Q_OBJECT \ + public: \ + Name(QObject *parent = nullptr) : QValidator(parent) {} \ + QValidator::State validate(QString &input, int &pos) const override; \ + }; +CABANA_VALIDATOR(NameValidator) +CABANA_VALIDATOR(NodeValidator) +CABANA_VALIDATOR(NonWhitespaceValidator) +CABANA_VALIDATOR(IpAddressValidator) +CABANA_VALIDATOR(DoubleValidator) +#undef CABANA_VALIDATOR + +namespace utils { + +QPixmap icon(const QString &id); +bool isDarkTheme(); +void setTheme(int theme); +inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { + auto size = (r.size() - text.size()) / 2; + p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); +} +inline auto qbytes(const std::vector &dat) { + return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); +} + +} + +class ToolButton : public QToolButton { + Q_OBJECT +public: + ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { + setIcon(icon); + setToolTip(tooltip); + setAutoRaise(true); + const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); + setIconSize({metric, metric}); + theme = settings.theme; + settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); + } + void setIcon(const QString &icon) { + icon_str = icon; + QToolButton::setIcon(utils::icon(icon_str)); + } + +private: + void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } + Connection settings_connection_; + QString icon_str; + int theme; +}; + +class TabBar : public QTabBar { + Q_OBJECT + +public: + TabBar(QWidget *parent) : QTabBar(parent) {} + int addTab(const QString &text); + +private: + void closeTabClicked(); +}; + +void initApp(int argc, char *argv[], bool disable_hidpi = true); +QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/utils/util.cc b/openpilot/tools/cabana/utils/util.cc index d5cd77a8c8..7bdf225975 100644 --- a/openpilot/tools/cabana/utils/util.cc +++ b/openpilot/tools/cabana/utils/util.cc @@ -1,18 +1,19 @@ #include "tools/cabana/utils/util.h" #include +#include #include -#include #include #include #include #include +#include #include #include -#include #include #include #include +#include #include #include #include @@ -20,11 +21,6 @@ #include #endif -#include -#include -#include -#include -#include #include "common/util.h" static const std::thread::id main_thread_id = std::this_thread::get_id(); @@ -53,22 +49,21 @@ void utils::drainMainThreadQueue() { // SegmentTree -void SegmentTree::build(const std::vector &arr) { - size = arr.size(); +void SegmentTree::build(int n, const std::function &y) { + size = n; tree.resize(4 * size); // size of the tree is 4 times the size of the array if (size > 0) { - build_tree(arr, 1, 0, size - 1); + build_tree(y, 1, 0, size - 1); } } -void SegmentTree::build_tree(const std::vector &arr, int n, int left, int right) { +void SegmentTree::build_tree(const std::function &y, int n, int left, int right) { if (left == right) { - const double y = arr[left].y(); - tree[n] = {y, y}; + tree[n] = {y(left), y(left)}; } else { const int mid = (left + right) >> 1; - build_tree(arr, 2 * n, left, mid); - build_tree(arr, 2 * n + 1, mid + 1, right); + build_tree(y, 2 * n, left, mid); + build_tree(y, 2 * n + 1, mid + 1, right); tree[n] = {std::min(tree[2 * n].first, tree[2 * n + 1].first), std::max(tree[2 * n].second, tree[2 * n + 1].second)}; } } @@ -84,119 +79,22 @@ std::pair SegmentTree::get_minmax(int n, int left, int right, in return {std::min(l.first, r.first), std::max(l.second, r.second)}; } -// MessageBytesDelegate - -MessageBytesDelegate::MessageBytesDelegate(QObject *parent, bool multiple_lines) - : font_metrics(QApplication::font()), multiple_lines(multiple_lines), QStyledItemDelegate(parent) { - fixed_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); - byte_size = QFontMetrics(fixed_font).size(Qt::TextSingleLine, "00 ") + QSize(0, 2); - for (int i = 0; i < 256; ++i) { - hex_text_table[i].setText(QStringLiteral("%1").arg(i, 2, 16, QLatin1Char('0')).toUpper()); - hex_text_table[i].prepare({}, fixed_font); - } - h_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameHMargin) + 1; - v_margin = QApplication::style()->pixelMetric(QStyle::PM_FocusFrameVMargin) + 1; -} - -QSize MessageBytesDelegate::sizeForBytes(int n) const { - int rows = multiple_lines ? std::max(1, n / 8) : 1; - return {(n / rows) * byte_size.width() + h_margin * 2, rows * byte_size.height() + v_margin * 2}; -} - -QSize MessageBytesDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { - auto data = index.data(BytesRole); - return sizeForBytes(data.isValid() ? static_cast *>(data.value())->size() : 0); -} - -void MessageBytesDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - if (option.state & QStyle::State_Selected) { - painter->fillRect(option.rect, option.palette.brush(QPalette::Normal, QPalette::Highlight)); - } - - QRect item_rect = option.rect.adjusted(h_margin, v_margin, -h_margin, -v_margin); - QColor highlighted_color = option.palette.color(QPalette::HighlightedText); - auto text_color = index.data(Qt::ForegroundRole).value(); - bool inactive = text_color.isValid(); - if (!inactive) { - text_color = option.palette.color(QPalette::Text); - } - auto data = index.data(BytesRole); - if (!data.isValid()) { - painter->setFont(option.font); - painter->setPen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - QString text = font_metrics.elidedText(index.data(Qt::DisplayRole).toString(), Qt::ElideRight, item_rect.width()); - painter->drawText(item_rect, Qt::AlignLeft | Qt::AlignVCenter, text); - return; - } - - // Paint hex column - const auto &bytes = *static_cast *>(data.value()); - const auto &colors = *static_cast *>(index.data(ColorsRole).value()); - - painter->setFont(fixed_font); - const QPen text_pen(option.state & QStyle::State_Selected ? highlighted_color : text_color); - const QPoint pt = item_rect.topLeft(); - for (int i = 0; i < bytes.size(); ++i) { - int row = !multiple_lines ? 0 : i / 8; - int column = !multiple_lines ? i : i % 8; - QRect r({pt.x() + column * byte_size.width(), pt.y() + row * byte_size.height()}, byte_size); - - if (!inactive && i < colors.size() && colors[i].alpha() > 0) { - if (option.state & QStyle::State_Selected) { - painter->setPen(option.palette.color(QPalette::Text)); - painter->fillRect(r, option.palette.color(QPalette::Window)); - } - painter->fillRect(r, toQColor(colors[i])); - } else { - painter->setPen(text_pen); - } - utils::drawStaticText(painter, r, hex_text_table[bytes[i]]); - } -} - -// TabBar - -int TabBar::addTab(const QString &text) { - int index = QTabBar::addTab(text); - QToolButton *btn = new ToolButton("x", tr("Close Tab")); - int width = style()->pixelMetric(QStyle::PM_TabCloseIndicatorWidth, nullptr, btn); - int height = style()->pixelMetric(QStyle::PM_TabCloseIndicatorHeight, nullptr, btn); - btn->setFixedSize({width, height}); - setTabButton(index, QTabBar::RightSide, btn); - QObject::connect(btn, &QToolButton::clicked, this, &TabBar::closeTabClicked); - return index; -} - -void TabBar::closeTabClicked() { - QObject *object = sender(); - for (int i = 0; i < count(); ++i) { - if (tabButton(i, QTabBar::RightSide) == object) { - emit tabCloseRequested(i); - break; - } - } -} - // UnixSignalHandler -UnixSignalHandler::UnixSignalHandler() { +UnixSignalHandler::UnixSignalHandler(std::function on_signal) { if (::socketpair(AF_UNIX, SOCK_STREAM, 0, sig_fd)) { - qFatal("Couldn't create TERM socketpair"); + fprintf(stderr, "Couldn't create TERM socketpair\n"); + abort(); } - waiter = std::thread([this]() { + waiter = std::thread([this, on_signal = std::move(on_signal)]() { int tmp = 0; while (::read(sig_fd[1], &tmp, sizeof(tmp)) < 0) { if (errno != EINTR) return; } if (shutting_down.load()) return; - // Marshal exit onto the GUI thread (qApp methods are not thread-safe). - QMetaObject::invokeMethod(qApp, []() { - printf("\nexiting...\n"); - qApp->closeAllWindows(); - qApp->exit(); - }, Qt::QueuedConnection); + on_signal(); }); std::signal(SIGINT, signalHandler); @@ -216,118 +114,131 @@ void UnixSignalHandler::signalHandler(int s) { (void)!::write(sig_fd[0], &s, sizeof(s)); } -// NameValidator +// validators -NameValidator::NameValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NameValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - input.replace(' ', '_'); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (!c.isLetterOrNumber() && c != '_') return QValidator::Invalid; +ValidState validateName(std::string &input) { + std::replace(input.begin(), input.end(), ' ', '_'); + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (!std::isalnum(c) && c != '_') return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// NodeValidator - -NodeValidator::NodeValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NodeValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateNodes(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; // Match ^\w+(,\w+)*$ ; a trailing comma is Intermediate (user still typing). bool need_word = true; - for (const QChar &c : input) { - if (c.isLetterOrNumber() || c == '_') { + for (const unsigned char c : input) { + if (std::isalnum(c) || c == '_') { need_word = false; } else if (c == ',' && !need_word) { need_word = true; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return need_word ? QValidator::Intermediate : QValidator::Acceptable; + return need_word ? ValidState::Intermediate : ValidState::Acceptable; } -// NonWhitespaceValidator - -NonWhitespaceValidator::NonWhitespaceValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State NonWhitespaceValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - for (const QChar &c : input) { - if (c.isSpace()) return QValidator::Invalid; +ValidState validateNonWhitespace(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; + for (const unsigned char c : input) { + if (std::isspace(c)) return ValidState::Invalid; } - return QValidator::Acceptable; + return ValidState::Acceptable; } -// IpAddressValidator - -IpAddressValidator::IpAddressValidator(QObject *parent) : QValidator(parent) {} - -QValidator::State IpAddressValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; +ValidState validateIpAddress(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; int dots = 0; int value = 0; bool has_digit = false; - for (const QChar &c : input) { - if (c.isDigit()) { - value = has_digit ? value * 10 + c.digitValue() : c.digitValue(); - if (value > 255) return QValidator::Invalid; + for (const unsigned char c : input) { + if (std::isdigit(c)) { + value = has_digit ? value * 10 + (c - '0') : (c - '0'); + if (value > 255) return ValidState::Invalid; has_digit = true; } else if (c == '.') { - if (!has_digit || dots >= 3) return QValidator::Invalid; + if (!has_digit || dots >= 3) return ValidState::Invalid; ++dots; has_digit = false; value = 0; } else { - return QValidator::Invalid; + return ValidState::Invalid; } } - return (dots == 3 && has_digit) ? QValidator::Acceptable : QValidator::Intermediate; + return (dots == 3 && has_digit) ? ValidState::Acceptable : ValidState::Intermediate; } -DoubleValidator::DoubleValidator(QObject *parent) : QValidator(parent) {} +ValidState validateDouble(const std::string &input) { + if (input.empty()) return ValidState::Intermediate; -QValidator::State DoubleValidator::validate(QString &input, int &pos) const { - Q_UNUSED(pos); - if (input.isEmpty()) return QValidator::Intermediate; - - // Match QString::toDouble(): C locale, no hex floats / inf / nan. - const std::string bytes = input.toLatin1().toStdString(); - // strtod accepts 0x… hex floats and p-exponents; QString::toDouble does not. - if (bytes.find_first_of("xXpP") != std::string::npos) { - return QValidator::Invalid; + // C locale, no hex floats / p-exponents / inf / nan (strtod accepts them, the DBC parser does not) + if (input.find_first_of("xXpP") != std::string::npos) { + return ValidState::Invalid; } - const char *start = bytes.c_str(); + const char *start = input.c_str(); char *end = nullptr; const double value = std::strtod(start, &end); if (end == start) { // Still typing a sign, decimal point, or exponent prefix. if (input == "-" || input == "+" || input == "." || input == "-." || input == "+.") { - return QValidator::Intermediate; + return ValidState::Intermediate; } - return QValidator::Invalid; + return ValidState::Invalid; } if (*end == '\0') { - // Reject inf/nan (strtod accepts them; QDoubleValidator / toDouble path should not). - return std::isfinite(value) ? QValidator::Acceptable : QValidator::Invalid; + return std::isfinite(value) ? ValidState::Acceptable : ValidState::Invalid; } // Partial exponent / trailing sign while typing (e.g. "1e", "1e-", "1."). for (const char *p = end; *p; ++p) { const char c = *p; if (!(c == 'e' || c == 'E' || c == '+' || c == '-' || c == '.' || (c >= '0' && c <= '9'))) { - return QValidator::Invalid; + return ValidState::Invalid; } } - return QValidator::Intermediate; + return ValidState::Intermediate; +} + +// embedded at build time from the bootstrap_icons package (see SConscript) +extern const unsigned char bootstrap_icons_svg[]; +extern const size_t bootstrap_icons_svg_len; + +static std::unordered_map load_bootstrap_icons() { + std::unordered_map icons; + + const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); + const std::string sym_open = " with + svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) + icons[id] = std::move(svg_str); + } + } + pos = end; + } + return icons; } namespace utils { @@ -384,77 +295,19 @@ bool setClipboardText(const std::string &text) { return false; } -bool isDarkTheme() { - QColor windowColor = QApplication::palette().color(QPalette::Window); - return windowColor.lightness() < 128; -} - -QPixmap icon(const QString &id) { - bool dark_theme = isDarkTheme(); - - QPixmap pm; - QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); - if (!QPixmapCache::find(key, &pm)) { - pm = bootstrapPixmap(id); - if (dark_theme) { - QPainter p(&pm); - p.setCompositionMode(QPainter::CompositionMode_SourceIn); - p.fillRect(pm.rect(), QColor("#bbbbbb")); - } - QPixmapCache::insert(key, pm); - } - return pm; -} - -void setTheme(int theme) { - auto style = QApplication::style(); - if (!style) return; - - static int prev_theme = 0; - if (theme != prev_theme) { - prev_theme = theme; - QPalette new_palette; - if (theme == DARK_THEME) { - // "Darcula" like dark theme - new_palette.setColor(QPalette::Window, QColor("#353535")); - new_palette.setColor(QPalette::WindowText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Base, QColor("#3c3f41")); - new_palette.setColor(QPalette::AlternateBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipBase, QColor("#3c3f41")); - new_palette.setColor(QPalette::ToolTipText, QColor("#bbb")); - new_palette.setColor(QPalette::Text, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Button, QColor("#3c3f41")); - new_palette.setColor(QPalette::ButtonText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::Highlight, QColor("#2f65ca")); - new_palette.setColor(QPalette::HighlightedText, QColor("#bbbbbb")); - new_palette.setColor(QPalette::BrightText, QColor("#f0f0f0")); - new_palette.setColor(QPalette::Disabled, QPalette::ButtonText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::WindowText, QColor("#777777")); - new_palette.setColor(QPalette::Disabled, QPalette::Text, QColor("#777777")); - new_palette.setColor(QPalette::Light, QColor("#777777")); - new_palette.setColor(QPalette::Dark, QColor("#353535")); - } else { - new_palette = style->standardPalette(); - } - qApp->setPalette(new_palette); - style->polish(qApp); - for (auto w : QApplication::allWidgets()) { - w->setPalette(new_palette); - } - } +std::string bootstrapSvg(const std::string &id) { + static auto icons = load_bootstrap_icons(); + auto it = icons.find(id); + return it != icons.end() ? it->second : std::string(); } } // namespace utils int num_decimals(double num) { - const QString string = QString::number(num); - auto dot_pos = string.indexOf('.'); - return dot_pos == -1 ? 0 : string.size() - dot_pos - 1; -} - -void sigTermHandler(int s) { - std::signal(s, SIG_DFL); - qApp->quit(); + char buf[32]; + snprintf(buf, sizeof(buf), "%g", num); + const char *dot = strpbrk(buf, ".,"); // Qt sets LC_ALL from the environment so the decimal mark may be a comma + return dot ? (int)strlen(dot + 1) : 0; } std::filesystem::path executableDir() { @@ -469,70 +322,3 @@ std::filesystem::path executableDir() { return std::filesystem::path(util::readlink("/proc/self/exe")).parent_path(); #endif } - -void initApp(int argc, char *argv[], bool disable_hidpi) { - // setup signal handlers to exit gracefully - std::signal(SIGINT, sigTermHandler); - std::signal(SIGTERM, sigTermHandler); - -#ifdef __APPLE__ - // Get the devicePixelRatio, and scale accordingly to maintain 1:1 rendering - QApplication tmp(argc, argv); - if (disable_hidpi) { - qputenv("QT_SCALE_FACTOR", QString::number(1.0 / tmp.devicePixelRatio()).toLocal8Bit()); - } -#endif - - qputenv("QT_DBL_CLICK_DIST", "150"); - // ensure the current dir matches the exectuable's directory - std::error_code ec; - std::filesystem::current_path(executableDir(), ec); -} - -// embedded at build time from the bootstrap_icons package (see SConscript) -extern const unsigned char bootstrap_icons_svg[]; -extern const size_t bootstrap_icons_svg_len; - -static std::unordered_map load_bootstrap_icons() { - std::unordered_map icons; - - const std::string content(reinterpret_cast(bootstrap_icons_svg), bootstrap_icons_svg_len); - const std::string sym_open = " with - svg_str.replace(0, 7, " ""); // "" (9) -> "" (6) - icons[id] = std::move(svg_str); - } - } - pos = end; - } - return icons; -} - -QPixmap bootstrapPixmap(const QString &id) { - static auto icons = load_bootstrap_icons(); - - QPixmap pixmap; - auto it = icons.find(id.toStdString()); - if (it != icons.end()) { - pixmap.loadFromData((const uchar *)it->second.data(), it->second.size(), "svg"); - } - return pixmap; -} diff --git a/openpilot/tools/cabana/utils/util.h b/openpilot/tools/cabana/utils/util.h index e0d13208a6..324c966b5a 100644 --- a/openpilot/tools/cabana/utils/util.h +++ b/openpilot/tools/cabana/utils/util.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include #include @@ -10,136 +10,68 @@ #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "tools/cabana/core/observable.h" -#include "tools/cabana/dbc/dbc.h" -#include "tools/cabana/settings.h" -#include "tools/cabana/utils/strings.h" - -// needed by QVariant::fromValue() in the Qt views; goes away with QVariant -Q_DECLARE_METATYPE(MessageId) -Q_DECLARE_METATYPE(ValueDescription) - -inline QColor toQColor(const CabanaColor &color) { - return QColor(color.r, color.g, color.b, color.a); -} - -class LogSlider : public QSlider { - Q_OBJECT +#include "tools/cabana/core/color.h" +class SegmentTree { public: - LogSlider(double factor, Qt::Orientation orientation, QWidget *parent = nullptr) : factor(factor), QSlider(orientation, parent) {} + SegmentTree() = default; + void build(int n, const std::function &y); + inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +private: + std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; + void build_tree(const std::function &y, int n, int left, int right); + std::vector> tree; + int size = 0; +}; + +// maps a linear slider position onto a log10 scale +class LogScale { +public: + LogScale(double factor) : factor(factor) {} void setRange(double min, double max) { log_min = factor * std::log10(min); log_max = factor * std::log10(max); - QSlider::setRange(min, max); - setValue(QSlider::value()); } - int value() const { - double v = log_min + (log_max - log_min) * ((QSlider::value() - minimum()) / double(maximum() - minimum())); + int value(int pos, int pos_min, int pos_max) const { + double v = log_min + (log_max - log_min) * ((pos - pos_min) / double(pos_max - pos_min)); return std::lround(std::pow(10, v / factor)); } - void setValue(int v) { + int position(int v, int pos_min, int pos_max) const { double log_v = std::clamp(factor * std::log10(v), log_min, log_max); - v = minimum() + (maximum() - minimum()) * ((log_v - log_min) / (log_max - log_min)); - QSlider::setValue(v); + return pos_min + (pos_max - pos_min) * ((log_v - log_min) / (log_max - log_min)); } private: double factor, log_min = 0, log_max = 1; }; -enum { - ColorsRole = Qt::UserRole + 1, - BytesRole = Qt::UserRole + 2 -}; +enum class ValidState { Invalid, Intermediate, Acceptable }; -class SegmentTree { -public: - SegmentTree() = default; - void build(const std::vector &arr); - inline std::pair minmax(int left, int right) const { return get_minmax(1, 0, size - 1, left, right); } +// single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_' +ValidState validateName(std::string &input); +// comma-separated identifiers: \w+(,\w+)* +ValidState validateNodes(const std::string &input); +// one or more non-whitespace characters (\S+) +ValidState validateNonWhitespace(const std::string &input); +// dotted IPv4 address (0-255 per octet) +ValidState validateIpAddress(const std::string &input); +// C-locale floating-point +ValidState validateDouble(const std::string &input); -private: - std::pair get_minmax(int n, int left, int right, int range_left, int range_right) const; - void build_tree(const std::vector &arr, int n, int left, int right); - std::vector> tree; - int size = 0; -}; - -class MessageBytesDelegate : public QStyledItemDelegate { - Q_OBJECT -public: - MessageBytesDelegate(QObject *parent, bool multiple_lines = false); - void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; - bool multipleLines() const { return multiple_lines; } - void setMultipleLines(bool v) { multiple_lines = v; } - QSize sizeForBytes(int n) const; - -private: - std::array hex_text_table; - QFontMetrics font_metrics; - QFont fixed_font; - QSize byte_size = {}; - bool multiple_lines = false; - int h_margin, v_margin; -}; - -// Accepts a single identifier: one or more [A-Za-z0-9_], spaces rewritten to '_'. -class NameValidator : public QValidator { - Q_OBJECT -public: - NameValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts comma-separated identifiers: \w+(,\w+)* -class NodeValidator : public QValidator { - Q_OBJECT -public: - NodeValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts one or more non-whitespace characters (\S+). -class NonWhitespaceValidator : public QValidator { - Q_OBJECT -public: - NonWhitespaceValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// Accepts a dotted IPv4 address (0-255 per octet). -class IpAddressValidator : public QValidator { - Q_OBJECT -public: - IpAddressValidator(QObject *parent=nullptr); - QValidator::State validate(QString &input, int &pos) const override; -}; - -// C-locale floating-point validator (matches QString::toDouble). -class DoubleValidator : public QValidator { - Q_OBJECT -public: - DoubleValidator(QObject *parent = nullptr); - QValidator::State validate(QString &input, int &pos) const override; +// "Darcula" like dark theme +struct DarkTheme { + static constexpr CabanaColor window{0x35, 0x35, 0x35}; + static constexpr CabanaColor window_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor base{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor tooltip_text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor text{0xbb, 0xbb, 0xbb}; + static constexpr CabanaColor button{0x3c, 0x3f, 0x41}; + static constexpr CabanaColor highlight{0x2f, 0x65, 0xca}; + static constexpr CabanaColor bright_text{0xf0, 0xf0, 0xf0}; + static constexpr CabanaColor disabled_text{0x77, 0x77, 0x77}; + static constexpr CabanaColor light{0x77, 0x77, 0x77}; + static constexpr CabanaColor dark{0x35, 0x35, 0x35}; }; namespace utils { @@ -148,67 +80,23 @@ bool isMainThread(); // inline on the main thread, queued until drainMainThreadQueue() otherwise void runOnMainThread(std::function fn); void drainMainThreadQueue(); -QPixmap icon(const QString &id); std::string homePath(); std::filesystem::path configPath(); bool getClipboardText(std::string *text); // false if no clipboard tool is available bool setClipboardText(const std::string &text); -bool isDarkTheme(); -void setTheme(int theme); -inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { - auto size = (r.size() - text.size()) / 2; - p->drawStaticText(r.left() + size.width(), r.top() + size.height(), text); -} +std::string bootstrapSvg(const std::string &id); // empty if unknown // boundary conversions for the remaining Qt byte-array based state APIs template std::vector toBytes(const T &dat) { return {dat.begin(), dat.end()}; } -inline auto qbytes(const std::vector &dat) { - return decltype(QString().toUtf8())((const char *)dat.data(), (int)dat.size()); -} } -class ToolButton : public QToolButton { - Q_OBJECT -public: - ToolButton(const QString &icon, const QString &tooltip = {}, QWidget *parent = nullptr) : QToolButton(parent) { - setIcon(icon); - setToolTip(tooltip); - setAutoRaise(true); - const int metric = QApplication::style()->pixelMetric(QStyle::PM_SmallIconSize); - setIconSize({metric, metric}); - theme = settings.theme; - settings_connection_ = settings.changed.connect([this]() { updateIcon(); }); - } - void setIcon(const QString &icon) { - icon_str = icon; - QToolButton::setIcon(utils::icon(icon_str)); - } - -private: - void updateIcon() { if (std::exchange(theme, settings.theme) != theme) setIcon(icon_str); } - Connection settings_connection_; - QString icon_str; - int theme; -}; - -class TabBar : public QTabBar { - Q_OBJECT - -public: - TabBar(QWidget *parent) : QTabBar(parent) {} - int addTab(const QString &text); - -private: - void closeTabClicked(); -}; - -// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread -// (no Qt notifiers/timers). Exit is marshaled onto the GUI thread. +// Watches SIGINT/SIGTERM via a self-pipe and a dedicated waiter thread. +// on_signal runs on the waiter thread; the caller marshals to the GUI thread. class UnixSignalHandler { public: - UnixSignalHandler(); + UnixSignalHandler(std::function on_signal); ~UnixSignalHandler(); static void signalHandler(int s); @@ -220,5 +108,3 @@ private: int num_decimals(double num); std::filesystem::path executableDir(); -void initApp(int argc, char *argv[], bool disable_hidpi = true); -QPixmap bootstrapPixmap(const QString &id); diff --git a/openpilot/tools/cabana/videowidget.cc b/openpilot/tools/cabana/videowidget.cc index 23f60f29f1..f7b1b41628 100644 --- a/openpilot/tools/cabana/videowidget.cc +++ b/openpilot/tools/cabana/videowidget.cc @@ -12,6 +12,7 @@ #include #include "tools/cabana/tools/routeinfo.h" +#include "tools/cabana/utils/qtutil.h" const int MIN_VIDEO_HEIGHT = 100; const int THUMBNAIL_MARGIN = 3; diff --git a/openpilot/tools/cabana/videowidget.h b/openpilot/tools/cabana/videowidget.h index 1b6d81406e..8416eece59 100644 --- a/openpilot/tools/cabana/videowidget.h +++ b/openpilot/tools/cabana/videowidget.h @@ -12,7 +12,7 @@ #include #include "tools/cabana/cameraview.h" -#include "tools/cabana/utils/util.h" +#include "tools/cabana/utils/qtutil.h" #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h" From 839d3f500474e7726f50cc273fe750e8a05222bd Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:06:33 -0700 Subject: [PATCH 21/53] ui: guard branch switcher before internet connected (#38692) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 32fbc2b3a7..33f6e0e4ab 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -246,6 +246,9 @@ class TargetBranchButton(BigButton): self.set_value(target) def _on_click(self): + if not ui_state.params.get("UpdaterAvailableBranches"): + gui_app.push_widget(BigDialog("", tr("Please connect to Wi-Fi to switch branches."))) + return gui_app.push_widget(BranchSelectPage(self._on_select)) def _on_select(self, branch: str): From e571e21d14dc6247177bd4713283c7bb9ef4ebdd Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 12:07:15 -0700 Subject: [PATCH 22/53] ui: check for update on target branch switch (#38693) --- .../selfdrive/ui/layouts/settings/software.py | 3 +++ .../ui/mici/layouts/settings/software.py | 23 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/openpilot/selfdrive/ui/layouts/settings/software.py b/openpilot/selfdrive/ui/layouts/settings/software.py index bf986e614e..0d3191aca6 100644 --- a/openpilot/selfdrive/ui/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/layouts/settings/software.py @@ -197,6 +197,9 @@ class SoftwareLayout(Widget): selection = self._branch_dialog.selection ui_state.params.put("UpdaterTargetBranch", selection, block=True) self._branch_btn.action_item.set_value(selection) + self._download_btn.action_item.set_enabled(False) + self._waiting_for_updater = True + self._waiting_start_ts = time.monotonic() subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) self._branch_dialog = None diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 33f6e0e4ab..eeb6eb9bf1 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -97,15 +97,20 @@ class CheckUpdateButton(BigButton): gui_app.push_widget(dlg) return + self._signal_updater("SIGHUP" if self.get_value() == "download update" else "SIGUSR1") + + def check_for_update(self): + self._signal_updater("SIGUSR1") + + def _signal_updater(self, sig: str): self.set_enabled(False) self._state = UpdaterState.WAITING_FOR_UPDATER + self._hide_value_t = None + self.set_value("") self.set_icon(self._txt_update_icon) def run(): - if self.get_value() == "download update": - subprocess.run("pkill -SIGHUP -f openpilot.system.updated.updated", shell=True) - else: - subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + subprocess.run(f"pkill -{sig} -f openpilot.system.updated.updated", shell=True) threading.Thread(target=run, daemon=True).start() @@ -232,8 +237,9 @@ class BranchSelectPage(NavScroller): class TargetBranchButton(BigButton): - def __init__(self): + def __init__(self, check_update_btn: CheckUpdateButton): super().__init__("target branch", ui_state.params.get("UpdaterTargetBranch") or "") + self._check_update_btn = check_update_btn self.set_click_callback(self._on_click) self.set_visible(not ui_state.params.get_bool("IsTestedBranch")) self.set_enabled(lambda: ui_state.is_offroad()) @@ -254,7 +260,7 @@ class TargetBranchButton(BigButton): def _on_select(self, branch: str): ui_state.params.put("UpdaterTargetBranch", branch, block=True) self.set_value(branch) - subprocess.run("pkill -SIGUSR1 -f openpilot.system.updated.updated", shell=True) + self._check_update_btn.check_for_update() class SoftwareLayoutMici(NavScroller): @@ -268,10 +274,11 @@ class SoftwareLayoutMici(NavScroller): gui_app.texture("icons_mici/settings/device/uninstall.png", 64, 64), uninstall_openpilot_callback, exit_on_confirm=False) + check_update_btn = CheckUpdateButton() self._scroller.add_widgets([ SoftwareInfoLayoutMici(), - CheckUpdateButton(), + check_update_btn, InstallUpdateButton(), - TargetBranchButton(), + TargetBranchButton(check_update_btn), uninstall_openpilot_btn, ]) From a67cdf9a5138f2b3e0b1692fb4c92e6fa7c0fc8f Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 28 Aug 2026 15:08:18 -0700 Subject: [PATCH 23/53] ui: sync gpu loading to offroad (#38727) ui: sync gpu loading state --- openpilot/selfdrive/ui/mici/layouts/home.py | 7 ++++++- openpilot/system/ui/widgets/icon_widget.py | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 64f2961326..b567b405a8 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -1,4 +1,5 @@ import datetime +import math import time from openpilot.cereal import log @@ -140,6 +141,7 @@ class MiciHomeLayout(Widget): self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) + self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46)) self._body_icon = IconWidget("icons_mici/body.png", (54, 37)) @@ -151,6 +153,7 @@ class MiciHomeLayout(Widget): NetworkIcon(), self._experimental_icon, self._chestnut_icon, + self._chestnut_loading_icon, self._chestnut_failed_icon, self._body_icon, self._mic_icon, @@ -248,7 +251,9 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.LOADING, ChestnutState.ACTIVE)) + self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) + self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) + self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) diff --git a/openpilot/system/ui/widgets/icon_widget.py b/openpilot/system/ui/widgets/icon_widget.py index bf7790b937..f6f8a47231 100644 --- a/openpilot/system/ui/widgets/icon_widget.py +++ b/openpilot/system/ui/widgets/icon_widget.py @@ -14,3 +14,6 @@ class IconWidget(Widget): def _render(self, _) -> None: color = rl.Color(255, 255, 255, int(self._opacity * 255)) rl.draw_texture_ex(self._texture, rl.Vector2(self._rect.x, self._rect.y), 0.0, 1.0, color) + + def set_opacity(self, opacity: float) -> None: + self._opacity = opacity From 682b6a20dfe6764774a40f2ef51148c8078b7c79 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Fri, 28 Aug 2026 15:46:56 -0700 Subject: [PATCH 24/53] add chestnut offroad alerts (#38706) * system: add chestnut offroad alerts * system: refine chestnut offroad alerts * system: refine chestnut power alerts * system: confirm chestnut power recovery from PCIe * system: detect missing chestnut power from INA voltage --- openpilot/common/hardware/usb.py | 8 +- openpilot/common/params_keys.h | 6 ++ openpilot/selfdrive/modeld/helpers.py | 4 +- .../selfdrive/selfdrived/alerts_offroad.json | 24 +++++ openpilot/system/hardware/chestnut/status.py | 96 +++++++++++++++++++ openpilot/system/hardware/hardwared.py | 28 ++++-- 6 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 openpilot/system/hardware/chestnut/status.py diff --git a/openpilot/common/hardware/usb.py b/openpilot/common/hardware/usb.py index b9f6db2757..c3c2050b6b 100644 --- a/openpilot/common/hardware/usb.py +++ b/openpilot/common/hardware/usb.py @@ -4,11 +4,17 @@ from pathlib import Path CHESTNUT_FW_VERSION = "ed4e39b7" CHESTNUT_USB_IDS = ((0xADD1, 0x0001), (0x3801, 0x0001)) CHESTNUT_ROM_USB_IDS = ((0x174C, 0x2464), (0x174C, 0x2463)) +CHESTNUT_USB_PRODUCT = f"custom {CHESTNUT_FW_VERSION}-CLEAN" USB_DEVICES_PATH = Path("/sys/bus/usb/devices") TYPEC_CC_ORIENTATION_PATH = Path("/sys/class/power_supply/usb/typec_cc_orientation") PRIMARY_USB_CONTROLLER = "a600000.ssusb" +def is_chestnut_usb_id(vendor_id: int, product_id: int, include_bootloader: bool = False) -> bool: + ids = CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS if include_bootloader else CHESTNUT_USB_IDS + return (vendor_id, product_id) in ids + + def get_usb_topology() -> set[str]: try: return set(os.listdir(USB_DEVICES_PATH)) @@ -81,7 +87,7 @@ def set_usb_state(device_state, devices: list[dict]) -> None: entry.linkErrorCount = device["linkErrorCount"] entry.usb3Lane = device.get("usb3Lane", "unknown") - if (entry.vendorId, entry.productId) in CHESTNUT_USB_IDS: + if is_chestnut_usb_id(entry.vendorId, entry.productId): chestnut_present = True device_state.chestnutPresent = chestnut_present diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 21b7463d92..ba6eae3dd5 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -92,6 +92,12 @@ inline static std::unordered_map keys = { {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ChestnutBranch", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutNotDetected", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutOverheated", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutPcieUnavailable", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ChestnutUncompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutUpdateFailed", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ChestnutUsbSlow", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index 84236f3fd0..d081050055 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -7,7 +7,7 @@ import tempfile from pathlib import Path from openpilot.common.file_chunker import get_manifest_path -from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_IDS, USB_DEVICES_PATH +from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH, is_chestnut_usb_id MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' @@ -50,7 +50,7 @@ def chestnut_present() -> bool: try: usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16)) product = (d / "product").read_text().strip() - if usb_id in CHESTNUT_USB_IDS and product == f"custom {CHESTNUT_FW_VERSION}-CLEAN": + if is_chestnut_usb_id(*usb_id) and product == CHESTNUT_USB_PRODUCT: return True except Exception: pass diff --git a/openpilot/selfdrive/selfdrived/alerts_offroad.json b/openpilot/selfdrive/selfdrived/alerts_offroad.json index b0179c0ac3..33fec6e330 100644 --- a/openpilot/selfdrive/selfdrived/alerts_offroad.json +++ b/openpilot/selfdrive/selfdrived/alerts_offroad.json @@ -19,6 +19,30 @@ }, "Offroad_ChestnutBranch": { "text": "Chestnut detected! Switch to the release-chestnut branch to use chestnut-class models.", + "severity": -1 + }, + "Offroad_ChestnutNotDetected": { + "text": "Chestnut not detected. Check USB and 12V connections.", + "severity": 0 + }, + "Offroad_ChestnutOverheated": { + "text": "Chestnut overheated. Ensure good airflow. Current GPU temperature is %1.", + "severity": 0 + }, + "Offroad_ChestnutPcieUnavailable": { + "text": "%1", + "severity": 0 + }, + "Offroad_ChestnutUncompiled": { + "text": "Chestnut model not compiled. Keep ignition on and reboot the comma.", + "severity": 0 + }, + "Offroad_ChestnutUpdateFailed": { + "text": "Chestnut update failed. Check the USB cable.", + "severity": 0 + }, + "Offroad_ChestnutUsbSlow": { + "text": "Chestnut USB link is slow. Check the USB cable. The current speed is %1.", "severity": 0 }, "Offroad_UnregisteredHardware": { diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py new file mode 100644 index 0000000000..c3321a3971 --- /dev/null +++ b/openpilot/system/hardware/chestnut/status.py @@ -0,0 +1,96 @@ +import time + +from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, is_chestnut_usb_id +from openpilot.selfdrive.modeld.helpers import chestnut_compiled + + +CHESTNUT_RELEASE_BRANCHES = ("release-chestnut", "release-chestnut-staging") +CHESTNUT_POWERED_VOLTAGE = 5000 +GPU_TEMP_LIMIT = 100. +MEMORY_TEMP_LIMIT = 95. +TEMP_HYSTERESIS = 5. + + +class ChestnutStatus: + def __init__(self): + self.started = time.monotonic() + self.offroad = True + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.overheated = False + self.usb_seen = False + self.usb_failed = False + + def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, + model_loading: bool, model_active: bool | None, state, set_alert) -> None: + detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] + devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] + firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT + + if self.offroad and not offroad: + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + self.link_failures = 0 + self.model_loading_seen = False + self.model_attempted = False + self.usb_seen = firmware_ok + self.usb_failed = False + + self.model_loading_seen |= model_loading + self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + + if not offroad and self.usb_seen and not firmware_ok: + self.usb_failed = True + + if not offroad and state is not None: + powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE + power_lost = state.supplyFault or not powered + if self.model_attempted and power_lost and not self.power_lost: + self.power_unavailable = not self.power_seen + self.power_seen |= powered + + if not offroad and self.model_attempted and state is not None: + self.link_failures = self.link_failures + 1 if state.pcieLtssm != 0x78 else 0 + self.pcie_failed |= self.link_failures >= 2 or power_lost + self.power_lost |= power_lost + + if self.pcie_failed and self.power_lost and state is not None: + self.power_restored |= not state.supplyFault and state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE + if self.usb_failed: + self.pcie_failed = False + self.power_seen = False + self.power_unavailable = False + self.power_lost = False + self.power_restored = False + + if state is not None: + gpu_limit = GPU_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + memory_limit = MEMORY_TEMP_LIMIT - (TEMP_HYSTERESIS if self.overheated else 0.) + self.overheated = state.tempC >= gpu_limit or state.memoryTempC >= memory_limit + + release = branch in CHESTNUT_RELEASE_BRANCHES + missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) + slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) + set_alert("Offroad_ChestnutNotDetected", missing) + set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) + set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) + if self.power_lost: + pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else + "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else + "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") + else: + pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." + set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) + set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) + set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) + self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 64ca4415d1..11fe41400d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -16,16 +16,17 @@ from openpilot.common.utils import strip_deprecated_keys from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.common.realtime import DT_HW -from openpilot.selfdrive.modeld.helpers import MODELS_DIR, chestnut_compiled from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.common.hardware import HARDWARE, COMMA_HARDWARE, PC from openpilot.common.basedir import BASEDIR -from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_ROM_USB_IDS, CHESTNUT_USB_IDS, get_usb_state, get_usb_topology, set_usb_state +from openpilot.common.git import get_short_branch +from openpilot.common.hardware.usb import CHESTNUT_FW_VERSION, CHESTNUT_USB_PRODUCT, get_usb_state, get_usb_topology, is_chestnut_usb_id, set_usb_state from openpilot.common.linux import LinuxSystemStats from openpilot.system.loggerd.config import get_available_percent from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController +from openpilot.system.hardware.chestnut.status import ChestnutStatus from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -48,6 +49,11 @@ class Chestnut: self.attempts = 0 self.last_attempt = 0. self.flashed = False + self.mismatch = False + + @property + def failed(self) -> bool: + return self.mismatch and self.attempts >= self.MAX_ATTEMPTS and self.thread is not None and not self.thread.is_alive() and not self.flashed def flash(self) -> None: ret = subprocess.run(["sudo", sys.executable, os.path.join(BASEDIR, "openpilot/system/hardware/chestnut/flash.py"), CHESTNUT_FW_VERSION], @@ -56,9 +62,9 @@ class Chestnut: self.flashed = ret.returncode == 0 def update(self, offroad: bool, usb_state: list[dict]) -> None: - mismatch = any((d["vendorId"], d["productId"]) in CHESTNUT_USB_IDS + CHESTNUT_ROM_USB_IDS and - d["product"] != f"custom {CHESTNUT_FW_VERSION}-CLEAN" for d in usb_state) - if not mismatch: + self.mismatch = any(is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True) and + d["product"] != CHESTNUT_USB_PRODUCT for d in usb_state) + if not self.mismatch: self.flashed = False return @@ -190,7 +196,7 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() pm = messaging.PubMaster(['deviceState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates"], poll="pandaStates") + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") count = 0 @@ -238,7 +244,8 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - big_model_available = (MODELS_DIR / 'big_driving_supercombo.onnx').is_file() or chestnut_compiled() + chestnut_status = ChestnutStatus() + branch = get_short_branch() while not end_event.is_set(): sm.update(PANDA_STATES_TIMEOUT) @@ -300,8 +307,11 @@ def hardware_thread(end_event, hw_queue) -> None: set_usb_state(msg.deviceState, last_hw_state.usb_state) chestnut.update(started_ts is None, last_hw_state.usb_state) - set_offroad_alert_if_changed("Offroad_ChestnutBranch", msg.deviceState.chestnutPresent and not big_model_available) - + chestnut_state = sm["chestnutState"] + chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] + chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, + params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), + chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, From 7cf55c3b7a2d9bcee87821e413fa322866f64c5b Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:11:52 -0700 Subject: [PATCH 25/53] common: fix OpenpilotPrefix cleanup on macOS (#38728) The destructor built its cleanup commands as "rm %s -rf", with the flags after the operand. GNU rm permutes arguments so this works on device and in CI, but BSD rm on macOS stops option parsing at the first operand and treats "-rf" as a second filename: $ mkdir -p /tmp/rmtest/sub && rm /tmp/rmtest -rf rm: /tmp/rmtest: is a directory rm: -rf: No such file or directory exit=1 So nothing is removed, and each of the four calls prints two errors plus "system command failed (256)" from check_system. Every run of a tool that owns an OpenpilotPrefix (replay, cabana) leaks its params dir, its comma_home and its /tmp/msgq_ dir; 33 of each had accumulated on my machine. Pass the flags first. --- openpilot/common/prefix.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpilot/common/prefix.h b/openpilot/common/prefix.h index 89f346b9e5..0f2c592527 100644 --- a/openpilot/common/prefix.h +++ b/openpilot/common/prefix.h @@ -27,14 +27,14 @@ public: auto param_path = Params().getParamPath(); if (util::file_exists(param_path)) { std::string real_path = util::readlink(param_path); - util::check_system(util::string_format("rm %s -rf", real_path.c_str())); + util::check_system(util::string_format("rm -rf %s", real_path.c_str())); unlink(param_path.c_str()); } if (getenv("COMMA_CACHE") == nullptr) { - util::check_system(util::string_format("rm %s -rf", Path::download_cache_root().c_str())); + util::check_system(util::string_format("rm -rf %s", Path::download_cache_root().c_str())); } - util::check_system(util::string_format("rm %s -rf", Path::comma_home().c_str())); - util::check_system(util::string_format("rm %s -rf", msgq_path.c_str())); + util::check_system(util::string_format("rm -rf %s", Path::comma_home().c_str())); + util::check_system(util::string_format("rm -rf %s", msgq_path.c_str())); unsetenv("OPENPILOT_PREFIX"); } From 0e320594844b1c1d80aada3f8c51b6e0a1c14115 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Sun, 30 Aug 2026 09:32:36 -0700 Subject: [PATCH 26/53] replay: capture downloader's stderr so download progress is reported again (#38734) --- openpilot/tools/replay/py_downloader.cc | 47 +++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/openpilot/tools/replay/py_downloader.cc b/openpilot/tools/replay/py_downloader.cc index a265dfd6a3..db2b2be127 100644 --- a/openpilot/tools/replay/py_downloader.cc +++ b/openpilot/tools/replay/py_downloader.cc @@ -2,8 +2,11 @@ #include #include +#include +#include #include #include +#include #include #include @@ -14,8 +17,15 @@ namespace { static std::mutex handler_mutex; static DownloadProgressHandler progress_handler = nullptr; -// Run a Python command and capture stdout. Stderr is left attached to the parent. -// Returns stdout content. If abort is signaled, kills the child process. +void reportProgress(const char *line) { + uint64_t cur = 0, total = 0; + if (sscanf(line, "PROGRESS:%llu:%llu", (unsigned long long *)&cur, (unsigned long long *)&total) != 2) return; + std::lock_guard lk(handler_mutex); + if (progress_handler && total > 0) progress_handler(cur, total, true); +} + +// Run a Python command and capture stdout. Stderr is scanned for PROGRESS lines and otherwise passed +// through to the parent's stderr. Returns stdout content. If abort is signaled, kills the child process. std::string runPython(const std::vector &args, std::atomic *abort = nullptr) { // Build argv for execvp std::vector argv; @@ -27,16 +37,22 @@ std::string runPython(const std::vector &args, std::atomic *a } argv.push_back(nullptr); - int stdout_pipe[2]; + int stdout_pipe[2], stderr_pipe[2]; if (pipe(stdout_pipe) != 0) { rWarning("py_downloader: pipe() failed"); return {}; } + if (pipe(stderr_pipe) != 0) { + rWarning("py_downloader: pipe() failed"); + close(stdout_pipe[0]); close(stdout_pipe[1]); + return {}; + } pid_t pid = fork(); if (pid < 0) { rWarning("py_downloader: fork() failed"); close(stdout_pipe[0]); close(stdout_pipe[1]); + close(stderr_pipe[0]); close(stderr_pipe[1]); return {}; } @@ -57,6 +73,9 @@ std::string runPython(const std::vector &args, std::atomic *a close(stdout_pipe[0]); dup2(stdout_pipe[1], STDOUT_FILENO); close(stdout_pipe[1]); + close(stderr_pipe[0]); + dup2(stderr_pipe[1], STDERR_FILENO); + close(stderr_pipe[1]); execvp("python3", const_cast(argv.data())); _exit(127); @@ -64,6 +83,27 @@ std::string runPython(const std::vector &args, std::atomic *a // Parent process close(stdout_pipe[1]); + close(stderr_pipe[1]); + + // stderr carries the progress lines, so a thread reads it while the loop below waits on stdout + std::thread stderr_thread([fd = stderr_pipe[0]]() { + FILE *f = fdopen(fd, "r"); + if (!f) { + close(fd); + return; + } + char *line = nullptr; + size_t cap = 0; + while (getline(&line, &cap, f) > 0) { + if (strncmp(line, "PROGRESS:", 9) == 0) { + reportProgress(line); + } else { + fputs(line, stderr); + } + } + free(line); + fclose(f); + }); std::string stdout_data; char buf[4096]; @@ -102,6 +142,7 @@ std::string runPython(const std::vector &args, std::atomic *a stdout_data.append(buf, n); } close(stdout_pipe[0]); + stderr_thread.join(); int status; waitpid(pid, &status, 0); From de197ba6fab5b99f8d322139ca156d111f8cfec1 Mon Sep 17 00:00:00 2001 From: royjr Date: Sun, 30 Aug 2026 16:20:16 -0400 Subject: [PATCH 27/53] chestnut: alert when big model ready (#1947) egpu: alert when big model ready Co-authored-by: Jason Wen --- openpilot/cereal/custom.capnp | 1 + openpilot/selfdrive/selfdrived/selfdrived.py | 1 + openpilot/sunnypilot/selfdrive/selfdrived/events.py | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/openpilot/cereal/custom.capnp b/openpilot/cereal/custom.capnp index 086b10c01c..cfbbbe472a 100644 --- a/openpilot/cereal/custom.capnp +++ b/openpilot/cereal/custom.capnp @@ -353,6 +353,7 @@ struct OnroadEventSP @0xda96579883444c35 { speedLimitPending @22; e2eChime @23; laneChangeRoadEdge @24; + bigModelReady @25; } } diff --git a/openpilot/selfdrive/selfdrived/selfdrived.py b/openpilot/selfdrive/selfdrived/selfdrived.py index 180b58c916..dfaa94e013 100755 --- a/openpilot/selfdrive/selfdrived/selfdrived.py +++ b/openpilot/selfdrive/selfdrived/selfdrived.py @@ -198,6 +198,7 @@ class SelfdriveD(CruiseHelper): loading = self.params.get_bool("ChestnutLoading") if self.big_model_loading and not loading: self.big_model_ready_t = time.monotonic() + self.events_sp.add(custom.OnroadEventSP.EventName.bigModelReady) self.big_model_loading = loading if self.big_model_loading: self.events.add(EventName.bigModelLoading) diff --git a/openpilot/sunnypilot/selfdrive/selfdrived/events.py b/openpilot/sunnypilot/selfdrive/selfdrived/events.py index 3c010cc776..b50873dea4 100644 --- a/openpilot/sunnypilot/selfdrive/selfdrived/events.py +++ b/openpilot/sunnypilot/selfdrive/selfdrived/events.py @@ -252,4 +252,12 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { AlertStatus.userPrompt, AlertSize.small, Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.1), }, + + EventNameSP.bigModelReady: { + ET.PERMANENT: Alert( + "Big Model Ready", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 2.), + }, } From 70df7f227b8b580fd4f6afbf0cdc5e5fa9155432 Mon Sep 17 00:00:00 2001 From: Robbe Derks Date: Mon, 31 Aug 2026 14:01:20 +0200 Subject: [PATCH 28/53] bump panda (new health packet) (#38736) pandad: support compact health packet --- openpilot/selfdrive/pandad/pandad.cc | 20 ++++++++++---------- panda | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/openpilot/selfdrive/pandad/pandad.cc b/openpilot/selfdrive/pandad/pandad.cc index 78d12bd2ed..d0b65586aa 100644 --- a/openpilot/selfdrive/pandad/pandad.cc +++ b/openpilot/selfdrive/pandad/pandad.cc @@ -111,22 +111,22 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setUptime(health.uptime_pkt); ps.setSafetyTxBlocked(health.safety_tx_blocked_pkt); ps.setSafetyRxInvalid(health.safety_rx_invalid_pkt); - ps.setIgnitionLine(health.ignition_line_pkt); - ps.setIgnitionCan(health.ignition_can_pkt); - ps.setControlsAllowed(health.controls_allowed_pkt); + ps.setIgnitionLine((health.flags_pkt & HEALTH_FLAG_IGNITION_LINE) != 0U); + ps.setIgnitionCan((health.flags_pkt & HEALTH_FLAG_IGNITION_CAN) != 0U); + ps.setControlsAllowed((health.flags_pkt & HEALTH_FLAG_CONTROLS_ALLOWED) != 0U); ps.setTxBufferOverflow(health.tx_buffer_overflow_pkt); ps.setRxBufferOverflow(health.rx_buffer_overflow_pkt); ps.setPandaType(hw_type); ps.setSafetyModel(cereal::CarParams::SafetyModel(health.safety_mode_pkt)); ps.setSafetyParam(health.safety_param_pkt); ps.setFaultStatus(cereal::PandaState::FaultStatus(health.fault_status_pkt)); - ps.setPowerSaveEnabled((bool)(health.power_save_enabled_pkt)); - ps.setHeartbeatLost((bool)(health.heartbeat_lost_pkt)); + ps.setPowerSaveEnabled((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U); + ps.setHeartbeatLost((health.flags_pkt & HEALTH_FLAG_HEARTBEAT_LOST) != 0U); ps.setAlternativeExperience(health.alternative_experience_pkt); ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); - ps.setInterruptLoad(health.interrupt_load_pkt); + ps.setInterruptLoad(health.interrupt_load_pkt / 255.0f); ps.setFanPower(health.fan_power); - ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); + ps.setSafetyRxChecksInvalid((health.flags_pkt & HEALTH_FLAG_SAFETY_RX_CHECKS_INVALID) != 0U); ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); @@ -184,10 +184,10 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } if (spoofing_started) { - health.ignition_line_pkt = 1; + health.flags_pkt |= HEALTH_FLAG_IGNITION_LINE; } - bool ignition_local = ((health.ignition_line_pkt != 0) || (health.ignition_can_pkt != 0)); + bool ignition_local = (health.flags_pkt & (HEALTH_FLAG_IGNITION_LINE | HEALTH_FLAG_IGNITION_CAN)) != 0U; // Make sure CAN buses are live: safety_setter_thread does not work if Panda CAN are silent and there is only one other CAN node if (health.safety_mode_pkt == (uint8_t)(cereal::CarParams::SafetyModel::SILENT)) { @@ -195,7 +195,7 @@ std::optional send_panda_states(PubMaster *pm, Panda *panda, bool is_onroa } bool power_save_desired = !ignition_local; - if (health.power_save_enabled_pkt != power_save_desired) { + if (((health.flags_pkt & HEALTH_FLAG_POWER_SAVE_ENABLED) != 0U) != power_save_desired) { panda->set_power_saving(power_save_desired); } diff --git a/panda b/panda index dd8a5b3df7..75aa44bec9 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit dd8a5b3df77706337a11555377e7180c5adc8726 +Subproject commit 75aa44bec9140849868239b1f1e3f22624adb8fe From 4adbb85742b465108ac579da680bcc633eedb2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 31 Aug 2026 09:25:32 -0700 Subject: [PATCH 29/53] BMRLNAP (#38681) --- openpilot/selfdrive/modeld/SConscript | 6 ++++-- openpilot/selfdrive/modeld/compile_modeld.py | 12 +++++++----- .../modeld/models/big_driving_supercombo.onnx | 4 ++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index f046be9915..f65aebf2f9 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -24,7 +24,9 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + " if 'pycache' not in x and os.path.isfile(os.path.join(tinygrad_root, x))] def estimate_pickle_max_size(onnx_size): - return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty + # QCOM programs for models with spatial recurrent features can approach 2x + # the ONNX size. Overestimating only adds an empty trailing chunk. + return 2.0 * onnx_size + 10 * 1024 * 1024 if arch == 'comma_arm64': tg_backend = 'QCOM' @@ -45,7 +47,7 @@ tg_devices = { # which device to put jit inputs to at runtime CHESTNUT = chestnut_present() if CHESTNUT: - chestnut_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' + chestnut_tg_flags = f'DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it chestnut_lock = File("models/.chestnut.lock").abspath diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index 2d27a41496..f54d09e869 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -139,16 +139,18 @@ def get_policy_npy_shapes(input_shapes): dp = input_shapes['desire_pulse'] # (1, 25, 8) tc = input_shapes['traffic_convention'] # (1, 2) at = input_shapes['action_t'] # (1, 2) - fb = input_shapes['features_buffer'] # (1, 24, 512) + fb = input_shapes['features_buffer'] # (1, T-1, ...) e.g. (1, 24, 32, 512) with spatial features + feat_dim = math.prod(fb[2:]) # TODO prev_feat shouldn't exist and be handled inside the JIT, but corrupt on QCOM for now - shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], fb[2])} + shapes = {'desire': (dp[2],), 'traffic_convention': tuple(tc), 'action_t': tuple(at), 'prev_feat': (fb[0], feat_dim)} return shapes, [math.prod(s) for s in shapes.values()] def make_input_queues(input_shapes, frame_skip, device): input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) - fb = input_shapes['features_buffer'] # (1, 24, 512), past features only; the model appends the current frame's feature + fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature + feat_dim = math.prod(fb[2:]) dp = input_shapes['desire_pulse'] # (1, 25, 8) shapes, sizes = get_policy_npy_shapes(input_shapes) @@ -156,7 +158,7 @@ def make_input_queues(input_shapes, frame_skip, device): # views into the packed inputs, to be refilled at runtime npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) input_queues.update({ - 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], fb[2]), dtype=np.float32), device=device).contiguous().realize(), + 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(), 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), }) @@ -211,7 +213,7 @@ def make_run_policy(model_runner, model_metadata, frame_skip): inputs = { 'img': img, 'big_img': big_img, - 'features_buffer': feat_buf, + 'features_buffer': feat_buf.reshape(model_metadata['input_shapes']['features_buffer']), 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention, 'action_t': action_t, diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx index 4a04bd7833..bd92b1b876 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a501760a9d1d5fef0eab2b8c5d122d06124fc26dc8e0782e0aa94b82a208f0ff -size 1757355221 +oid sha256:a086d5249fc308bb73993d1e64630c669d4c7df5bde85f42ad61902543648525 +size 765953504 From 9fa7ef3d1722020ea83868c10182812d2650a336 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:46:52 -0700 Subject: [PATCH 30/53] ui: clarify branch switcher error message (#38732) --- openpilot/selfdrive/ui/mici/layouts/settings/software.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index eeb6eb9bf1..7a3d84e1e7 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -253,7 +253,7 @@ class TargetBranchButton(BigButton): def _on_click(self): if not ui_state.params.get("UpdaterAvailableBranches"): - gui_app.push_widget(BigDialog("", tr("Please connect to Wi-Fi to switch branches."))) + gui_app.push_widget(BigDialog("", tr("Failed to get available branches. Ensure you're connected to the internet and try again."))) return gui_app.push_widget(BranchSelectPage(self._on_select)) From da8ce858ec5a2478f17ac2e0b94bfd478f1c7a6e Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:55:16 -0700 Subject: [PATCH 31/53] ui(mici): name updater signal constants (#38731) * mici: name updater signal constants * drop SIGNAL_ prefix * self contained --------- Co-authored-by: Shane Smiskol --- .../selfdrive/ui/mici/layouts/settings/software.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/settings/software.py b/openpilot/selfdrive/ui/mici/layouts/settings/software.py index 7a3d84e1e7..539ebaca11 100644 --- a/openpilot/selfdrive/ui/mici/layouts/settings/software.py +++ b/openpilot/selfdrive/ui/mici/layouts/settings/software.py @@ -74,6 +74,10 @@ class SoftwareInfoLayoutMici(Widget): class CheckUpdateButton(BigButton): + UPDATER_PROC = "openpilot.system.updated.updated" + CHECK_FOR_UPDATE = "SIGUSR1" + DOWNLOAD_UPDATE = "SIGHUP" + def __init__(self): self._txt_update_icon = gui_app.texture("icons_mici/settings/device/update.png", 64, 75) self._txt_up_to_date_icon = gui_app.texture("icons_mici/settings/device/up_to_date.png", 64, 64) @@ -97,10 +101,10 @@ class CheckUpdateButton(BigButton): gui_app.push_widget(dlg) return - self._signal_updater("SIGHUP" if self.get_value() == "download update" else "SIGUSR1") + self._signal_updater(self.DOWNLOAD_UPDATE if self.get_value() == "download update" else self.CHECK_FOR_UPDATE) def check_for_update(self): - self._signal_updater("SIGUSR1") + self._signal_updater(self.CHECK_FOR_UPDATE) def _signal_updater(self, sig: str): self.set_enabled(False) @@ -110,7 +114,7 @@ class CheckUpdateButton(BigButton): self.set_icon(self._txt_update_icon) def run(): - subprocess.run(f"pkill -{sig} -f openpilot.system.updated.updated", shell=True) + subprocess.run(f"pkill -{sig} -f {self.UPDATER_PROC}", shell=True) threading.Thread(target=run, daemon=True).start() From 98ed8111f6bb5136aa1c7f0cf3078f3ac3a43210 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Mon, 31 Aug 2026 21:15:23 -0700 Subject: [PATCH 32/53] modeld_v2: big to small model fallback (#1974) --- openpilot/sunnypilot/modeld_v2/modeld.py | 72 +++++++++++-------- .../modeld_v2/tests/test_fallback.py | 62 ++++++++++++++++ openpilot/sunnypilot/models/default_model.py | 22 +++++- openpilot/sunnypilot/models/manager.py | 6 ++ openpilot/sunnypilot/models/model_name.py | 2 + 5 files changed, 133 insertions(+), 31 deletions(-) create mode 100644 openpilot/sunnypilot/modeld_v2/tests/test_fallback.py diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index d9c04d7824..cd421d70d5 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -8,22 +8,22 @@ See the LICENSE.md file in the root directory for more details. import os os.environ['GMMU'] = '0' +import numpy as np +import threading +import time +from setproctitle import setproctitle +from tinygrad.tensor import Tensor + +import openpilot.cereal.messaging as messaging from openpilot.common.hardware import COMMA_HARDWARE from openpilot.selfdrive.modeld.helpers import chestnut_present, load_oob -import time -import numpy as np -import openpilot.cereal.messaging as messaging from openpilot.cereal import log from opendbc.car.structs import car from openpilot.cereal.services import SERVICE_LIST -from setproctitle import setproctitle from openpilot.cereal.messaging import PubMaster, SubMaster from openpilot.cereal.visionipc import VisionStreamType from msgq.visionipc import VisionIpcClient, VisionBuf from opendbc.car.car_helpers import get_demo_car_params - -from tinygrad.tensor import Tensor - from openpilot.common.file_chunker import open_file_chunked from openpilot.common.swaglog import cloudlog from openpilot.common.params import Params @@ -42,13 +42,13 @@ from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS - from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.selfdrive.controls.lib.relc import RoadEdgeLaneChangeController PROCESS_NAME = "openpilot.selfdrive.modeld.modeld_tinygrad" +BIG_MODEL_TIMEOUT = 60 def _pkl_exists(path): @@ -68,6 +68,7 @@ def _find_driving_pkl(bundle): pkl_path = os.path.join(model_root, pkl_name) if _pkl_exists(pkl_path): return pkl_path + return None class FrameMeta: @@ -102,7 +103,7 @@ class ModelState(ModelStateBase): self.chestnut = chestnut pkl_path = _find_driving_pkl(model_bundle) - assert pkl_path is not None, "No driving pkl found — all models must be compiled with compile_modeld.py" + assert pkl_path is not None, f"No driving pkl found for {'chestnut' if chestnut else 'small model'} — all models must be compiled with compile_modeld.py" self._init_combined(pkl_path, cam_w, cam_h, model_bundle) def _init_combined(self, pkl_path, cam_w, cam_h, bundle): @@ -185,9 +186,6 @@ class ModelState(ModelStateBase): else: self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) - if self.chestnut: - self.warmup() - def warmup(self) -> None: dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names} transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k} @@ -288,8 +286,7 @@ class ModelState(ModelStateBase): buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): - cloudlog.error("model output not finite, dropping frame") - return None + raise RuntimeError("model output not finite") return outputs @@ -363,21 +360,26 @@ def main(demo=False): model = None if CHESTNUT: - import threading - def load(): - nonlocal model - model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True) - t = threading.Thread(target=load, daemon=True) - t.start() - t.join(60) - if model is None: - params.put_bool("ChestnutActive", False) - raise RuntimeError("chestnut model load failed or timed out (60s)") - params.put_bool("ChestnutActive", True) - else: - model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) + big_model = None + def load_big(): + nonlocal big_model + try: + m = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=True) + m.warmup() + big_model = m + except Exception: + cloudlog.exception("chestnut load failed") + loader = threading.Thread(target=load_big, daemon=True) + loader.start() + loader.join(BIG_MODEL_TIMEOUT) + model = big_model + params.put_bool("ChestnutActive", model is not None) + small_model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) if model is None or CHESTNUT else None + if model is None: + model = small_model params.put_bool("ChestnutLoading", False) + assert model is not None cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging @@ -386,7 +388,7 @@ def main(demo=False): sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() - chestnut_state = ChestnutState(pm, CHESTNUT) if CHESTNUT else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ) @@ -509,7 +511,19 @@ def main(demo=False): inputs['action_t'] = np.array([lat_action_t, long_action_t], dtype=np.float32) mt1 = time.perf_counter() - model_output = model.run(bufs, transforms, inputs, prepare_only) + try: + model_output = model.run(bufs, transforms, inputs, prepare_only) + except Exception: + if not params.get_bool("ChestnutActive"): + raise + cloudlog.exception("chestnut failed, falling back to small") + params.put_bool("ChestnutActive", False) + assert small_model is not None + model = small_model + if chestnut_state is not None: + chestnut_state.big = False + run_count = 0 + model_output = None mt2 = time.perf_counter() model_execution_time = mt2 - mt1 diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_fallback.py b/openpilot/sunnypilot/modeld_v2/tests/test_fallback.py new file mode 100644 index 0000000000..1cc8410f06 --- /dev/null +++ b/openpilot/sunnypilot/modeld_v2/tests/test_fallback.py @@ -0,0 +1,62 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" + +import io +import requests + +from openpilot.common.file_chunker import get_chunk_name +from openpilot.common.hardware import hw +from openpilot.common.test import OpenpilotTestCase +from openpilot.selfdrive.modeld.helpers import dump_oob +import openpilot.sunnypilot.modeld_v2.modeld as modeld_module +from openpilot.sunnypilot.modeld_v2.tests import helpers as tests_helpers +from openpilot.sunnypilot.modeld_v2.tests.helpers import DummyModel, DummyBundle, CAM_W, CAM_H +from openpilot.sunnypilot.models.fetcher import ModelParser, ModelFetcher + +tmp_path = tests_helpers.tmp_path + + +class TestFallback(OpenpilotTestCase): + def test_find_dual_model_in_bundle(self, tmp_path, monkeypatch): + lebowski_file = 'driving_lebowski.pkl' + tsfdo_file = 'driving_tsfdo.pkl' + (tmp_path / lebowski_file).write_bytes(b'fkasdjfkljf') + (tmp_path / tsfdo_file).write_bytes(b'dskfajklsdjlsfka') + + monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path))) + big_bundle = DummyBundle(models=[DummyModel('supercombo', lebowski_file)]) + small_bundle = DummyBundle(models=[DummyModel('supercombo', tsfdo_file)]) + big_pkl = modeld_module._find_driving_pkl(big_bundle) + small_pkl = modeld_module._find_driving_pkl(small_bundle) + + assert big_pkl is not None and lebowski_file in big_pkl + assert small_pkl is not None and tsfdo_file in small_pkl + + def test_download_models_and_init_modelstate_fallback(self, tmp_path, monkeypatch): + monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path))) + big_json = requests.get(ModelFetcher.MODEL_URL_CHESTNUT).json() + big_bundle = ModelParser.parse_models(big_json)[-1] + small_json = requests.get(ModelFetcher.MODEL_URL).json() + small_bundle = ModelParser.parse_models(small_json)[-1] + + buf = io.BytesIO() + dump_oob(tests_helpers.make_pkl_data(tests_helpers.ARCHETYPES['supercombo_non20hz']), buf) + oob_bytes = buf.getvalue() + + for bundle in (big_bundle, small_bundle): + artifact = bundle.models[0].artifact + for i in range(len(artifact.chunks)): + (tmp_path / get_chunk_name(artifact.fileName, i, len(artifact.chunks))).write_bytes(oob_bytes if i == 0 else b"") + + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: small_bundle) + assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=False).chestnut is False + + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None, *, chestnut=None: big_bundle) + try: + assert modeld_module.ModelState(CAM_W, CAM_H, chestnut=True).chestnut is True + except Exception as e: + assert "AMD" in str(e) or "device" in str(e).lower() diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index 69f08c2cd3..e3c9360835 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -1,11 +1,14 @@ import argparse import os import hashlib +import requests +import re from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.sunnypilot import get_file_hash from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MODEL +from openpilot.sunnypilot.models.fetcher import ModelFetcher def get_default_model() -> str: @@ -30,14 +33,29 @@ def update_model_hash(): print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") +def get_ref_for_name(url: str, name: str) -> str: + response = requests.get(url, timeout=10) + if response.status_code == 200: + bundles = response.json()["bundles"] + matching = [b for b in bundles if re.search(name, f"{b['short_name']} {b['display_name']}", re.IGNORECASE)] + if matching: + return max(matching, key=lambda b: int(b["index"]))["ref"] + return "" + + def update_default_model_names(default_model_name: str, default_big_model_name: str): print("[CHANGE DEFAULT MODEL NAMES]") + small_ref = get_ref_for_name(ModelFetcher.MODEL_URL, default_model_name) + big_ref = get_ref_for_name(ModelFetcher.MODEL_URL_CHESTNUT, default_big_model_name) + with open(DEFAULT_MODEL_NAME_PATH, "w") as f: f.write(f'DEFAULT_MODEL = "{default_model_name}"\n') + f.write(f'DEFAULT_MODEL_REF = "{small_ref}"\n') f.write(f'DEFAULT_BIG_MODEL = "{default_big_model_name}"\n') + f.write(f'DEFAULT_BIG_MODEL_REF = "{big_ref}"\n') - print(f'New default small model name: "{default_model_name}"') - print(f'New default big model name: "{default_big_model_name}"') + print(f'New default small model name: "{default_model_name}" (ref: {small_ref})') + print(f'New default big model name: "{default_big_model_name}" (ref: {big_ref})') print("[DONE]") diff --git a/openpilot/sunnypilot/models/manager.py b/openpilot/sunnypilot/models/manager.py index 16253db0ff..035f59891c 100644 --- a/openpilot/sunnypilot/models/manager.py +++ b/openpilot/sunnypilot/models/manager.py @@ -328,6 +328,12 @@ class ModelManagerSP: validate_active_bundles(self.params, self.source_models) self.active_bundle = get_active_bundle(self.params, chestnut=self.chestnut_present) + if get_selected_bundle(self.params, "chestnut") is not None and get_selected_bundle(self.params, "qcom") is None: + if self.params.get("ModelManager_DownloadRef") is None: + from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL_REF + if DEFAULT_MODEL_REF: + self.params.put("ModelManager_DownloadRef", DEFAULT_MODEL_REF) + self._process_download_requests() if self.params.get("ModelManager_ClearCache"): diff --git a/openpilot/sunnypilot/models/model_name.py b/openpilot/sunnypilot/models/model_name.py index 374e8473df..fce14d0990 100644 --- a/openpilot/sunnypilot/models/model_name.py +++ b/openpilot/sunnypilot/models/model_name.py @@ -1,2 +1,4 @@ DEFAULT_MODEL = "CD210" +DEFAULT_MODEL_REF = "5b6436a90cf6902b8aaa71c2b6f3d7164d8ae391" DEFAULT_BIG_MODEL = "Lebowski" +DEFAULT_BIG_MODEL_REF = "fa0c6876d3cf070e91e25e5353ceadc68a5b3285" From e10c0fd960016e385f1135d9af4af03b634a7ad2 Mon Sep 17 00:00:00 2001 From: XiaoXX Date: Tue, 1 Sep 2026 12:35:56 +0800 Subject: [PATCH 33/53] modem.py: accept hex chars in ICCID (#38735) E.118 specifies decimal digits, but many real SIMs carry hex characters in EF_ICCID (e.g. China Mobile's 898600B5... range, some MVNO/IoT SIMs). AT+QCCID returns them verbatim, and the strict isdigit() check blanked the ICCID, leaving the modem daemon stuck in INITIALIZING forever and cellular dead. ModemManager parses ICCID as hex for the same reason. Verified on a comma four with a China Mobile SIM (EG916Q-GL): previously stuck retrying 'identity read incomplete', now dials and passes traffic. --- openpilot/common/hardware/comma/modem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpilot/common/hardware/comma/modem.py b/openpilot/common/hardware/comma/modem.py index 171ed8c0e4..901efae2b1 100755 --- a/openpilot/common/hardware/comma/modem.py +++ b/openpilot/common/hardware/comma/modem.py @@ -5,6 +5,7 @@ import logging import os import select import signal +import string import struct import subprocess import tempfile @@ -354,7 +355,7 @@ class Modem: imei = "" iccid = (self._atv("AT+QCCID", "+QCCID:") or "").rstrip("F") - if not iccid.isdigit(): + if not all(c in string.hexdigits for c in iccid): iccid = "" imsi = first_line("AT+CIMI") From 51987a62d07c44cc9e14b4d85dcb445edecd17d3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 1 Sep 2026 01:11:30 -0400 Subject: [PATCH 34/53] ci: route build_model runner by hardware type --- .github/workflows/sunnypilot-build-model.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 459fa74595..10be63ee22 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -121,7 +121,7 @@ jobs: if-no-files-found: error build_model: - runs-on: [self-hosted, chestnut] + runs-on: [self-hosted, "${{ inputs.target_hardware == 'chestnut' && 'chestnut' || 'tici' }}"] needs: get_model env: MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) From a2e422eee0e92523101329437b24bafa09538aa7 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Mon, 31 Aug 2026 22:30:28 -0700 Subject: [PATCH 35/53] TGC (#38739) * 23e6a04e-e6e5-462b-a0bb-e4088275ee43/12864 tgc * here --- openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx | 4 ++-- tinygrad_repo | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx index bd92b1b876..7b51732275 100644 --- a/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx +++ b/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a086d5249fc308bb73993d1e64630c669d4c7df5bde85f42ad61902543648525 -size 765953504 +oid sha256:1791d5940b2c048d0639813426dd2cf1d6f2a6727ed51e17c8bcea8bbe754123 +size 765950064 diff --git a/tinygrad_repo b/tinygrad_repo index c015351ac5..b87159cee1 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit c015351ac5c00c10c58dbdaf530ef5b2883ab948 +Subproject commit b87159cee1b137c327f901a6aef69f394aa629f6 From 7d5596d5c3527f274a3615181f52095909ffb8e9 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 05:59:02 -0700 Subject: [PATCH 36/53] monitor chestnut USB in hardwared (#38741) hardwared: monitor chestnut USB independently --- openpilot/cereal/log.capnp | 1 + openpilot/cereal/services.py | 1 + openpilot/selfdrive/modeld/modeld.py | 67 ++-------- .../system/hardware/chestnut/monitoring.py | 124 ++++++++++++++++++ openpilot/system/hardware/chestnut/status.py | 37 ++++-- openpilot/system/hardware/hardwared.py | 25 +++- 6 files changed, 180 insertions(+), 75 deletions(-) create mode 100644 openpilot/system/hardware/chestnut/monitoring.py diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fb4867b2a6..9ca1092e89 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2593,6 +2593,7 @@ struct Event { clocks @35 :Clocks; deviceState @6 :DeviceState; chestnutState @152 :ChestnutState; + chestnutGpuState @153 :ChestnutState; logMessage @18 :Text; errorLogMessage @85 :Text; diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index 08633d6975..f906e4610f 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -26,6 +26,7 @@ _services: dict[str, tuple] = { "temperatureSensor": (True, 2., 200), "deviceState": (True, 2., 1), "chestnutState": (True, 10., 10), + "chestnutGpuState": (False, 10.), "touch": (True, 20., 1), "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "controlsState": (True, 100., 10, QueueSize.MEDIUM), diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index a795f366ce..1178349c47 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,8 +6,6 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device -import usb1 -import struct import threading import time import numpy as np @@ -32,7 +30,6 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked -from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob @@ -73,45 +70,14 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. shouldStop=bool(stop)) -class ChestnutState: - # only modeld can access chestnut +class ChestnutGpuState: + # SMU metrics are only accessible from modeld. def __init__(self, pm: PubMaster, big: bool): self.pm = pm self.big = big self.valid = True self.sends = 0 self.metrics = {} - self._asm_usb = None - - def _close_asm_usb(self) -> None: - if self._asm_usb is not None: - self._asm_usb.close() - self._asm_usb = None - - def _open_asm_usb(self): - context = usb1.USBContext() - for vendor_id, product_id in CHESTNUT_USB_IDS: - if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: - return handle - context.close() - - def _read_ina(self) -> tuple[int, int, bool]: - if "AMD" in Device._opened_devices and self._asm_usb is None: - try: - raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) - return struct.unpack(' int: @@ -119,8 +85,6 @@ class ChestnutState: return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) def send(self) -> None: - msg = messaging.new_message('chestnutState') - state = msg.chestnutState self.sends += 1 if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: @@ -142,25 +106,14 @@ class ChestnutState: cloudlog.exception("chestnut state read failed") self.valid = False self.metrics.clear() + + msg = messaging.new_message('chestnutGpuState') + state = msg.chestnutGpuState if self.big: for k, v in self.metrics.items(): setattr(state, k, v) - - asm_valid = False - try: - # ASM runs on USB-C power, these still read without a gpu - state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() - asm_valid = True - except Exception: - pass - if "AMD" in Device._opened_devices: - try: - state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0] - except Exception: - pass - - msg.valid = asm_valid and (not self.big or self.valid) - self.pm.send('chestnutState', msg) + msg.valid = self.big and self.valid + self.pm.send('chestnutGpuState', msg) class FrameMeta: @@ -310,13 +263,13 @@ def main(demo=False): cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None + chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -425,7 +378,7 @@ def main(demo=False): mt1 = time.perf_counter() try: send_chestnut = (chestnut_state is not None and - run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0) model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): diff --git a/openpilot/system/hardware/chestnut/monitoring.py b/openpilot/system/hardware/chestnut/monitoring.py new file mode 100644 index 0000000000..829db99618 --- /dev/null +++ b/openpilot/system/hardware/chestnut/monitoring.py @@ -0,0 +1,124 @@ +import struct +from contextlib import suppress + +import usb1 + +import openpilot.cereal.messaging as messaging +from openpilot.cereal.services import SERVICE_LIST +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS + + +USB_TIMEOUT_MS = 100 +PCIE_LTSSM_ADDRESS = 0xB450 + + +class ChestnutUsb: + def __init__(self): + self.context: usb1.USBContext | None = None + self.handle = None + + def close(self) -> None: + handle, context = self.handle, self.context + self.handle = None + self.context = None + with suppress(Exception): + if handle is not None: + handle.close() + with suppress(Exception): + if context is not None: + context.close() + + def connect(self) -> bool: + if self.handle is not None: + return True + + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True) + if handle is not None: + self.context = context + self.handle = handle + return True + context.close() + return False + + def _read(self, request: int, value: int, length: int) -> bytes: + if self.handle is None: + raise usb1.USBErrorNoDevice + raw = bytes(self.handle.controlRead(0xC0, request, value, 0, length, timeout=USB_TIMEOUT_MS)) + if len(raw) != length: + raise ValueError(f"short chestnut USB response: {len(raw)}/{length}") + return raw + + def read_ina(self) -> tuple[int, int, bool]: + return struct.unpack(' int: + return self._read(0xE4, PCIE_LTSSM_ADDRESS, 1)[0] + + +class ChestnutMonitoring: + def __init__(self, usb: ChestnutUsb | None = None): + self.usb = usb or ChestnutUsb() + self.gpu_state = None + self.seen = False + self.enabled = False + self.usb_failed = False + + def set_enabled(self, enabled: bool) -> None: + if self.enabled == enabled: + return + self.enabled = enabled + self.usb.close() + self.usb_failed = False + + def retry(self) -> None: + self.usb_failed = False + + def model_alive(self, sm: messaging.SubMaster, now: float) -> bool: + modeld = next((p for p in sm['managerState'].processes if p.name == 'modeld'), None) + if modeld is not None and modeld.shouldBeRunning and not modeld.running: + return False + recv_time = sm.recv_time['chestnutGpuState'] + return recv_time > 0. and now - recv_time < 10. / SERVICE_LIST['chestnutGpuState'].frequency + + def update_gpu_state(self, sm: messaging.SubMaster, now: float) -> None: + if sm.updated['chestnutGpuState']: + self.gpu_state = sm['chestnutGpuState'] if sm.valid['chestnutGpuState'] else None + elif not self.model_alive(sm, now): + self.gpu_state = None + + def update(self, sm: messaging.SubMaster, now: float, model_loading: bool = False): + self.update_gpu_state(sm, now) + return self.build_message(model_loading) + + def build_message(self, model_loading: bool = False): + if not self.enabled: + return None + + msg = messaging.new_message('chestnutState') + if self.gpu_state is not None: + msg.chestnutState = self.gpu_state + state = msg.chestnutState + + if self.usb_failed: + return msg if self.seen else None + + try: + if not self.usb.connect(): + self.usb_failed = True + return msg if self.seen else None + + self.seen = True + voltage, current, fault = self.usb.read_ina() + pcie_ltssm = self.usb.read_pcie_ltssm() + state.supplyVoltage = voltage + state.supplyCurrent = current + state.supplyFault = fault + state.pcieLtssm = pcie_ltssm + msg.valid = True + except Exception as e: + if not model_loading or not isinstance(e, usb1.USBErrorTimeout): + self.usb.close() + self.usb_failed = True + return msg diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index c3321a3971..34456b2c11 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -21,14 +21,13 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 - self.model_loading_seen = False self.model_attempted = False self.overheated = False self.usb_seen = False self.usb_failed = False def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, - model_loading: bool, model_active: bool | None, state, set_alert) -> None: + model_active: bool | None, state, usb_failed: bool, set_alert) -> None: detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT @@ -40,16 +39,15 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 - self.model_loading_seen = False self.model_attempted = False self.usb_seen = firmware_ok self.usb_failed = False - self.model_loading_seen |= model_loading - self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None + self.model_attempted |= model_active is not None - if not offroad and self.usb_seen and not firmware_ok: - self.usb_failed = True + if not offroad: + self.usb_seen |= firmware_ok + self.usb_failed = not offroad and self.usb_seen and (not firmware_ok or usb_failed) if not offroad and state is not None: powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE @@ -80,17 +78,28 @@ class ChestnutStatus: release = branch in CHESTNUT_RELEASE_BRANCHES missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 - set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) - set_alert("Offroad_ChestnutNotDetected", missing) - set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) - set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) + update_failed = offroad and firmware_failed + compiled = firmware_ok and chestnut_compiled() + uncompiled = offroad and firmware_ok and not compiled + if self.power_lost: pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") else: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." - set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) - set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) - set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) + + alerts = ( + ("Offroad_ChestnutNotDetected", missing, None), + ("Offroad_ChestnutUpdateFailed", update_failed, None), + ("Offroad_ChestnutUncompiled", uncompiled, None), + ("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert), + ("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None), + ("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None), + ) + active_alert = next((name for name, active, _ in alerts if active), None) + + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1 and not missing) + for name, _, extra_text in alerts: + set_alert(name, name == active_alert, extra_text) self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index 11fe41400d..c7d11fa5b0 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -27,6 +27,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController from openpilot.system.hardware.chestnut.status import ChestnutStatus +from openpilot.system.hardware.chestnut.monitoring import ChestnutMonitoring from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -195,8 +196,9 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() - pm = messaging.PubMaster(['deviceState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") + pm = messaging.PubMaster(['deviceState', 'chestnutState']) + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", + "chestnutState", "chestnutGpuState", "managerState"], poll="pandaStates") count = 0 @@ -244,7 +246,9 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() + chestnut_monitoring = ChestnutMonitoring() chestnut_status = ChestnutStatus() + model_loading = params.get_bool("ChestnutLoading") branch = get_short_branch() while not end_event.is_set(): @@ -276,6 +280,8 @@ def hardware_thread(end_event, hw_queue) -> None: # Run at 2Hz, plus either edge of ignition ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: + if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: + pm.send('chestnutState', chestnut_msg) continue msg = messaging.new_message('deviceState', valid=True) @@ -309,9 +315,11 @@ def hardware_thread(end_event, hw_queue) -> None: chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut_state = sm["chestnutState"] chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] + model_loading = params.get_bool("ChestnutLoading") + model_active = params.get("ChestnutActive") chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, - params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), - chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) + model_active, chestnut_state if chestnut_valid else None, chestnut_monitoring.usb_failed, + set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, @@ -419,6 +427,15 @@ def hardware_thread(end_event, hw_queue) -> None: if off_ts is None: off_ts = time.monotonic() + chestnut_usb_ready = any(is_chestnut_usb_id(d["vendorId"], d["productId"]) and d["product"] == CHESTNUT_USB_PRODUCT + for d in last_hw_state.usb_state) + flash_active = chestnut.thread is not None and chestnut.thread.is_alive() + chestnut_monitoring.set_enabled(started_ts is not None and (chestnut_usb_ready or chestnut_monitoring.seen) and not flash_active) + if chestnut_usb_ready and chestnut_monitoring.usb_failed: + chestnut_monitoring.retry() + if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: + pm.send('chestnutState', chestnut_msg) + # Offroad power monitoring voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage power_monitor.calculate(voltage, onroad_conditions["ignition"]) From 06af2abe67db217f96e6152ebf4f018fa1cb4bca Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 07:20:56 -0700 Subject: [PATCH 37/53] modeld: wait for stable chestnut (#38742) modeld: wait for stable chestnut --- openpilot/common/params_keys.h | 1 + openpilot/selfdrive/modeld/helpers.py | 6 ++++++ openpilot/selfdrive/modeld/modeld.py | 24 +++++++++++++++++++++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index ba6eae3dd5..07a2283ece 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -135,5 +135,6 @@ inline static std::unordered_map keys = { {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, {"ChestnutActive", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"ChestnutLoading", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, + {"ChestnutModelError", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | CLEAR_ON_IGNITION_ON, BOOL}}, {"Version", {PERSISTENT, STRING}}, }; diff --git a/openpilot/selfdrive/modeld/helpers.py b/openpilot/selfdrive/modeld/helpers.py index d081050055..23eb6cbd21 100644 --- a/openpilot/selfdrive/modeld/helpers.py +++ b/openpilot/selfdrive/modeld/helpers.py @@ -11,6 +11,8 @@ from openpilot.common.hardware.usb import CHESTNUT_USB_PRODUCT, USB_DEVICES_PATH MODELS_DIR = Path(__file__).resolve().parent / 'models' TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json' +CHESTNUT_POWERED_VOLTAGE = 5000 +CHESTNUT_PCIE_READY = 0x78 def get_tg_input_devices(process_name: str, chestnut: bool): @@ -58,3 +60,7 @@ def chestnut_present() -> bool: def chestnut_compiled() -> bool: return Path(get_manifest_path(modeld_pkl_path(chestnut=True))).is_file() + + +def chestnut_ready(state) -> bool: + return state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE and not state.supplyFault and state.pcieLtssm == CHESTNUT_PCIE_READY diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index 1178349c47..c31418ee0d 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -31,7 +31,7 @@ from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_IN from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -205,12 +205,25 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - CHESTNUT = chestnut_present() and chestnut_compiled() + chestnut_available = chestnut_present() and chestnut_compiled() + CHESTNUT = False + if chestnut_available: + poller = messaging.Poller() + sock = messaging.sub_sock("chestnutState", poller=poller, conflate=True) + deadline = time.monotonic() + 4. / SERVICE_LIST['deviceState'].frequency + while not CHESTNUT and (remaining := deadline - time.monotonic()) > 0.: + if not poller.poll(round(remaining * 1000)): + break + msg = messaging.recv_one_or_none(sock) + CHESTNUT = msg is not None and msg.valid and chestnut_ready(msg.chestnutState) if CHESTNUT: os.environ['HCQDEV_WAIT_TIMEOUT_MS'] = '3000' params = Params() params.put_bool("ChestnutLoading", CHESTNUT) - params.remove("ChestnutActive") + if chestnut_available and not CHESTNUT: + params.put_bool("ChestnutActive", False) + else: + params.remove("ChestnutActive") config_realtime_process(7, 54) @@ -254,7 +267,11 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model + if model is None: + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", model is not None) + if model is not None: + params.remove("ChestnutModelError") small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or CHESTNUT else None if model is None: @@ -385,6 +402,7 @@ def main(demo=False): raise # fallback to small model cloudlog.exception("big model failed, fall back to small") + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", False) model = small_model if chestnut_state is not None: From c9f1602040149be793ea3d3e17a5fd8d915408c8 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 11:13:30 -0700 Subject: [PATCH 38/53] Revert "monitor chestnut USB in hardwared (#38741)" (#38744) This reverts commit 7d5596d5c3527f274a3615181f52095909ffb8e9. --- openpilot/cereal/log.capnp | 1 - openpilot/cereal/services.py | 1 - openpilot/selfdrive/modeld/modeld.py | 67 ++++++++-- .../system/hardware/chestnut/monitoring.py | 124 ------------------ openpilot/system/hardware/chestnut/status.py | 37 ++---- openpilot/system/hardware/hardwared.py | 25 +--- 6 files changed, 75 insertions(+), 180 deletions(-) delete mode 100644 openpilot/system/hardware/chestnut/monitoring.py diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index 9ca1092e89..fb4867b2a6 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -2593,7 +2593,6 @@ struct Event { clocks @35 :Clocks; deviceState @6 :DeviceState; chestnutState @152 :ChestnutState; - chestnutGpuState @153 :ChestnutState; logMessage @18 :Text; errorLogMessage @85 :Text; diff --git a/openpilot/cereal/services.py b/openpilot/cereal/services.py index f906e4610f..08633d6975 100755 --- a/openpilot/cereal/services.py +++ b/openpilot/cereal/services.py @@ -26,7 +26,6 @@ _services: dict[str, tuple] = { "temperatureSensor": (True, 2., 200), "deviceState": (True, 2., 1), "chestnutState": (True, 10., 10), - "chestnutGpuState": (False, 10.), "touch": (True, 20., 1), "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment "controlsState": (True, 100., 10, QueueSize.MEDIUM), diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index c31418ee0d..de66decf5c 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -6,6 +6,8 @@ import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom from tinygrad.tensor import Tensor from tinygrad.device import Device +import usb1 +import struct import threading import time import numpy as np @@ -30,6 +32,7 @@ from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked +from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob @@ -70,14 +73,45 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. shouldStop=bool(stop)) -class ChestnutGpuState: - # SMU metrics are only accessible from modeld. +class ChestnutState: + # only modeld can access chestnut def __init__(self, pm: PubMaster, big: bool): self.pm = pm self.big = big self.valid = True self.sends = 0 self.metrics = {} + self._asm_usb = None + + def _close_asm_usb(self) -> None: + if self._asm_usb is not None: + self._asm_usb.close() + self._asm_usb = None + + def _open_asm_usb(self): + context = usb1.USBContext() + for vendor_id, product_id in CHESTNUT_USB_IDS: + if (handle := context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True)) is not None: + return handle + context.close() + + def _read_ina(self) -> tuple[int, int, bool]: + if "AMD" in Device._opened_devices and self._asm_usb is None: + try: + raw = Device["AMD"].iface.pci_dev.usb.usb.control_read(0xC0, 5) + return struct.unpack(' int: @@ -85,6 +119,8 @@ class ChestnutGpuState: return smu._send_msg(smu.smu_mod.PPSMC_MSG_GetPptLimit, 0, read_back_arg=True, timeout=100) def send(self) -> None: + msg = messaging.new_message('chestnutState') + state = msg.chestnutState self.sends += 1 if self.big and "AMD" in Device._opened_devices and self.sends % 100 == 1: try: @@ -106,14 +142,25 @@ class ChestnutGpuState: cloudlog.exception("chestnut state read failed") self.valid = False self.metrics.clear() - - msg = messaging.new_message('chestnutGpuState') - state = msg.chestnutGpuState if self.big: for k, v in self.metrics.items(): setattr(state, k, v) - msg.valid = self.big and self.valid - self.pm.send('chestnutGpuState', msg) + + asm_valid = False + try: + # ASM runs on USB-C power, these still read without a gpu + state.supplyVoltage, state.supplyCurrent, state.supplyFault = self._read_ina() + asm_valid = True + except Exception: + pass + if "AMD" in Device._opened_devices: + try: + state.pcieLtssm = Device["AMD"].iface.pci_dev.usb.read(0xB450, 1)[0] + except Exception: + pass + + msg.valid = asm_valid and (not self.big or self.valid) + self.pm.send('chestnutState', msg) class FrameMeta: @@ -280,13 +327,13 @@ def main(demo=False): cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting") # messaging - pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutGpuState"] if CHESTNUT else []) + pub_socks = ["modelV2", "drivingModelData", "cameraOdometry"] + (["chestnutState"] if CHESTNUT else []) pm = PubMaster(pub_socks) sm = SubMaster(["deviceState", "carState", "narrowRoadCameraState", "extrinsicsCalibration", "driverMonitoringState", "carControl", "lateralDelay"]) publish_state = PublishState() params = Params() - chestnut_state = ChestnutGpuState(pm, model.chestnut) if CHESTNUT else None + chestnut_state = ChestnutState(pm, model.chestnut) if CHESTNUT else None # setup filter to track dropped frames frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) @@ -395,7 +442,7 @@ def main(demo=False): mt1 = time.perf_counter() try: send_chestnut = (chestnut_state is not None and - run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutGpuState'].frequency) == 0) + run_count % round(ModelConstants.MODEL_RUN_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): diff --git a/openpilot/system/hardware/chestnut/monitoring.py b/openpilot/system/hardware/chestnut/monitoring.py deleted file mode 100644 index 829db99618..0000000000 --- a/openpilot/system/hardware/chestnut/monitoring.py +++ /dev/null @@ -1,124 +0,0 @@ -import struct -from contextlib import suppress - -import usb1 - -import openpilot.cereal.messaging as messaging -from openpilot.cereal.services import SERVICE_LIST -from openpilot.common.hardware.usb import CHESTNUT_USB_IDS - - -USB_TIMEOUT_MS = 100 -PCIE_LTSSM_ADDRESS = 0xB450 - - -class ChestnutUsb: - def __init__(self): - self.context: usb1.USBContext | None = None - self.handle = None - - def close(self) -> None: - handle, context = self.handle, self.context - self.handle = None - self.context = None - with suppress(Exception): - if handle is not None: - handle.close() - with suppress(Exception): - if context is not None: - context.close() - - def connect(self) -> bool: - if self.handle is not None: - return True - - context = usb1.USBContext() - for vendor_id, product_id in CHESTNUT_USB_IDS: - handle = context.openByVendorIDAndProductID(vendor_id, product_id, skip_on_error=True) - if handle is not None: - self.context = context - self.handle = handle - return True - context.close() - return False - - def _read(self, request: int, value: int, length: int) -> bytes: - if self.handle is None: - raise usb1.USBErrorNoDevice - raw = bytes(self.handle.controlRead(0xC0, request, value, 0, length, timeout=USB_TIMEOUT_MS)) - if len(raw) != length: - raise ValueError(f"short chestnut USB response: {len(raw)}/{length}") - return raw - - def read_ina(self) -> tuple[int, int, bool]: - return struct.unpack(' int: - return self._read(0xE4, PCIE_LTSSM_ADDRESS, 1)[0] - - -class ChestnutMonitoring: - def __init__(self, usb: ChestnutUsb | None = None): - self.usb = usb or ChestnutUsb() - self.gpu_state = None - self.seen = False - self.enabled = False - self.usb_failed = False - - def set_enabled(self, enabled: bool) -> None: - if self.enabled == enabled: - return - self.enabled = enabled - self.usb.close() - self.usb_failed = False - - def retry(self) -> None: - self.usb_failed = False - - def model_alive(self, sm: messaging.SubMaster, now: float) -> bool: - modeld = next((p for p in sm['managerState'].processes if p.name == 'modeld'), None) - if modeld is not None and modeld.shouldBeRunning and not modeld.running: - return False - recv_time = sm.recv_time['chestnutGpuState'] - return recv_time > 0. and now - recv_time < 10. / SERVICE_LIST['chestnutGpuState'].frequency - - def update_gpu_state(self, sm: messaging.SubMaster, now: float) -> None: - if sm.updated['chestnutGpuState']: - self.gpu_state = sm['chestnutGpuState'] if sm.valid['chestnutGpuState'] else None - elif not self.model_alive(sm, now): - self.gpu_state = None - - def update(self, sm: messaging.SubMaster, now: float, model_loading: bool = False): - self.update_gpu_state(sm, now) - return self.build_message(model_loading) - - def build_message(self, model_loading: bool = False): - if not self.enabled: - return None - - msg = messaging.new_message('chestnutState') - if self.gpu_state is not None: - msg.chestnutState = self.gpu_state - state = msg.chestnutState - - if self.usb_failed: - return msg if self.seen else None - - try: - if not self.usb.connect(): - self.usb_failed = True - return msg if self.seen else None - - self.seen = True - voltage, current, fault = self.usb.read_ina() - pcie_ltssm = self.usb.read_pcie_ltssm() - state.supplyVoltage = voltage - state.supplyCurrent = current - state.supplyFault = fault - state.pcieLtssm = pcie_ltssm - msg.valid = True - except Exception as e: - if not model_loading or not isinstance(e, usb1.USBErrorTimeout): - self.usb.close() - self.usb_failed = True - return msg diff --git a/openpilot/system/hardware/chestnut/status.py b/openpilot/system/hardware/chestnut/status.py index 34456b2c11..c3321a3971 100644 --- a/openpilot/system/hardware/chestnut/status.py +++ b/openpilot/system/hardware/chestnut/status.py @@ -21,13 +21,14 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 + self.model_loading_seen = False self.model_attempted = False self.overheated = False self.usb_seen = False self.usb_failed = False def update(self, offroad: bool, branch: str, usb_state: list[dict], firmware_failed: bool, - model_active: bool | None, state, usb_failed: bool, set_alert) -> None: + model_loading: bool, model_active: bool | None, state, set_alert) -> None: detected = [d for d in usb_state if is_chestnut_usb_id(d["vendorId"], d["productId"], include_bootloader=True)] devices = [d for d in detected if is_chestnut_usb_id(d["vendorId"], d["productId"])] firmware_ok = len(devices) == 1 and devices[0]["product"] == CHESTNUT_USB_PRODUCT @@ -39,15 +40,16 @@ class ChestnutStatus: self.power_lost = False self.power_restored = False self.link_failures = 0 + self.model_loading_seen = False self.model_attempted = False self.usb_seen = firmware_ok self.usb_failed = False - self.model_attempted |= model_active is not None + self.model_loading_seen |= model_loading + self.model_attempted |= self.model_loading_seen and not model_loading and model_active is not None - if not offroad: - self.usb_seen |= firmware_ok - self.usb_failed = not offroad and self.usb_seen and (not firmware_ok or usb_failed) + if not offroad and self.usb_seen and not firmware_ok: + self.usb_failed = True if not offroad and state is not None: powered = state.supplyVoltage >= CHESTNUT_POWERED_VOLTAGE @@ -78,28 +80,17 @@ class ChestnutStatus: release = branch in CHESTNUT_RELEASE_BRANCHES missing = self.usb_failed or (offroad and release and time.monotonic() - self.started > 10. and len(detected) != 1) slow_usb = offroad and len(devices) == 1 and devices[0]["speedMbps"] < 5000 - update_failed = offroad and firmware_failed - compiled = firmware_ok and chestnut_compiled() - uncompiled = offroad and firmware_ok and not compiled - + set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1) + set_alert("Offroad_ChestnutNotDetected", missing) + set_alert("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None) + set_alert("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None) if self.power_lost: pcie_alert = ("Chestnut power restored. 12V is stable again, cycle ignition." if self.power_restored else "Chestnut power disconnected. Check 12V connection, then cycle ignition." if self.power_unavailable else "Chestnut power lost. Possibly caused by an engine-crank voltage drop. Check 12V connection, then cycle ignition.") else: pcie_alert = "Chestnut GPU unavailable. PCIe link is not up. Check the GPU is securely seated." - - alerts = ( - ("Offroad_ChestnutNotDetected", missing, None), - ("Offroad_ChestnutUpdateFailed", update_failed, None), - ("Offroad_ChestnutUncompiled", uncompiled, None), - ("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert), - ("Offroad_ChestnutOverheated", self.overheated, f"{state.tempC:.0f} °C" if state is not None else None), - ("Offroad_ChestnutUsbSlow", slow_usb, f"{devices[0]['speedMbps']} Mbps" if slow_usb else None), - ) - active_alert = next((name for name, active, _ in alerts if active), None) - - set_alert("Offroad_ChestnutBranch", not release and len(devices) == 1 and not missing) - for name, _, extra_text in alerts: - set_alert(name, name == active_alert, extra_text) + set_alert("Offroad_ChestnutPcieUnavailable", self.pcie_failed, pcie_alert) + set_alert("Offroad_ChestnutUncompiled", offroad and firmware_ok and not chestnut_compiled()) + set_alert("Offroad_ChestnutUpdateFailed", offroad and firmware_failed) self.offroad = offroad diff --git a/openpilot/system/hardware/hardwared.py b/openpilot/system/hardware/hardwared.py index c7d11fa5b0..11fe41400d 100755 --- a/openpilot/system/hardware/hardwared.py +++ b/openpilot/system/hardware/hardwared.py @@ -27,7 +27,6 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import FanController from openpilot.system.hardware.chestnut.status import ChestnutStatus -from openpilot.system.hardware.chestnut.monitoring import ChestnutMonitoring from openpilot.common.version import terms_version, training_version from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID @@ -196,9 +195,8 @@ def hw_state_thread(end_event, hw_queue): def hardware_thread(end_event, hw_queue) -> None: system_stats = LinuxSystemStats() - pm = messaging.PubMaster(['deviceState', 'chestnutState']) - sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", - "chestnutState", "chestnutGpuState", "managerState"], poll="pandaStates") + pm = messaging.PubMaster(['deviceState']) + sm = messaging.SubMaster(["peripheralState", "gpsLocationExternal", "selfdriveState", "pandaStates", "chestnutState"], poll="pandaStates") count = 0 @@ -246,9 +244,7 @@ def hardware_thread(end_event, hw_queue) -> None: fan_controller = FanController(int(1./DT_HW)) chestnut = Chestnut() - chestnut_monitoring = ChestnutMonitoring() chestnut_status = ChestnutStatus() - model_loading = params.get_bool("ChestnutLoading") branch = get_short_branch() while not end_event.is_set(): @@ -280,8 +276,6 @@ def hardware_thread(end_event, hw_queue) -> None: # Run at 2Hz, plus either edge of ignition ign_edge = (started_ts is not None) != all(onroad_conditions.values()) if (sm.frame % round(SERVICE_LIST['pandaStates'].frequency * DT_HW) != 0) and not ign_edge: - if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: - pm.send('chestnutState', chestnut_msg) continue msg = messaging.new_message('deviceState', valid=True) @@ -315,11 +309,9 @@ def hardware_thread(end_event, hw_queue) -> None: chestnut.update(started_ts is None, last_hw_state.usb_state) chestnut_state = sm["chestnutState"] chestnut_valid = sm.alive["chestnutState"] and sm.valid["chestnutState"] - model_loading = params.get_bool("ChestnutLoading") - model_active = params.get("ChestnutActive") chestnut_status.update(started_ts is None, branch, last_hw_state.usb_state, chestnut.failed, - model_active, chestnut_state if chestnut_valid else None, chestnut_monitoring.usb_failed, - set_offroad_alert_if_changed) + params.get_bool("ChestnutLoading"), params.get("ChestnutActive"), + chestnut_state if chestnut_valid else None, set_offroad_alert_if_changed) # this subset is only used for offroad temp_sources = [ msg.deviceState.memoryTempC, @@ -427,15 +419,6 @@ def hardware_thread(end_event, hw_queue) -> None: if off_ts is None: off_ts = time.monotonic() - chestnut_usb_ready = any(is_chestnut_usb_id(d["vendorId"], d["productId"]) and d["product"] == CHESTNUT_USB_PRODUCT - for d in last_hw_state.usb_state) - flash_active = chestnut.thread is not None and chestnut.thread.is_alive() - chestnut_monitoring.set_enabled(started_ts is not None and (chestnut_usb_ready or chestnut_monitoring.seen) and not flash_active) - if chestnut_usb_ready and chestnut_monitoring.usb_failed: - chestnut_monitoring.retry() - if (chestnut_msg := chestnut_monitoring.update(sm, time.monotonic(), model_loading)) is not None: - pm.send('chestnutState', chestnut_msg) - # Offroad power monitoring voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage power_monitor.calculate(voltage, onroad_conditions["ignition"]) From cb85ac1f0e78425752cf744b2ca2cd268720a337 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 1 Sep 2026 13:59:55 -0700 Subject: [PATCH 39/53] amd warp (#38684) * modeld: fuse warp and policy TinyJit * bump tg * fix? * this simple trick... * debug 1 * bump tg * pack all * wips * fix * BIG_INTO_SMALL remove * slower --- openpilot/selfdrive/modeld/SConscript | 24 ++- openpilot/selfdrive/modeld/compile_modeld.py | 147 ++++++++++-------- openpilot/selfdrive/modeld/modeld.py | 43 ++--- .../test/process_replay/model_replay.py | 10 +- 4 files changed, 108 insertions(+), 116 deletions(-) diff --git a/openpilot/selfdrive/modeld/SConscript b/openpilot/selfdrive/modeld/SConscript index f65aebf2f9..5a99aa890c 100644 --- a/openpilot/selfdrive/modeld/SConscript +++ b/openpilot/selfdrive/modeld/SConscript @@ -10,11 +10,6 @@ from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.modeld.helpers import TG_INPUT_DEVICES_PATH, chestnut_present, modeld_pkl_path -CAMERA_CONFIGS = [ - (_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208 - (_os_fisheye.width, _os_fisheye.height), # mici: 1344x760 -] - Import('env', 'arch') chunker_file = File("#openpilot/common/file_chunker.py") lenv = env.Clone() @@ -29,17 +24,17 @@ def estimate_pickle_max_size(onnx_size): return 2.0 * onnx_size + 10 * 1024 * 1024 if arch == 'comma_arm64': + from openpilot.common.hardware import HARDWARE + camera = _os_fisheye if HARDWARE.get_device_type() == "mici" else _ar_ox_fisheye + camera_configs = [(camera.width, camera.height)] tg_backend = 'QCOM' tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1' else: + camera_configs = [(c.width, c.height) for c in (_ar_ox_fisheye, _os_fisheye)] tg_backend = 'CPU' tg_flags = f'DEV=CPU' if arch == 'Darwin' else 'DEV=CPU:LLVM' tg_devices = { # which device to put jit inputs to at runtime - 'openpilot.selfdrive.modeld.modeld': { - 'default': {'WARP_DEV': tg_backend, 'QUEUE_DEV': tg_backend}, - 'chestnut': {'WARP_DEV': tg_backend, 'QUEUE_DEV': 'AMD'} - }, 'openpilot.selfdrive.modeld.dmonitoringmodeld': { 'default': {'DEV': tg_backend} }, @@ -47,7 +42,7 @@ tg_devices = { # which device to put jit inputs to at runtime CHESTNUT = chestnut_present() if CHESTNUT: - chestnut_tg_flags = f'DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2' + chestnut_tg_flags = 'DEBUG=1 DEV=USB+AMD:LLVM FRAME_DEV=CPU FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1' # the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it chestnut_lock = File("models/.chestnut.lock").abspath @@ -77,10 +72,9 @@ frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ for chestnut in [False, True] if CHESTNUT else [False]: target_pkl_path = File(modeld_pkl_path(chestnut)).abspath - # BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a chestnut - file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut else ('big_' if os.getenv('BIG_INTO_SMALL') else '', tg_flags) + file_prefix, cmd_flags = ('big_', chestnut_tg_flags) if chestnut 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) + camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in camera_configs) # CPU 7 is isolated with isolcpus on AGNOS, so explicitly pin the compiler to it. taskset = 'taskset -c 7 ' if arch == 'comma_arm64' else '' cmd = (f'{cmd_flags} {mac_brew_string} {taskset}python3 {modeld_dir}/compile_modeld.py ' @@ -108,7 +102,7 @@ for chestnut in [False, True] if CHESTNUT else [False]: actions = Action(do_compile, " [CHESTNUT] $TARGET") if chestnut else [cmd, Action(do_chunk, " [CHUNK] $TARGET")] node = lenv.Command( chunk_targets, - tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file], + tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(camera_res_args), Value(chunk_targets), chunker_file], actions, ) if chestnut: @@ -122,7 +116,7 @@ lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_file dm_w, dm_h = DM_INPUT_SIZE compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")] -for cam_w, cam_h in CAMERA_CONFIGS: +for cam_w, cam_h in camera_configs: dm_pkl_path = File(f"models/dm_warp_{cam_w}x{cam_h}_tinygrad.pkl").abspath cmd = (f'{tg_flags} {mac_brew_string} python3 {modeld_dir}/compile_dm_warp.py ' f'--camera-resolution {cam_w}x{cam_h} --warp-to {dm_w}x{dm_h} ' diff --git a/openpilot/selfdrive/modeld/compile_modeld.py b/openpilot/selfdrive/modeld/compile_modeld.py index f54d09e869..d851c3cc86 100755 --- a/openpilot/selfdrive/modeld/compile_modeld.py +++ b/openpilot/selfdrive/modeld/compile_modeld.py @@ -37,17 +37,12 @@ from tinygrad.engine.jit import TinyJit NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size']) -WARP_INPUTS = ['tfm', 'big_tfm'] -POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] - -UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32) -UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX) - -WARP_DEV = os.getenv('WARP_DEV') +MODELD_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] -def make_random_images(keys, shape, device=None): - return {k: Tensor.randint(shape, low=0, high=256, dtype='uint8', device=device).realize() for k in keys} +def nv12_copy_size(stride: int, y_height: int, uv_height: int) -> int: + # Retain the padded Y and UV plane storage, but skip the trailing kernel/guard allocation. + return stride * (y_height + uv_height) def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad, border_fill_val=None): @@ -99,7 +94,7 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): def frame_prepare_tinygrad(input_frame, M_inv): # UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling - M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=WARP_DEV) + M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]], device=Device.DEFAULT) # deinterleave NV12 UV plane (UVUV... -> separate U, V) uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride) with Context(SPLIT_REDUCEOP=0): @@ -118,23 +113,6 @@ def make_frame_prepare(nv12: NV12Frame, model_w, model_h): return frame_prepare_tinygrad -def make_warp_input_queues(vision_input_shapes, frame_skip, device): - img = vision_input_shapes['img'] # (1, 12, 128, 256) - n_frames = img[1] // 6 - img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) - - npy = { - 'tfm': np.zeros((3, 3), dtype=np.float32), - 'big_tfm': np.zeros((3, 3), dtype=np.float32), - } - input_queues = { - 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), - **{k: Tensor(v, device='NPY').realize() for k, v in npy.items()}, - } - return input_queues, npy - - def get_policy_npy_shapes(input_shapes): dp = input_shapes['desire_pulse'] # (1, 25, 8) tc = input_shapes['traffic_convention'] # (1, 2) @@ -146,23 +124,32 @@ def get_policy_npy_shapes(input_shapes): return shapes, [math.prod(s) for s in shapes.values()] -def make_input_queues(input_shapes, frame_skip, device): - input_queues, npy = make_warp_input_queues(input_shapes, frame_skip, device) - +def make_input_queues(input_shapes, frame_skip, device, frame_copy_size): + img = input_shapes['img'] # (1, 12, 128, 256) fb = input_shapes['features_buffer'] # (1, T-1, ...), past features only; the model appends the current frame's feature feat_dim = math.prod(fb[2:]) dp = input_shapes['desire_pulse'] # (1, 25, 8) + n_frames = img[1] // 6 + img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3]) - shapes, sizes = get_policy_npy_shapes(input_shapes) - packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32) + policy_shapes, _ = get_policy_npy_shapes(input_shapes) + shapes = {'tfm': (3, 3), 'big_tfm': (3, 3)} | policy_shapes + sizes = [math.prod(s) for s in shapes.values()] + packed_npy_size = sum(sizes) * np.dtype(np.float32).itemsize + packed_input = np.zeros(packed_npy_size + 2 * frame_copy_size, dtype=np.uint8) + packed_npy_inputs = packed_input[:packed_npy_size].view(np.float32) + frames = packed_input[packed_npy_size:] + frame_views = {'img': frames[:frame_copy_size], 'big_img': frames[frame_copy_size:]} # views into the packed inputs, to be refilled at runtime - npy.update({k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)}) - input_queues.update({ + npy = {k: v.reshape(s) for (k, s), v in zip(shapes.items(), np.split(packed_npy_inputs, np.cumsum(sizes[:-1])), strict=True)} + input_queues = { + 'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), + 'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(), 'feat_q': Tensor(np.zeros((frame_skip * fb[1], fb[0], feat_dim), dtype=np.float32), device=device).contiguous().realize(), 'desire_q': Tensor(np.zeros((frame_skip * dp[1], dp[0], dp[2]), dtype=np.float32), device=device).contiguous().realize(), - 'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(), - }) - return input_queues, npy + 'packed_npy_inputs': Tensor(packed_input, device='NPY').realize(), + } + return input_queues, npy, frame_views def shift_and_sample(buf, new_val, sample_fn): @@ -178,13 +165,15 @@ def sample_desire(buf, frame_skip): return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0) -def make_warp(nv12, model_w, model_h, frame_skip): +def make_warp(nv12, model_w, model_h): frame_prepare = make_frame_prepare(nv12, model_w, model_h) def warp(tfm, big_tfm, frame, big_frame): - tfm = tfm.to(WARP_DEV) - big_tfm = big_tfm.to(WARP_DEV) - Tensor.realize(tfm, big_tfm) + tfm = tfm.to(Device.DEFAULT) + big_tfm = big_tfm.to(Device.DEFAULT) + frame = frame.to(Device.DEFAULT) + big_frame = big_frame.to(Device.DEFAULT) + Tensor.realize(tfm, big_tfm, frame, big_frame) warped_frame = frame_prepare(frame, tfm).unsqueeze(0) warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) @@ -197,10 +186,10 @@ def make_run_policy(model_runner, model_metadata, frame_skip): sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) npy_shapes, npy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) + model_input_dtypes = {name: spec.dtype for name, spec in model_runner.graph_inputs.items()} def run_policy(warped, img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): packed_npy_inputs = packed_npy_inputs.to(Device.DEFAULT) - warped = warped.to(Device.DEFAULT) Tensor.realize(packed_npy_inputs, warped) img = shift_and_sample(img_q, warped[0:1], sample_skip_fn) @@ -218,28 +207,45 @@ def make_run_policy(model_runner, model_metadata, frame_skip): 'traffic_convention': traffic_convention, 'action_t': action_t, } + inputs = {name: value.cast(model_input_dtypes[name]) for name, value in inputs.items()} out = next(iter(model_runner(inputs).values())).cast('float32') return out, return run_policy -def compile_jit(jit, make_random_inputs, input_keys, make_queues): - SEED = 42 - def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): - input_queues, npy = make_queues(Device.DEFAULT) - rng = np.random.default_rng(seed) - Tensor.manual_seed(seed) +def make_run_model(warp, run_policy, model_metadata, frame_copy_size): + _, policy_sizes = get_policy_npy_shapes(model_metadata['input_shapes']) + packed_npy_size = (18 + sum(policy_sizes)) * np.dtype(np.float32).itemsize - testing = test_val is not None or test_buffers is not None - n_runs = 1 if testing else 3 + def run_model(img_q, big_img_q, feat_q, desire_q, packed_npy_inputs): + packed_input = packed_npy_inputs.to(Device.DEFAULT) + Tensor.realize(packed_input) + packed_npy_inputs = packed_input[:packed_npy_size].bitcast('float32') + frame = packed_input[packed_npy_size:packed_npy_size + frame_copy_size] + big_frame = packed_input[packed_npy_size + frame_copy_size:] + tfm, big_tfm, policy_inputs = packed_npy_inputs.split([9, 9, sum(policy_sizes)]) + warped = warp(tfm.reshape(3, 3), big_tfm.reshape(3, 3), frame, big_frame) + return run_policy(warped, img_q, big_img_q, feat_q, desire_q, policy_inputs) + return run_model + + +def compile_jit(jit, input_keys, make_queues, benchmark_runs): + if benchmark_runs < 1: + raise ValueError("benchmark_runs must be at least 1") + + SEED = 42 + def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True): + input_queues, npy, frame_views = make_queues(Device.DEFAULT) + rng = np.random.default_rng(seed) for i in range(n_runs): for v in npy.values(): v[:] = rng.standard_normal(v.shape).astype(v.dtype) + for v in frame_views.values(): + v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8) Device.default.synchronize() - random_inputs = make_random_inputs() st = time.perf_counter() - outs = fn(**{k: input_queues[k] for k in input_keys}, **random_inputs) + outs = fn(**{k: input_queues[k] for k in input_keys}) mt = time.perf_counter() Device.default.synchronize() et = time.perf_counter() @@ -258,14 +264,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues): return val, buffers print('capture + replay') - test_val, test_buffers = random_inputs_run(jit, SEED) - print('pickle round trip') + test_val, test_buffers = random_inputs_run(jit, SEED, 3) + print(f'pickle round trip ({benchmark_runs} runs per seed)') with tempfile.TemporaryFile(dir=".") as f: dump_oob(jit, f) f.seek(0) - jit = load_oob(f) - random_inputs_run(jit, SEED, test_val, test_buffers, expect_match=True) - random_inputs_run(jit, SEED+1, test_val, test_buffers, expect_match=False) + loaded_jit = load_oob(f) + random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True) + random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False) + # Keep the original so per-resolution JITs share model weight buffers in the final pickle. return jit @@ -294,27 +301,31 @@ if __name__ == "__main__": p.add_argument('--onnx', required=True) p.add_argument('--output', required=True) p.add_argument('--frame-skip', type=int, required=True) + p.add_argument('--benchmark-runs', type=int, default=1, + help='timed loaded-JIT runs for each correctness seed') args = p.parse_args() model_path = read_file_chunked_to_disk(args.onnx) model_w, model_h = args.model_size model_runner = OnnxRunner(model_path) - out = {'metadata': make_metadata_dict(model_path)} + out = { + 'metadata': make_metadata_dict(model_path), + 'input_devices': {'model': Device.DEFAULT}, + 'run_model': {}, + } - run_policy_jit = TinyJit(make_run_policy(model_runner, out['metadata'], args.frame_skip), prune=True) - - make_policy_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip) - make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, *out['metadata']['input_shapes']['img'][2:]), device=WARP_DEV) - out['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, - make_policy_queues) + run_policy = make_run_policy(model_runner, out['metadata'], args.frame_skip) for cam_w, cam_h in args.camera_resolutions: nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) - make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) - warp = TinyJit(make_warp(nv12, model_w, model_h, args.frame_skip), prune=True) - make_warp_queues = partial(make_warp_input_queues, out['metadata']['input_shapes'], args.frame_skip) - out[(cam_w,cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + frame_copy_size = nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height) + make_model_queues = partial(make_input_queues, out['metadata']['input_shapes'], args.frame_skip, + frame_copy_size=frame_copy_size) + warp = make_warp(nv12, model_w, model_h) + run_model_jit = TinyJit(make_run_model(warp, run_policy, out['metadata'], frame_copy_size), prune=True) + out['run_model'][(cam_w,cam_h)] = compile_jit(run_model_jit, MODELD_INPUTS, make_model_queues, + args.benchmark_runs) with open(args.output, "wb") as f: dump_oob(out, f) diff --git a/openpilot/selfdrive/modeld/modeld.py b/openpilot/selfdrive/modeld/modeld.py index de66decf5c..040c6b980f 100755 --- a/openpilot/selfdrive/modeld/modeld.py +++ b/openpilot/selfdrive/modeld/modeld.py @@ -4,7 +4,6 @@ import ctypes from functools import cached_property import os os.environ['GMMU'] = '0' # for chestnut fast loading, noop for qcom -from tinygrad.tensor import Tensor from tinygrad.device import Device import usb1 import struct @@ -29,14 +28,13 @@ from openpilot.common.transformations.model import get_warp_matrix from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser -from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS +from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, nv12_copy_size, MODELD_INPUTS from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState from openpilot.common.file_chunker import open_file_chunked from openpilot.common.hardware.usb import CHESTNUT_USB_IDS from openpilot.selfdrive.modeld.constants import ModelConstants, Plan -from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, get_tg_input_devices, load_oob +from openpilot.selfdrive.modeld.helpers import chestnut_present, chestnut_compiled, chestnut_ready, modeld_pkl_path, load_oob -PROCESS_NAME = "openpilot.selfdrive.modeld.modeld" SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') LAT_SMOOTH_SECONDS = 0.0 @@ -177,9 +175,9 @@ class ModelState: prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, cam_w: int, cam_h: int, chestnut: bool): - input_devices = get_tg_input_devices(PROCESS_NAME, chestnut) - self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV'] jits = load_oob(open_file_chunked(modeld_pkl_path(chestnut))) + input_devices = jits['input_devices'] + self.model_device = input_devices['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] @@ -189,13 +187,11 @@ class ModelState: self.chestnut = chestnut 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.frame_copy_size = nv12_copy_size(*get_nv12_info(cam_w, cam_h)[:3]) + self.input_queues, self.npy, self.frame_views = make_input_queues( + self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size) 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'] - self.warp = jits[(cam_w,cam_h)] + self.run_model = jits['run_model'][(cam_w,cam_h)] def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]: parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()} @@ -203,14 +199,8 @@ class ModelState: def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> dict[str, np.ndarray]: - 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] + for key, buf in bufs.items(): + np.copyto(self.frame_views[key], np.frombuffer(buf.data, dtype=np.uint8, count=self.frame_copy_size)) # Model decides when action is completed, so desire input is just a pulse triggered on rising edge inputs['desire_pulse'][0] = 0 @@ -221,11 +211,7 @@ class ModelState: self.npy['tfm'][:,:] = transforms['img'][:,:] self.npy['big_tfm'][:,:] = transforms['big_img'][:,:] - warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']) - - outs, = self.run_policy( - **{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped - ) + outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS}) if after_enqueue is not None: after_enqueue() model_output = outs.numpy()[0] @@ -239,14 +225,13 @@ class ModelState: return outputs_dict def warmup(self) -> None: - dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names} + dummy_frames = {k: np.zeros(self.frame_copy_size, dtype=np.uint8) for k in self.vision_input_names} eye = np.eye(3, dtype=np.float32) dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2} self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()}) - self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.input_queues, self.npy, self.frame_views = make_input_queues( + self.input_shapes, self.frame_skip, device=self.model_device, frame_copy_size=self.frame_copy_size) self.prev_desire[:] = 0 - self.full_frames.clear() - self._blob_cache.clear() def main(demo=False): diff --git a/openpilot/selfdrive/test/process_replay/model_replay.py b/openpilot/selfdrive/test/process_replay/model_replay.py index ba610c6a2b..927c9b38f1 100755 --- a/openpilot/selfdrive/test/process_replay/model_replay.py +++ b/openpilot/selfdrive/test/process_replay/model_replay.py @@ -33,9 +33,9 @@ MODEL_REPLAY_BUCKET="model_replay_master" GITHUB = GithubUtils(API_TOKEN, DATA_TOKEN) EXEC_TIMINGS = [ - # model, instant max, average max - ("modelV2", 0.05, 0.028), - ("driverStateV2", 0.05, 0.018), + # model, instant max, average max, chestnut average max + ("modelV2", 0.05, 0.03, 0.05), + ("driverStateV2", 0.05, 0.018, 0.018), ] def get_log_fn(test_route, ref="master"): @@ -169,11 +169,13 @@ def model_replay(lr, frs): dmonitoringmodeld_msgs = replay_process(dmonitoringmodeld, dmodeld_logs, frs) msgs = modeld_msgs + dmonitoringmodeld_msgs + chestnut = any(m.modelV2.big for m in modeld_msgs if m.which() == "modelV2") header = ['model', 'max instant', 'max instant allowed', 'average', 'max average allowed', 'test result'] rows = [] timings_ok = True - for (s, instant_max, avg_max) in EXEC_TIMINGS: + for (s, instant_max, avg_max, chestnut_avg_max) in EXEC_TIMINGS: + avg_max = chestnut_avg_max if chestnut else avg_max ts = [getattr(m, s).modelExecutionTime for m in msgs if m.which() == s] # TODO some init can happen in first iteration ts = ts[1:] From 36561258facd79875b58567f8b72ef751810ae3a Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 15:45:57 -0700 Subject: [PATCH 40/53] ui: show usb connection (#38745) * ui: show USB status * ui: resize USB icon * ui: classify USB device once * ui: debounce USB disconnect --- openpilot/selfdrive/assets/icons_mici/usb.png | 3 +++ openpilot/selfdrive/ui/mici/layouts/home.py | 6 ++++- openpilot/selfdrive/ui/ui_state.py | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 openpilot/selfdrive/assets/icons_mici/usb.png diff --git a/openpilot/selfdrive/assets/icons_mici/usb.png b/openpilot/selfdrive/assets/icons_mici/usb.png new file mode 100644 index 0000000000..2f3afb0de9 --- /dev/null +++ b/openpilot/selfdrive/assets/icons_mici/usb.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07bda2fe5d6be0b2854044053c384fe002e96406da119863a443b9344258b500 +size 1544 diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index b567b405a8..30d4a19451 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -140,6 +140,7 @@ class MiciHomeLayout(Widget): self._version_text = self._get_version_text() self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48)) + self._usb_icon = IconWidget("icons_mici/usb.png", (62, 40)) self._chestnut_icon = IconWidget("icons_mici/chestnut_green.png", (68, 40)) self._chestnut_loading_icon = IconWidget("icons_mici/chestnut.png", (68, 40)) self._chestnut_failed_icon = IconWidget("icons_mici/chestnut_orange.png", (68, 40)) @@ -152,6 +153,7 @@ class MiciHomeLayout(Widget): IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9), NetworkIcon(), self._experimental_icon, + self._usb_icon, self._chestnut_icon, self._chestnut_loading_icon, self._chestnut_failed_icon, @@ -251,7 +253,9 @@ class MiciHomeLayout(Widget): # ***** Center-aligned bottom section icons ***** self._experimental_icon.set_visible(ui_state.experimental_mode) - self._chestnut_icon.set_visible(ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE)) + self._usb_icon.set_visible(ui_state.usb_connected and ui_state.usb_unknown) + self._chestnut_icon.set_visible(not ui_state.usb_unknown and (ui_state.usb_connected or + ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) diff --git a/openpilot/selfdrive/ui/ui_state.py b/openpilot/selfdrive/ui/ui_state.py index c169cbeaff..ae1fad0b66 100644 --- a/openpilot/selfdrive/ui/ui_state.py +++ b/openpilot/selfdrive/ui/ui_state.py @@ -12,6 +12,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState from openpilot.system.ui.lib.application import gui_app from openpilot.common.hardware import HARDWARE, PC +from openpilot.common.hardware.usb import TYPEC_CC_ORIENTATION_PATH, get_usb_state, is_chestnut_usb_id, read_int from openpilot.selfdrive.modeld.helpers import chestnut_compiled BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50 @@ -90,6 +91,10 @@ class UIState: self.chestnut_compiled: bool = chestnut_compiled() self.chestnut_active: bool | None = None self.chestnut_loading: bool = False + self.usb_connected: bool = False + self.usb_connected_ts: float | None = None + self.usb_disconnected_ts: float | None = None + self.usb_unknown: bool = False self.chestnut_state = ChestnutState.DISCONNECTED self.started: bool = False self.ignition: bool = False @@ -246,6 +251,23 @@ class UIState: self.chestnut_compiled = chestnut_compiled() self.chestnut_active = self.params.get("ChestnutActive") self.chestnut_loading = self.params.get_bool("ChestnutLoading") + now = time.monotonic() + if read_int(TYPEC_CC_ORIENTATION_PATH) != 0: + self.usb_disconnected_ts = None + if not self.usb_connected: + self.usb_connected = True + self.usb_connected_ts = now + self.usb_unknown = False + elif self.usb_connected_ts is not None and now - self.usb_connected_ts > 10.: + self.usb_unknown = not any(is_chestnut_usb_id(d["vendorId"], d["productId"], True) for d in get_usb_state()) + self.usb_connected_ts = None + elif self.usb_connected: + if self.usb_disconnected_ts is None: + self.usb_disconnected_ts = now + elif now - self.usb_disconnected_ts > PARAM_UPDATE_TIME: + self.usb_connected = False + self.usb_connected_ts = None + self.usb_unknown = False class Device: From 79658800ce142a84009e83a7f98e1c5d3415ceee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Tue, 1 Sep 2026 17:15:13 -0700 Subject: [PATCH 41/53] cereal: log big model in drivingModelData (#38747) --- openpilot/cereal/log.capnp | 1 + openpilot/selfdrive/modeld/fill_model_msg.py | 1 + openpilot/selfdrive/test/process_replay/migration.py | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/openpilot/cereal/log.capnp b/openpilot/cereal/log.capnp index fb4867b2a6..177a220451 100644 --- a/openpilot/cereal/log.capnp +++ b/openpilot/cereal/log.capnp @@ -1005,6 +1005,7 @@ struct DrivingModelData { frameIdExtra @1 :UInt32; frameDropPerc @6 :Float32; modelExecutionTime @7 :Float32; + big @8 :Bool; action @2 :ModelDataV2.Action; diff --git a/openpilot/selfdrive/modeld/fill_model_msg.py b/openpilot/selfdrive/modeld/fill_model_msg.py index 558f881b37..055c182f6c 100644 --- a/openpilot/selfdrive/modeld/fill_model_msg.py +++ b/openpilot/selfdrive/modeld/fill_model_msg.py @@ -63,6 +63,7 @@ def fill_driving_model_data(msg: capnp._DynamicStructBuilder, modelv2_send: capn driving_model_data.frameIdExtra = modelV2.frameIdExtra driving_model_data.frameDropPerc = modelV2.frameDropPerc driving_model_data.modelExecutionTime = modelV2.modelExecutionTime + driving_model_data.big = modelV2.big driving_model_data.action = modelV2.action driving_model_data.meta.laneChangeState = modelV2.meta.laneChangeState driving_model_data.meta.laneChangeDirection = modelV2.meta.laneChangeDirection diff --git a/openpilot/selfdrive/test/process_replay/migration.py b/openpilot/selfdrive/test/process_replay/migration.py index 2fb8932abc..ee1fb3b943 100644 --- a/openpilot/selfdrive/test/process_replay/migration.py +++ b/openpilot/selfdrive/test/process_replay/migration.py @@ -149,7 +149,7 @@ def migrate_drivingModelData(msgs): add_ops = [] for _, msg in msgs: dmd = messaging.new_message('drivingModelData', valid=msg.valid, logMonoTime=msg.logMonoTime) - for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "action"]: + for field in ["frameId", "frameIdExtra", "frameDropPerc", "modelExecutionTime", "big", "action"]: setattr(dmd.drivingModelData, field, getattr(msg.modelV2, field)) for meta_field in ["laneChangeState", "laneChangeState"]: setattr(dmd.drivingModelData.meta, meta_field, getattr(msg.modelV2.meta, meta_field)) From 8b88f7dd6e0fe50985e27676370dc71145679d23 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 18:32:39 -0700 Subject: [PATCH 42/53] ui: show one GPU status (#38748) ui: show one GPU status icon --- openpilot/selfdrive/ui/mici/layouts/home.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/openpilot/selfdrive/ui/mici/layouts/home.py b/openpilot/selfdrive/ui/mici/layouts/home.py index 30d4a19451..553fa37f8c 100644 --- a/openpilot/selfdrive/ui/mici/layouts/home.py +++ b/openpilot/selfdrive/ui/mici/layouts/home.py @@ -252,13 +252,17 @@ class MiciHomeLayout(Widget): self._version_commit_label.render() # ***** Center-aligned bottom section icons ***** + usb_connected = ui_state.usb_connected + usb_unknown = ui_state.usb_unknown + chestnut_state = ui_state.chestnut_state self._experimental_icon.set_visible(ui_state.experimental_mode) - self._usb_icon.set_visible(ui_state.usb_connected and ui_state.usb_unknown) - self._chestnut_icon.set_visible(not ui_state.usb_unknown and (ui_state.usb_connected or - ui_state.chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) - self._chestnut_loading_icon.set_visible(ui_state.chestnut_state == ChestnutState.LOADING) + self._usb_icon.set_visible(usb_connected and usb_unknown) + self._chestnut_icon.set_visible(not usb_unknown and chestnut_state not in + (ChestnutState.LOADING, ChestnutState.UNCOMPILED, ChestnutState.FAILED) and + (usb_connected or chestnut_state in (ChestnutState.READY, ChestnutState.ACTIVE))) + self._chestnut_loading_icon.set_visible(not usb_unknown and chestnut_state == ChestnutState.LOADING) self._chestnut_loading_icon.set_opacity(0.35 + 0.65 * (0.5 - 0.5 * math.cos(rl.get_time() * 6.0))) - self._chestnut_failed_icon.set_visible(ui_state.chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) + self._chestnut_failed_icon.set_visible(not usb_unknown and chestnut_state in (ChestnutState.UNCOMPILED, ChestnutState.FAILED)) self._mic_icon.set_visible(ui_state.recording_audio) self._body_icon.set_visible(bool(ui_state.is_body)) From 6249f4d5b0e63c05f08bce12ca3afebda9f764a3 Mon Sep 17 00:00:00 2001 From: Daniel Koepping Date: Tue, 1 Sep 2026 18:32:59 -0700 Subject: [PATCH 43/53] AGNOS 19.7 (#38750) --- launch_env.sh | 2 +- openpilot/common/hardware/comma/agnos.json | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 094622005a..4fcb243f12 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -16,7 +16,7 @@ export VECLIB_MAXIMUM_THREADS=1 export QCOM_PRIORITY=12 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="19.6" + export AGNOS_VERSION="19.7" fi export STAGING_ROOT="/data/safe_staging" diff --git a/openpilot/common/hardware/comma/agnos.json b/openpilot/common/hardware/comma/agnos.json index ae1cbbcd38..4a48016ed2 100644 --- a/openpilot/common/hardware/comma/agnos.json +++ b/openpilot/common/hardware/comma/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd.img.xz", - "hash": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", - "hash_raw": "b30f5eef65ec3878f3aa3dcaf2cc95c09e2c1e661cd3a38e94da37dee76f68bd", + "url": "https://commadist.azureedge.net/agnosupdate/boot-6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d.img.xz", + "hash": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", + "hash_raw": "6ecf6f987cd11968104abcccabbe268485d329cdb73012dfd3c381a6b8deb27d", "size": 46897152, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "6650e4c46df99ae6dfd6ee895a34b8a2a3cc490a8ce18e16cc3c451c3f822b6e" + "ondevice_hash": "d12e1e5b9455b62a1464558716493b33e470d7a7e88da1c4105a3b21d0961808" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img.xz", - "hash": "b134fd04e9da27fa1d359ea0f2742c216fa21a08b5c47e9be22ab3b0563d9b9b", - "hash_raw": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", + "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img.xz", + "hash": "74ffc9c551e1f29cda897ace8a69080fe644f8039977c6885f2b48362e39b744", + "hash_raw": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "91242772af771ae96fe2eebc105f2b80a7e1dbaaf6003c2574b62d51b806f468", + "ondevice_hash": "6a992680183685eea9db99d915219a37935f45989330d9b619e880450257f448", "alt": { - "hash": "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3", - "url": "https://commadist.azureedge.net/agnosupdate/system-5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3.img", + "hash": "3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f", + "url": "https://commadist.azureedge.net/agnosupdate/system-3c271e2b3d20d2f0a8bf6555a1319f3efb12845490967d6151195174a01e912f.img", "size": 4718592000 } } -] +] \ No newline at end of file From ab389498a8808e8ea82808adabe355af7475b749 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 22:12:54 -0400 Subject: [PATCH 44/53] [bot] Update Python packages (#1950) * Update Python packages * bump tg * bump * ci: route build_model runner by target_hardware instead of hardcoding chestnut * hack, remove before merge * Revert build-model runner hack and uv.lock update * why were they hard coded --------- Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- openpilot/sunnypilot/models/fetcher.py | 4 ++-- openpilot/sunnypilot/models/helpers.py | 2 +- .../sunnypilot/models/tests/test_manager_download.py | 12 ++++++------ tinygrad_repo | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index d0115be045..f997dbbc51 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -138,8 +138,8 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v21.json" - MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v22.json" + MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json" + MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json" MODEL_SOURCES = { "qcom": (MODEL_URL, ""), diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index 3c3cc1107b..ac69096597 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.common.hardware.hw import Paths from openpilot.selfdrive.modeld.helpers import chestnut_present # SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO -REQUIRED_JSON_VERSION = 18 +REQUIRED_JSON_VERSION = 19 CUSTOM_MODEL_PATH = Paths.model_root() METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' diff --git a/openpilot/sunnypilot/models/tests/test_manager_download.py b/openpilot/sunnypilot/models/tests/test_manager_download.py index 489e0bc096..b995d4fbd1 100644 --- a/openpilot/sunnypilot/models/tests/test_manager_download.py +++ b/openpilot/sunnypilot/models/tests/test_manager_download.py @@ -386,7 +386,7 @@ class TestManagerDownload(ManagerDownloadTestBase): def body(): artifact = self.make_artifact(chunked=True) self._bundle.ref = "test-ref" - self._bundle.minimumSelectorVersion = 18 + self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION params, store = self._make_params_with_store() self.manager.params = params asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "qcom")) @@ -406,7 +406,7 @@ class TestManagerDownload(ManagerDownloadTestBase): def body(): self.make_artifact(chunked=True) self._bundle.ref = "big-ref" - self._bundle.minimumSelectorVersion = 18 + self._bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION params, store = self._make_params_with_store() self.manager.params = params asyncio.run(self.manager._download_bundle(self._bundle, self.dest, "chestnut")) @@ -469,7 +469,7 @@ def manifest_bundle(short_name: str, ref: str, index: int = 0, is_big: bool = Fa "environment": "release", "runner": "tinygrad", "is_big": is_big, - "minimum_selector_version": "18", + "minimum_selector_version": str(helpers.REQUIRED_JSON_VERSION), "ref": ref, "models": [{ "type": "supercombo", @@ -655,7 +655,7 @@ class TestActiveBundleValidation(OpenpilotTestCase): def _raw_bundle(ref: str, runner: int | None = None) -> dict: bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle.ref = ref - bundle.minimumSelectorVersion = 18 + bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION if runner is not None: bundle.runner = runner return bundle.to_dict() @@ -697,7 +697,7 @@ class TestActiveBundleSelection(OpenpilotTestCase): def _raw_bundle(ref: str) -> dict: bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle.ref = ref - bundle.minimumSelectorVersion = 18 + bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION return bundle.to_dict() def _params(self, qcom=None, chestnut=None): @@ -744,7 +744,7 @@ class TestEffectiveSource(OpenpilotTestCase): def _raw_bundle(ref: str) -> dict: bundle = custom.ModelManagerSP.ModelBundle.new_message() bundle.ref = ref - bundle.minimumSelectorVersion = 18 + bundle.minimumSelectorVersion = helpers.REQUIRED_JSON_VERSION return bundle.to_dict() def test_runtime_no_gpu(self): diff --git a/tinygrad_repo b/tinygrad_repo index 66ee3cfb4f..966a8f5112 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 66ee3cfb4f3a3908a6a20ddfbec7774ba7c09b4e +Subproject commit 966a8f5112dbc0e4f6d8120c1ec4d98e95fa2bcb From 68be7773955e2a0dd7901ea604ba3301bed13daf Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 1 Sep 2026 22:14:43 -0400 Subject: [PATCH 45/53] bump tg --- tinygrad_repo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tinygrad_repo b/tinygrad_repo index 966a8f5112..e837e367aa 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit 966a8f5112dbc0e4f6d8120c1ec4d98e95fa2bcb +Subproject commit e837e367aac9e1a66e689f4f32ce20ca9367df13 From 47db84ebfb47f82bfe3ebb3d78cb13d4bc9a91a3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 2 Sep 2026 01:28:43 -0400 Subject: [PATCH 46/53] models: add big model ONNX hash tracking (#1982) --- openpilot/sunnypilot/models/default_model.py | 14 ++++++++++ .../sunnypilot/models/tests/big_model_hash | 1 + .../models/tests/test_default_model.py | 27 ++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 openpilot/sunnypilot/models/tests/big_model_hash diff --git a/openpilot/sunnypilot/models/default_model.py b/openpilot/sunnypilot/models/default_model.py index e3c9360835..84e962cdcd 100755 --- a/openpilot/sunnypilot/models/default_model.py +++ b/openpilot/sunnypilot/models/default_model.py @@ -20,7 +20,9 @@ def get_default_model() -> str: DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "model_name.py") MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "model_hash") +BIG_MODEL_HASH_PATH = os.path.join(BASEDIR, "openpilot", "sunnypilot", "models", "tests", "big_model_hash") SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "driving_supercombo.onnx") +BIG_SUPERCOMBO_ONNX_PATH = os.path.join(BASEDIR, "openpilot", "selfdrive", "modeld", "models", "big_driving_supercombo.onnx") def update_model_hash(): @@ -32,6 +34,18 @@ def update_model_hash(): print(f"Generated and updated new combined model hash to {MODEL_HASH_PATH}") + if os.path.exists(BIG_SUPERCOMBO_ONNX_PATH): + import subprocess + rel = os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd()) + pointer = subprocess.check_output(["git", "show", f"HEAD:{rel}"], text=True) + oid = next(l.split(":", 1)[1] for l in pointer.splitlines() if l.startswith("oid sha256:")) + big_combined_hash = hashlib.sha256(oid.encode()).hexdigest() + + with open(BIG_MODEL_HASH_PATH, "w") as f: + f.write(big_combined_hash) + + print(f"Generated and updated new big model hash to {BIG_MODEL_HASH_PATH}") + def get_ref_for_name(url: str, name: str) -> str: response = requests.get(url, timeout=10) diff --git a/openpilot/sunnypilot/models/tests/big_model_hash b/openpilot/sunnypilot/models/tests/big_model_hash new file mode 100644 index 0000000000..e957940fdd --- /dev/null +++ b/openpilot/sunnypilot/models/tests/big_model_hash @@ -0,0 +1 @@ +8bba37156aa17d49210cad028744c839ea9ed7b1f19428a8ecfafdc1e07a73b6 \ No newline at end of file diff --git a/openpilot/sunnypilot/models/tests/test_default_model.py b/openpilot/sunnypilot/models/tests/test_default_model.py index b72c2b4c89..c322d3b699 100644 --- a/openpilot/sunnypilot/models/tests/test_default_model.py +++ b/openpilot/sunnypilot/models/tests/test_default_model.py @@ -5,12 +5,25 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +import os +import subprocess + from openpilot.sunnypilot import get_file_hash -from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH +from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, SUPERCOMBO_ONNX_PATH, BIG_MODEL_HASH_PATH, \ + BIG_SUPERCOMBO_ONNX_PATH import hashlib from openpilot.common.test import OpenpilotTestCase +def _get_lfs_oid(path: str) -> str: + """Extract the LFS OID (SHA256 of actual content) from git, works whether the file is smudged or not.""" + pointer = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True) + for line in pointer.splitlines(): + if line.startswith("oid sha256:"): + return line.split(":", 1)[1] + raise ValueError(f"No LFS OID found for {path}") + + class TestDefaultModel(OpenpilotTestCase): def test_compare_onnx_hashes(self): supercombo_hash = get_file_hash(SUPERCOMBO_ONNX_PATH) @@ -21,3 +34,15 @@ class TestDefaultModel(OpenpilotTestCase): current_hash = f.read().strip() assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash" + + def test_compare_big_onnx_hashes(self): + if not os.path.exists(BIG_SUPERCOMBO_ONNX_PATH): + self.skipTest("big_driving_supercombo.onnx not present") + + oid = _get_lfs_oid(os.path.relpath(BIG_SUPERCOMBO_ONNX_PATH, os.getcwd())) + combined_hash = hashlib.sha256(oid.encode()).hexdigest() + + with open(BIG_MODEL_HASH_PATH) as f: + current_hash = f.read().strip() + + assert combined_hash == current_hash, "Run openpilot/sunnypilot/models/default_model.py to update the default model name and hash" From e87dbbaba710bbfe7661d9ff064d46170cac9442 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 2 Sep 2026 14:53:31 -0400 Subject: [PATCH 47/53] models: sanitize default model name for HF (#1984) --- .github/workflows/build-default-models.yaml | 17 ++++++++++------- release/ci/upload_default_model.py | 3 ++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index 8030c7133d..f2cb007670 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -30,6 +30,7 @@ jobs: runs-on: ubuntu-24.04 outputs: model_name: ${{ steps.resolve.outputs.model_name }} + safe_model_name: ${{ steps.resolve.outputs.safe_model_name }} onnx_ref: ${{ steps.resolve.outputs.onnx_ref }} onnx_path: ${{ steps.resolve.outputs.onnx_path }} hf_defaults_path: ${{ steps.resolve.outputs.hf_defaults_path }} @@ -64,7 +65,9 @@ jobs: exit 1 fi + SAFE_NAME="${NAME// /-}" echo "model_name=${NAME}" >> $GITHUB_OUTPUT + echo "safe_model_name=${SAFE_NAME}" >> $GITHUB_OUTPUT echo "onnx_ref=${ONNX_REF}" >> $GITHUB_OUTPUT echo "onnx_path=${ONNX_PATH}" >> $GITHUB_OUTPUT echo "hf_defaults_path=${HF_DEFAULTS_PATH}" >> $GITHUB_OUTPUT @@ -135,7 +138,7 @@ jobs: - name: Prepare output env: - MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }} run: | source ${UV_PROJECT_ENVIRONMENT}/bin/activate export PYTHONPATH=${{ github.workspace }} @@ -158,13 +161,13 @@ jobs: - name: Upload small model artifact uses: actions/upload-artifact@v4 with: - name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }} path: ${{ github.workspace }}/small_output/ - name: Upload artifact name file uses: actions/upload-artifact@v4 with: - name: artifact-name-${{ needs.resolve.outputs.model_name }} + name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} path: ${{ github.workspace }}/small_output/artifact_name.txt - name: Re-enable powersave @@ -254,7 +257,7 @@ jobs: - name: Prepare output env: - MODEL_NAME: ${{ needs.resolve.outputs.model_name }} + MODEL_NAME: ${{ needs.resolve.outputs.safe_model_name }} run: | source ${UV_PROJECT_ENVIRONMENT}/bin/activate export PYTHONPATH=${{ github.workspace }} @@ -277,13 +280,13 @@ jobs: - name: Upload big model artifact uses: actions/upload-artifact@v4 with: - name: model-${{ needs.resolve.outputs.model_name }}-${{ github.run_number }} + name: model-${{ needs.resolve.outputs.safe_model_name }}-${{ github.run_number }} path: ${{ github.workspace }}/big_output/ - name: Upload artifact name file uses: actions/upload-artifact@v4 with: - name: artifact-name-${{ needs.resolve.outputs.model_name }} + name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} path: ${{ github.workspace }}/big_output/artifact_name.txt - name: Re-enable powersave @@ -318,7 +321,7 @@ jobs: if: ${{ inputs.target == 'small' || inputs.target == 'big' }} uses: actions/download-artifact@v4 with: - name: artifact-name-${{ needs.resolve.outputs.model_name }} + name: artifact-name-${{ needs.resolve.outputs.safe_model_name }} path: artifact_name - name: Read artifact name diff --git a/release/ci/upload_default_model.py b/release/ci/upload_default_model.py index eac48e3be4..97263ae6f5 100644 --- a/release/ci/upload_default_model.py +++ b/release/ci/upload_default_model.py @@ -38,7 +38,8 @@ def main(): api = HfApi() onnx_sha256 = hash_file(args.onnx_path) short_ref = args.onnx_ref[:8] - folder_name = f"model-{args.model_name}-{short_ref}-{args.run_number}" + safe_name = args.model_name.replace(" ", "-") + folder_name = f"model-{safe_name}-{short_ref}-{args.run_number}" print(f"ONNX hash: {onnx_sha256}") print(f"ONNX ref: {args.onnx_ref} (short: {short_ref})") From 752c07f9e4aa2a2a4812e0c7206685589572b9c8 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 3 Sep 2026 05:22:43 -0700 Subject: [PATCH 48/53] ci: Replace hf oath with token (#1986) replace oauth with token --- .github/workflows/build-all-tinygrad-models.yaml | 1 + .github/workflows/build-default-models.yaml | 4 ++-- .github/workflows/build-single-tinygrad-model.yaml | 13 ++++--------- 3 files changed, 7 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml index 7bddeab2bc..8c2bcad0ad 100644 --- a/.github/workflows/build-all-tinygrad-models.yaml +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -78,6 +78,7 @@ jobs: - name: Get next recompiled dir number id: create-recompiled-dir env: + HF_TOKEN: ${{ secrets.HF_TOKEN }} HF_REPO: ${{ github.event.inputs.hf_repo }} run: | pip install huggingface_hub diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index f2cb007670..d748d3ac10 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -341,7 +341,7 @@ jobs: - name: Upload model to HF if: ${{ inputs.target == 'small' || inputs.target == 'big' }} env: - HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} ARTIFACT_NAME: ${{ steps.artifact.outputs.artifact_name }} run: | rm -f output/artifact_name.txt @@ -367,7 +367,7 @@ jobs: - name: Generate DM metadata and upload to HF if: ${{ inputs.target == 'dm' }} env: - HF_OIDC_RESOURCE: datasets/${{ env.HF_REPO }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} run: | export PYTHONPATH=$(pwd) python3 -c " diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml index c9d495c15f..c795541e66 100644 --- a/.github/workflows/build-single-tinygrad-model.yaml +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -146,7 +146,7 @@ jobs: - name: Validate hf_repo and JSON version env: - HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} run: | if [ ! -f "$JSON_FILE" ]; then echo "JSON file $JSON_FILE does not exist!" @@ -155,13 +155,8 @@ jobs: python3 -c " import sys from huggingface_hub import HfApi - try: - api = HfApi() - api.repo_info(repo_id=sys.argv[1], repo_type='dataset') - print(f'Success: Repo {sys.argv[1]} exists.') - except Exception as e: - print('HF validation failed:', e) - sys.exit(1) + HfApi().repo_info(repo_id=sys.argv[1], repo_type='dataset') + print(f'Success: Repo {sys.argv[1]} exists.') " "${{ inputs.hf_repo }}" - name: Download artifact name file @@ -192,7 +187,7 @@ jobs: - name: Upload to Hugging Face env: - HF_OIDC_RESOURCE: datasets/${{ inputs.hf_repo }} + HF_TOKEN: ${{ secrets.HF_TOKEN }} ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} run: | hf upload ${{ inputs.hf_repo }} \ From 132b31f4cf7b52a5a7e69f98afaba68ca0d43d62 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 3 Sep 2026 10:10:21 -0400 Subject: [PATCH 49/53] ci: poll GH API in prepare model jobs (#1987) --- .../workflows/sunnypilot-build-prebuilt.yaml | 114 ++++++++++++++---- 1 file changed, 90 insertions(+), 24 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index c93dbe02c1..dfcc7068e2 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -216,6 +216,9 @@ jobs: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 if: ${{ needs.prepare_strategy.outputs.include_big_model == 'true' }} + concurrency: + group: prepare-chestnut + cancel-in-progress: false outputs: onnx_sha256: ${{ steps.resolve.outputs.onnx_sha256 }} env: @@ -228,8 +231,10 @@ jobs: run: | REF="${{ github.head_ref || github.ref_name }}" - ONNX_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/big_driving_supercombo.onnx?ref=${REF}" --jq '.sha') + ONNX_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "ONNX hash: $ONNX_HASH" + [ -n "$ONNX_HASH" ] || { echo "::error::Failed to extract ONNX hash"; exit 1; } echo "onnx_sha256=$ONNX_HASH" >> $GITHUB_OUTPUT TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') @@ -238,7 +243,7 @@ jobs: JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" check_defaults() { - DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) [ "$TINYGRAD_MATCH" = "true" ] || return 1 BUNDLE=$(echo "$DEFAULTS" | jq --arg hash "$ONNX_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) @@ -252,18 +257,35 @@ jobs: echo "No matching model on HF — dispatching build" gh workflow run build-default-models.yaml --ref "$REF" -f target=big + sleep 10 - echo "Polling HF for big model availability..." + BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') + echo "Dispatched build run: $BUILD_RUN_ID" + + echo "Waiting for build run to complete..." for i in $(seq 1 90); do sleep 30 - if check_defaults; then - echo "Big model available on HF after $((i * 30))s" - exit 0 + STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') + CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') + echo "Poll $i/90: status=$STATUS conclusion=$CONCLUSION" + if [ "$STATUS" = "completed" ]; then + if [ "$CONCLUSION" = "success" ]; then + echo "Build run succeeded, verifying HF..." + sleep 10 + if check_defaults; then + echo "Big model verified on HF" + exit 0 + fi + echo "::error::Build succeeded but model not found on HF" + exit 1 + else + echo "::error::Build run failed with conclusion=$CONCLUSION" + exit 1 + fi fi - echo "Poll $i/90: not yet available" done - echo "::error::Big model not available on HF after 45 minutes" + echo "::error::Build run did not complete within 45 minutes" exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -277,6 +299,9 @@ jobs: prepare_small_model: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 + concurrency: + group: prepare-small-model + cancel-in-progress: false outputs: driving_onnx_sha256: ${{ steps.resolve.outputs.driving_onnx_sha256 }} env: @@ -289,8 +314,10 @@ jobs: run: | REF="${{ github.head_ref || github.ref_name }}" - DRIVING_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/driving_supercombo.onnx?ref=${REF}" --jq '.sha') + DRIVING_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "Driving ONNX hash: $DRIVING_HASH" + [ -n "$DRIVING_HASH" ] || { echo "::error::Failed to extract driving ONNX hash"; exit 1; } echo "driving_onnx_sha256=$DRIVING_HASH" >> $GITHUB_OUTPUT TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') @@ -299,7 +326,7 @@ jobs: JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" check_defaults() { - DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) [ "$TINYGRAD_MATCH" = "true" ] || return 1 DRIVING=$(echo "$DEFAULTS" | jq --arg hash "$DRIVING_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) @@ -313,18 +340,35 @@ jobs: echo "No matching model on HF — dispatching build" gh workflow run build-default-models.yaml --ref "$REF" -f target=small + sleep 10 - echo "Polling HF for model availability..." + BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') + echo "Dispatched build run: $BUILD_RUN_ID" + + echo "Waiting for build run to complete..." for i in $(seq 1 60); do sleep 30 - if check_defaults; then - echo "Model available on HF after $((i * 30))s" - exit 0 + STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') + CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') + echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION" + if [ "$STATUS" = "completed" ]; then + if [ "$CONCLUSION" = "success" ]; then + echo "Build run succeeded, verifying HF..." + sleep 10 + if check_defaults; then + echo "Small model verified on HF" + exit 0 + fi + echo "::error::Build succeeded but model not found on HF" + exit 1 + else + echo "::error::Build run failed with conclusion=$CONCLUSION" + exit 1 + fi fi - echo "Poll $i/60: not yet available" done - echo "::error::Small driving model not available on HF after 30 minutes" + echo "::error::Small model build did not complete within 30 minutes" exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -338,6 +382,9 @@ jobs: prepare_dm_model: needs: [ prepare_strategy ] runs-on: ubuntu-24.04 + concurrency: + group: prepare-dm-model + cancel-in-progress: false outputs: dm_onnx_sha256: ${{ steps.resolve.outputs.dm_onnx_sha256 }} env: @@ -350,8 +397,10 @@ jobs: run: | REF="${{ github.head_ref || github.ref_name }}" - DM_HASH=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) + BLOB_SHA=$(gh api "repos/${GH_REPO}/contents/openpilot/selfdrive/modeld/models/dmonitoring_model.onnx?ref=${REF}" --jq '.sha') + DM_HASH=$(gh api "repos/${GH_REPO}/git/blobs/${BLOB_SHA}" --jq '.content' | base64 -d | grep '^oid sha256:' | cut -d: -f2) echo "DM ONNX hash: $DM_HASH" + [ -n "$DM_HASH" ] || { echo "::error::Failed to extract DM ONNX hash"; exit 1; } echo "dm_onnx_sha256=$DM_HASH" >> $GITHUB_OUTPUT TINYGRAD_REF=$(gh api "repos/${GH_REPO}/contents/tinygrad_repo?ref=${REF}" --jq '.sha') @@ -360,7 +409,7 @@ jobs: JSON_URL="https://huggingface.co/datasets/${HF_REPO}/resolve/main/${HF_DEFAULTS_PATH}/default_models.json" check_defaults() { - DEFAULTS=$(curl -fsSL "$JSON_URL" 2>/dev/null) || return 1 + DEFAULTS=$(curl -fsSL "${JSON_URL}?t=$(date +%s)" 2>/dev/null) || return 1 TINYGRAD_MATCH=$(echo "$DEFAULTS" | jq -r --arg ref "$TINYGRAD_REF" '.tinygrad_ref == $ref' 2>/dev/null) [ "$TINYGRAD_MATCH" = "true" ] || return 1 DM=$(echo "$DEFAULTS" | jq --arg hash "$DM_HASH" '.bundles[] | select(.onnx_sha256 == $hash)' 2>/dev/null) @@ -374,18 +423,35 @@ jobs: echo "No matching DM model on HF — dispatching build" gh workflow run build-default-models.yaml --ref "$REF" -f target=dm + sleep 10 - echo "Polling HF for DM model availability..." + BUILD_RUN_ID=$(gh run list --workflow build-default-models.yaml --branch "$REF" --limit 1 --json databaseId --jq '.[0].databaseId') + echo "Dispatched build run: $BUILD_RUN_ID" + + echo "Waiting for build run to complete..." for i in $(seq 1 60); do sleep 30 - if check_defaults; then - echo "DM model available on HF after $((i * 30))s" - exit 0 + STATUS=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.status') + CONCLUSION=$(gh api "repos/${GH_REPO}/actions/runs/${BUILD_RUN_ID}" --jq '.conclusion') + echo "Poll $i/60: status=$STATUS conclusion=$CONCLUSION" + if [ "$STATUS" = "completed" ]; then + if [ "$CONCLUSION" = "success" ]; then + echo "Build run succeeded, verifying HF..." + sleep 10 + if check_defaults; then + echo "DM model verified on HF" + exit 0 + fi + echo "::error::Build succeeded but DM model not found on HF" + exit 1 + else + echo "::error::Build run failed with conclusion=$CONCLUSION" + exit 1 + fi fi - echo "Poll $i/60: not yet available" done - echo "::error::DM model not available on HF after 30 minutes" + echo "::error::DM model build did not complete within 30 minutes" exit 1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 302f3ad892038144edabc5626bda2294a4ff9b11 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:52:09 -0700 Subject: [PATCH 50/53] ci: compile dm warp (#1989) --- .github/workflows/build-default-models.yaml | 18 ++++++++++++++++++ .../download-hf-model-chunks/action.yml | 7 +++++++ 2 files changed, 25 insertions(+) diff --git a/.github/workflows/build-default-models.yaml b/.github/workflows/build-default-models.yaml index d748d3ac10..4f22002ae2 100644 --- a/.github/workflows/build-default-models.yaml +++ b/.github/workflows/build-default-models.yaml @@ -484,11 +484,29 @@ jobs: print(f'Chunked {pkl} into {len(targets)} chunks') " + - name: Compile DM warp + run: | + source ${UV_PROJECT_ENVIRONMENT}/bin/activate + export PYTHONPATH="${PYTHONPATH}:${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}" + + TG_FLAGS="DEV=QCOM IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1" + MODEL_DIR="${{ github.workspace }}/openpilot/selfdrive/modeld" + DM_SIZE=$(python3 -c "from openpilot.common.transformations.model import DM_INPUT_SIZE as s; print(f'{s[0]}x{s[1]}')") + + for res in $(python3 -c "from openpilot.common.transformations.camera import _ar_ox_fisheye as a, _os_fisheye as o; print(f'{a.width}x{a.height} {o.width}x{o.height}')"); do + WARP_PKL="${MODEL_DIR}/models/dm_warp_${res}_tinygrad.pkl" + taskset -c 7 env ${TG_FLAGS} python3 ${MODEL_DIR}/compile_dm_warp.py \ + --camera-resolution ${res} \ + --warp-to ${DM_SIZE} \ + --output ${WARP_PKL} + done + - name: Prepare DM output run: | mkdir -p dm_output cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunk* dm_output/ cp ${{ github.workspace }}/${{ env.DM_PKL }}.chunkmanifest dm_output/ + cp ${{ github.workspace }}/openpilot/selfdrive/modeld/models/dm_warp_* dm_output/ - name: Upload DM artifact uses: actions/upload-artifact@v4 diff --git a/.github/workflows/download-hf-model-chunks/action.yml b/.github/workflows/download-hf-model-chunks/action.yml index 01ab5385da..677385f1d6 100644 --- a/.github/workflows/download-hf-model-chunks/action.yml +++ b/.github/workflows/download-hf-model-chunks/action.yml @@ -46,6 +46,13 @@ runs: printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${CANONICAL}.chunk${CHUNK_IDX}" >> "$DOWNLOAD_LIST" done < <(echo "$ARTIFACT" | jq -r '.chunks[].file_name') echo "$NUM_CHUNKS" > "${DEST_DIR}/${CANONICAL}.chunkmanifest" + + if [ "$CANONICAL" = "dmonitoring_model_tinygrad.pkl" ]; then + for warp in dm_warp_1928x1208_tinygrad.pkl dm_warp_1344x760_tinygrad.pkl; do + ENCODED_URL=$(python3 -c "import urllib.parse; print(urllib.parse.quote('${BASE_URL}/${warp}', safe=':/'))") + printf '%s\t%s\n' "$ENCODED_URL" "${DEST_DIR}/${warp}" >> "$DOWNLOAD_LIST" + done + fi } echo "$MODELS_JSON" | jq -c '.[]' | while IFS= read -r model; do From 7eb457f6c474a8fe50a9319de507d532cd61aaf6 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 4 Sep 2026 22:50:12 -0400 Subject: [PATCH 51/53] chaos (#1991) burn it all --- .github/CODEOWNERS | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index e8f1b7eb8b..0000000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1,11 +0,0 @@ -* @sunnypilot/dev-internal -/.github/ @devtekve @sunnyhaibin -/release/ci/ @devtekve @sunnyhaibin -/tinygrad_repo @devtekve @Discountchubbs -/tinygrad/ @devtekve @Discountchubbs -/selfdrive/controls/lib/longitudinal_planner.py @devtekve @Discountchubbs -/selfdrive/controls/lib/longitudinal_mpc_lib/long_mpc.py @devtekve @Discountchubbs -/selfdrive/modeld/ @devtekve @Discountchubbs -/sunnypilot/model* @devtekve @Discountchubbs -/sunnypilot/sunnylink/ @devtekve -/system/athena/ @devtekve \ No newline at end of file From 047ae41c0d9fe3ae5656d1541a376d6fbf17c9a3 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Fri, 4 Sep 2026 21:15:00 -0700 Subject: [PATCH 52/53] modeld_v2: one dev warp and enqueue (#1990) --- .github/workflows/sunnypilot-build-model.yaml | 2 +- .../sunnypilot/modeld_v2/compile_modeld.py | 16 +++--- openpilot/sunnypilot/modeld_v2/modeld.py | 56 +++++++------------ openpilot/sunnypilot/models/fetcher.py | 2 +- 4 files changed, 32 insertions(+), 44 deletions(-) diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index 10be63ee22..837c53be69 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -188,7 +188,7 @@ jobs: if [ "${{ inputs.target_hardware }}" == "chestnut" ]; then echo "CHESTNUT build" export CHESTNUT=1 - TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2" + TG_FLAGS="DEBUG=1 DEV=USB+AMD:LLVM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0 TC_OPT=2 TC_OCCUPANCY_OPT=1" OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl" else echo "QCOM build" diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index 7a58352c36..a274097563 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -41,7 +41,6 @@ from tinygrad.tensor import Tensor MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy') WARP_INPUTS = ['tfm', 'big_tfm'] POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] -WARP_DEV = os.getenv('WARP_DEV') def _detect_desire_key(shapes: dict) -> str | None: @@ -154,12 +153,13 @@ def make_warp_queues(device=Device.DEFAULT): def make_warp(nv12: NV12Frame, model_w: int, model_h: int): frame_prepare = make_frame_prepare(nv12, model_w, model_h) - WARP_DEV = os.getenv('WARP_DEV', Device.DEFAULT) def warp(tfm, big_tfm, frame, big_frame): - tfm = tfm.to(WARP_DEV) - big_tfm = big_tfm.to(WARP_DEV) - Tensor.realize(tfm, big_tfm) + tfm = tfm.to(Device.DEFAULT) + big_tfm = big_tfm.to(Device.DEFAULT) + frame = frame.to(Device.DEFAULT) + big_frame = big_frame.to(Device.DEFAULT) + Tensor.realize(tfm, big_tfm, frame, big_frame) warped_frame = frame_prepare(frame, tfm).unsqueeze(0) warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) @@ -368,16 +368,18 @@ if __name__ == "__main__": run_policy_func = make_run_policy(vision_runner, policy_runners, features_slice, derived_frame_skip, all_shapes) run_policy_jit = TinyJit(run_policy_func, prune=True) make_policy_queues = partial(generate_queues_and_npy, all_shapes, derived_frame_skip, is_supercombo=is_supercombo) - make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=WARP_DEV) + make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=Device.DEFAULT) output_data['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, make_policy_queues) for cam_w, cam_h in args.camera_resolutions: print(f"Compiling warp JIT for {cam_w}x{cam_h}...") nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) - make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=WARP_DEV) + make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=Device.DEFAULT) warp = TinyJit(make_warp(nv12, model_w, model_h), prune=True) output_data[(cam_w, cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + output_data['metadata']['warp_dev'] = Device.DEFAULT + with open(args.output, "wb") as file: dump_oob(output_data, file) diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index cd421d70d5..2ab96934ef 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -6,6 +6,7 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ +from collections.abc import Callable import os os.environ['GMMU'] = '0' import numpy as np @@ -110,18 +111,12 @@ class ModelState(ModelStateBase): cloudlog.warning(f"loading combined pkl: {pkl_path}") jits = load_oob(open_file_chunked(pkl_path)) - self.WARP_DEV = 'QCOM' if COMMA_HARDWARE else 'CPU' - self.DEV = 'AMD' if self.chestnut else self.WARP_DEV - self.QUEUE_DEV = self.DEV metadata = jits['metadata'] - - self.is_legacy_model = 'run_policy' not in jits # remove after next recompile - if self.is_legacy_model: - self.warp = jits[(cam_w, cam_h)]['warp_enqueue'] - self.run_policy = jits[(cam_w, cam_h)]['run_policy'] - else: - self.run_policy = jits['run_policy'] - self.warp = jits[(cam_w, cam_h)] + self.WARP_DEV = metadata.get('warp_dev', 'QCOM' if COMMA_HARDWARE else 'CPU') + self.DEV = 'AMD' if self.chestnut else ('QCOM' if COMMA_HARDWARE else 'CPU') + self.QUEUE_DEV = self.DEV + self.run_policy = jits['run_policy'] + self.warp = jits[(cam_w, cam_h)] if 'model' in metadata: model_metadata = metadata['model'] @@ -180,11 +175,7 @@ class ModelState(ModelStateBase): yuv_size = self.frame_buf_params[self._road_key][3] frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() big_frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() - - if self.is_legacy_model: # Remove this conditional hack after recompile - self.warp(**self.input_queues, frame=frame_tensor, big_frame=big_frame_tensor) - else: - self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) + self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) def warmup(self) -> None: dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names} @@ -217,7 +208,8 @@ class ModelState(ModelStateBase): return self._desire_key def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: + inputs: dict[str, np.ndarray], prepare_only: bool, + after_enqueue: Callable[[], None] | None = None) -> 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] @@ -239,20 +231,18 @@ class ModelState(ModelStateBase): self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3) self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3) - if self.is_legacy_model: # remove after next recompile - if prepare_only: - self.warp(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - return None - raw_outputs = self.run_policy(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - else: - if prepare_only: - self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - return None - warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped) + if prepare_only: + self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + return None + warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) + raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped) + if after_enqueue is not None: + after_enqueue() if self._combined_model_type == 'supercombo': model_output = raw_outputs.numpy().flatten() + if self.chestnut and not np.all(np.isfinite(model_output)): + raise RuntimeError("model output not finite") sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()} outputs = self.parser.parse_outputs(sliced) if 'prev_feat' in self.numpy_inputs: @@ -285,9 +275,6 @@ class ModelState(ModelStateBase): buf[0, :-1] = buf[0, 1:] buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0 - if self.chestnut and not np.all(np.isfinite(outputs.get('plan', np.array([0.])))): - raise RuntimeError("model output not finite") - return outputs def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, @@ -512,7 +499,9 @@ def main(demo=False): mt1 = time.perf_counter() try: - model_output = model.run(bufs, transforms, inputs, prepare_only) + send_chestnut = (chestnut_state is not None and + run_count % round(model.constants.MODEL_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) + model_output = model.run(bufs, transforms, inputs, prepare_only, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): raise @@ -559,9 +548,6 @@ def main(demo=False): pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id - if chestnut_state is not None and run_count % round(model.constants.MODEL_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0: - chestnut_state.send() - if __name__ == "__main__": try: import argparse diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index f997dbbc51..810b9b7e49 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -139,7 +139,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json" - MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v23.json" + MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v24.json" MODEL_SOURCES = { "qcom": (MODEL_URL, ""), From 6135084c941d4d947dd90c78326a557c3c857f89 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sat, 5 Sep 2026 20:17:14 -0700 Subject: [PATCH 53/53] modeld_v2: realize frames on npy -> amd (#1993) --- openpilot/sunnypilot/modeld_v2/__init__.py | 5 - .../sunnypilot/modeld_v2/compile_modeld.py | 155 ++++++++--------- openpilot/sunnypilot/modeld_v2/constants.py | 3 + openpilot/sunnypilot/modeld_v2/meta_helper.py | 19 +-- openpilot/sunnypilot/modeld_v2/modeld.py | 154 +++++++++-------- .../modeld_v2/parse_model_outputs.py | 41 +++-- .../modeld_v2/parse_model_outputs_split.py | 159 ------------------ .../sunnypilot/modeld_v2/tests/helpers.py | 6 +- .../tests/test_combined_pkl_loader.py | 17 ++ .../tests/test_parse_model_outputs.py | 81 +++++++++ openpilot/sunnypilot/models/constants.py | 121 ------------- openpilot/sunnypilot/models/fetcher.py | 2 +- openpilot/sunnypilot/models/helpers.py | 31 ---- 13 files changed, 298 insertions(+), 496 deletions(-) delete mode 100644 openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py create mode 100644 openpilot/sunnypilot/modeld_v2/tests/test_parse_model_outputs.py delete mode 100644 openpilot/sunnypilot/models/constants.py diff --git a/openpilot/sunnypilot/modeld_v2/__init__.py b/openpilot/sunnypilot/modeld_v2/__init__.py index 639622e827..e69de29bb2 100644 --- a/openpilot/sunnypilot/modeld_v2/__init__.py +++ b/openpilot/sunnypilot/modeld_v2/__init__.py @@ -1,5 +0,0 @@ -from pathlib import Path - -MODEL_PATH = Path(__file__).parent / 'models/supercombo.onnx' -MODEL_PKL_PATH = Path(__file__).parent / 'models/supercombo_tinygrad.pkl' -METADATA_PATH = Path(__file__).parent / 'models/supercombo_metadata.pkl' diff --git a/openpilot/sunnypilot/modeld_v2/compile_modeld.py b/openpilot/sunnypilot/modeld_v2/compile_modeld.py index a274097563..1e88769928 100755 --- a/openpilot/sunnypilot/modeld_v2/compile_modeld.py +++ b/openpilot/sunnypilot/modeld_v2/compile_modeld.py @@ -32,7 +32,7 @@ def _patch_tinygrad_fetch_fw(): helpers.fetch_fw = fetch_fw _patch_tinygrad_fetch_fw() -from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, make_frame_prepare, sample_desire, sample_skip, shift_and_sample +import openpilot.selfdrive.modeld.compile_modeld as stock from tinygrad import dtypes from tinygrad.device import Device from tinygrad.engine.jit import TinyJit @@ -41,7 +41,7 @@ from tinygrad.tensor import Tensor MODEL_TYPES = ('vision_policy', 'supercombo', 'vision_multi_policy') WARP_INPUTS = ['tfm', 'big_tfm'] POLICY_INPUTS = ['img_q', 'big_img_q', 'feat_q', 'desire_q', 'packed_npy_inputs'] - +nv12_copy_size = stock.nv12_copy_size def _detect_desire_key(shapes: dict) -> str | None: return next((key for key in shapes if key.startswith('desire')), None) @@ -138,7 +138,7 @@ def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True) -def make_random_images(keys, shape, device): +def make_random_images(keys, shape, device, rng=None): return {k: Tensor.randint(shape, low=0, high=256, dtype=dtypes.uint8, device=device).realize() for k in keys} @@ -151,25 +151,9 @@ def make_warp_queues(device=Device.DEFAULT): return queues, npy -def make_warp(nv12: NV12Frame, model_w: int, model_h: int): - frame_prepare = make_frame_prepare(nv12, model_w, model_h) - - def warp(tfm, big_tfm, frame, big_frame): - tfm = tfm.to(Device.DEFAULT) - big_tfm = big_tfm.to(Device.DEFAULT) - frame = frame.to(Device.DEFAULT) - big_frame = big_frame.to(Device.DEFAULT) - Tensor.realize(tfm, big_tfm, frame, big_frame) - - warped_frame = frame_prepare(frame, tfm).unsqueeze(0) - warped_big_frame = frame_prepare(big_frame, big_tfm).unsqueeze(0) - return Tensor.cat(warped_frame, warped_big_frame) - return warp - - def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, frame_skip: int, input_shapes: dict): - sample_skip_fn = partial(sample_skip, frame_skip=frame_skip) - sample_desire_fn = partial(sample_desire, frame_skip=frame_skip) + sample_skip_fn = partial(stock.sample_skip, frame_skip=frame_skip) + sample_desire_fn = partial(stock.sample_desire, frame_skip=frame_skip) desire_key = _detect_desire_key(input_shapes) road_key, wide_key = _detect_vision_keys(input_shapes) @@ -186,14 +170,14 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, warped_dev = warped.to(Device.DEFAULT) Tensor.realize(packed_npy_inputs_dev, warped_dev) - img = shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn) - big_img = shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn) + img = stock.shift_and_sample(img_q, warped_dev[0:1], sample_skip_fn) + big_img = stock.shift_and_sample(big_img_q, warped_dev[1:2], sample_skip_fn) unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)] unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True)) desire_dev = unpacked_dict['desire'] - desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn) + desire_buf = stock.shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn) inputs = {desire_key: desire_buf} for key, tensor_val in unpacked_dict.items(): @@ -202,13 +186,13 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, if 'prev_feat' in unpacked_dict: prev_feat_dev = unpacked_dict['prev_feat'] - inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).reshape(input_shapes['features_buffer']) + inputs['features_buffer'] = stock.shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).reshape(input_shapes['features_buffer']) if vision_runner: vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize() if 'features_buffer' not in inputs: new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0) - inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() + inputs['features_buffer'] = stock.shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners] return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0]) @@ -219,27 +203,28 @@ def make_run_policy(vision_runner, policy_runners: list, features_slice: slice, policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize() if 'features_buffer' not in inputs and features_slice is not None: new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0) - shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() + stock.shift_and_sample(feat_q, new_feat, sample_skip_fn).realize() return policy_out return run_policy -def compile_jit(jit, make_random_inputs, input_keys, make_queues): +def compile_jit(jit, input_keys, make_queues, make_random_inputs=None, benchmark_runs: int = 1): SEED = 42 - def random_inputs_run(fn, seed, test_val=None, test_buffers=None, expect_match=True): - input_queues, npy = make_queues(Device.DEFAULT) + def random_inputs_run(fn, seed, n_runs, test_val=None, test_buffers=None, expect_match=True): + queues_res = make_queues(Device.DEFAULT) + input_queues, npy = queues_res[0], queues_res[1] + frame_views = queues_res[2] if len(queues_res) > 2 else {} rng = np.random.default_rng(seed) Tensor.manual_seed(seed) - testing = test_val is not None or test_buffers is not None - n_runs = 1 if testing else 3 - for i in range(n_runs): for v in npy.values(): v[:] = rng.standard_normal(v.shape).astype(v.dtype) + for v in frame_views.values(): + v[:] = rng.integers(0, 256, size=v.shape, dtype=np.uint8) Device.default.synchronize() - random_inputs = make_random_inputs() + random_inputs = make_random_inputs(rng=rng) if make_random_inputs is not None else {} st = time.perf_counter() outs = fn(**{k: input_queues[k] for k in input_keys if k in input_queues}, **random_inputs) mt = time.perf_counter() @@ -260,14 +245,15 @@ def compile_jit(jit, make_random_inputs, input_keys, make_queues): return val, buffers print('capture + replay') - test_val, test_buffers = random_inputs_run(jit, SEED) - print('pickle round trip') + test_val, test_buffers = random_inputs_run(jit, SEED, 3) + print(f'pickle round trip ({benchmark_runs} runs per seed)') with tempfile.TemporaryFile(dir=".") as f: dump_oob(jit, f) f.seek(0) - deserialized_jit = load_oob(f) - random_inputs_run(deserialized_jit, SEED, test_val=test_val, test_buffers=test_buffers) - return deserialized_jit + loaded_jit = load_oob(f) + random_inputs_run(loaded_jit, SEED, benchmark_runs, test_val, test_buffers, expect_match=True) + random_inputs_run(loaded_jit, SEED+1, benchmark_runs, test_val, test_buffers, expect_match=False) + return jit def _parse_size(size_str: str) -> tuple[int, int]: @@ -317,6 +303,7 @@ if __name__ == "__main__": parser.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH') parser.add_argument('--camera-resolutions', type=_parse_size, nargs='+', required=True) parser.add_argument('--frame-skip', type=int, default=None, help='frame skip value (auto-derived if not provided)') + parser.add_argument('--benchmark-runs', type=int, default=1, help='benchmark runs') parser.add_argument('--output', required=True) parser.add_argument('--vision-onnx', help='vision ONNX (for split models)') @@ -335,50 +322,64 @@ if __name__ == "__main__": args.on_policy_onnx = read_file_chunked_to_disk(args.on_policy_onnx) args.supercombo_onnx = read_file_chunked_to_disk(args.supercombo_onnx) - vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None - - if args.model_type == 'vision_policy': - assert vision_runner and args.policy_onnx - policy_runners = [OnnxRunner(args.policy_onnx)] - output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx), 'policy': make_metadata_dict(args.policy_onnx)} - elif args.model_type == 'supercombo': + if args.model_type == 'supercombo': assert args.supercombo_onnx - policy_runners = [OnnxRunner(args.supercombo_onnx)] - output_data['metadata'] = {'model': make_metadata_dict(args.supercombo_onnx)} - elif args.model_type == 'vision_multi_policy': - assert vision_runner - policy_runners, policy_names = _load_policy_runners(args) - output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx)} - for name in policy_names: - runner_arg = getattr(args, f"{name}_onnx") - output_data['metadata'][name] = make_metadata_dict(runner_arg) + model_metadata = make_metadata_dict(args.supercombo_onnx) + output_data['metadata'] = {'model': model_metadata, **model_metadata} + output_data['input_devices'] = {'model': Device.DEFAULT} + output_data['run_model'] = {} + derived_frame_skip = args.frame_skip or derive_frame_skip({}, model_metadata['input_shapes']) + model_runner = OnnxRunner(args.supercombo_onnx) + run_policy = stock.make_run_policy(model_runner, model_metadata, derived_frame_skip) + for cam_w, cam_h in args.camera_resolutions: + print(f"Compiling unified run_model JIT for {cam_w}x{cam_h}...") + nv12 = stock.NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + frame_copy_size = stock.nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height) + make_model_queues = partial(stock.make_input_queues, model_metadata['input_shapes'], derived_frame_skip, + frame_copy_size=frame_copy_size) + warp = stock.make_warp(nv12, model_w, model_h) + run_model_jit = TinyJit(stock.make_run_model(warp, run_policy, model_metadata, frame_copy_size), prune=True) + output_data['run_model'][(cam_w, cam_h)] = compile_jit(run_model_jit, stock.MODELD_INPUTS, make_model_queues, benchmark_runs=args.benchmark_runs) + else: + vision_runner = OnnxRunner(args.vision_onnx) if args.vision_onnx else None + if args.model_type == 'vision_policy': + assert vision_runner and args.policy_onnx + policy_runners = [OnnxRunner(args.policy_onnx)] + output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx), 'policy': make_metadata_dict(args.policy_onnx)} + elif args.model_type == 'vision_multi_policy': + assert vision_runner + policy_runners, policy_names = _load_policy_runners(args) + output_data['metadata'] = {'vision': make_metadata_dict(args.vision_onnx)} + for name in policy_names: + runner_arg = getattr(args, f"{name}_onnx") + output_data['metadata'][name] = make_metadata_dict(runner_arg) - policy_keys = [key for key in output_data['metadata'].keys() if key != 'vision'] - first_policy_meta = output_data['metadata'][policy_keys[0]] if policy_keys else {} - vision_meta = output_data['metadata'].get('vision', {}) + policy_keys = [key for key in output_data['metadata'].keys() if key != 'vision'] + first_policy_meta = output_data['metadata'][policy_keys[0]] if policy_keys else {} + vision_meta = output_data['metadata'].get('vision', {}) - derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {})) - all_shapes = {key: value for meta in output_data['metadata'].values() for key, value in meta['input_shapes'].items()} - feat_meta = output_data['metadata'].get('vision') or output_data['metadata'].get('model') or output_data['metadata'].get('policy') - assert feat_meta is not None - features_slice = feat_meta['output_slices']['hidden_state'] - is_supercombo = vision_runner is None + derived_frame_skip = args.frame_skip or derive_frame_skip(vision_meta.get('input_shapes', {}), first_policy_meta.get('input_shapes', {})) + all_shapes = {key: value for meta in output_data['metadata'].values() for key, value in meta['input_shapes'].items()} + feat_meta = output_data['metadata'].get('vision') or output_data['metadata'].get('policy') + assert feat_meta is not None + features_slice = feat_meta['output_slices']['hidden_state'] - print(f"Compiling run_policy JIT (model_size={model_w}x{model_h}, frame_skip={derived_frame_skip})...") - run_policy_func = make_run_policy(vision_runner, policy_runners, features_slice, derived_frame_skip, all_shapes) - run_policy_jit = TinyJit(run_policy_func, prune=True) - make_policy_queues = partial(generate_queues_and_npy, all_shapes, derived_frame_skip, is_supercombo=is_supercombo) - make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=Device.DEFAULT) - output_data['run_policy'] = compile_jit(run_policy_jit, make_random_model_inputs, POLICY_INPUTS, make_policy_queues) + print(f"Compiling run_policy JIT (model_size={model_w}x{model_h}, frame_skip={derived_frame_skip})...") + run_policy_func = make_run_policy(vision_runner, policy_runners, features_slice, derived_frame_skip, all_shapes) + run_policy_jit = TinyJit(run_policy_func, prune=True) + make_policy_queues = partial(generate_queues_and_npy, all_shapes, derived_frame_skip, is_supercombo=False) + make_random_model_inputs = partial(make_random_images, keys=['warped'], shape=(2, 6, model_h // 2, model_w // 2), device=Device.DEFAULT) + output_data['run_policy'] = compile_jit(run_policy_jit, POLICY_INPUTS, make_policy_queues, make_random_inputs=make_random_model_inputs) - for cam_w, cam_h in args.camera_resolutions: - print(f"Compiling warp JIT for {cam_w}x{cam_h}...") - nv12 = NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) - make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=nv12.size, device=Device.DEFAULT) - warp = TinyJit(make_warp(nv12, model_w, model_h), prune=True) - output_data[(cam_w, cam_h)] = compile_jit(warp, make_random_warp_inputs, WARP_INPUTS, make_warp_queues) + for cam_w, cam_h in args.camera_resolutions: + print(f"Compiling warp JIT for {cam_w}x{cam_h}...") + nv12 = stock.NV12Frame(cam_w, cam_h, *get_nv12_info(cam_w, cam_h)) + frame_copy_size = stock.nv12_copy_size(nv12.stride, nv12.y_height, nv12.uv_height) + make_random_warp_inputs = partial(make_random_images, keys=['frame', 'big_frame'], shape=frame_copy_size, device=Device.DEFAULT) + warp = TinyJit(stock.make_warp(nv12, model_w, model_h), prune=True) + output_data[(cam_w, cam_h)] = compile_jit(warp, WARP_INPUTS, make_warp_queues, make_random_inputs=make_random_warp_inputs) - output_data['metadata']['warp_dev'] = Device.DEFAULT + output_data['metadata']['warp_dev'] = Device.DEFAULT with open(args.output, "wb") as file: dump_oob(output_data, file) diff --git a/openpilot/sunnypilot/modeld_v2/constants.py b/openpilot/sunnypilot/modeld_v2/constants.py index cf5157591e..c51996b537 100644 --- a/openpilot/sunnypilot/modeld_v2/constants.py +++ b/openpilot/sunnypilot/modeld_v2/constants.py @@ -14,6 +14,8 @@ class ModelConstants: # model inputs constants MODEL_FREQ = 20 + MODEL_RUN_FREQ = 20 + MODEL_CONTEXT_FREQ = 5 FEATURE_LEN = 512 FULL_HISTORY_BUFFER_LEN = 99 DESIRE_LEN = 8 @@ -35,6 +37,7 @@ class ModelConstants: LANE_LINES_WIDTH = 2 ROAD_EDGES_WIDTH = 2 PLAN_WIDTH = 15 + ACTION_WIDTH = 2 DESIRE_PRED_WIDTH = 8 LAT_PLANNER_SOLUTION_WIDTH = 4 DESIRED_CURV_WIDTH = 1 diff --git a/openpilot/sunnypilot/modeld_v2/meta_helper.py b/openpilot/sunnypilot/modeld_v2/meta_helper.py index bcf73adc6f..bf4116bb9b 100644 --- a/openpilot/sunnypilot/modeld_v2/meta_helper.py +++ b/openpilot/sunnypilot/modeld_v2/meta_helper.py @@ -1,26 +1,9 @@ from openpilot.sunnypilot.modeld_v2.constants import Meta -from openpilot.cereal import custom from openpilot.sunnypilot.modeld_v2.meta_20hz import Meta20hz from openpilot.sunnypilot.models.helpers import get_active_bundle -ModelBundle = custom.ModelManagerSP.ModelBundle - def load_meta_constants(): - """ - Determines and loads the appropriate meta model class based on the metadata provided. The function checks - specific keys and conditions within the provided metadata dictionary to identify the corresponding meta - model class to return. - - :param model_metadata: Dictionary containing metadata about the model. It includes - details such as input shapes, output slices, and other configurations for identifying - metadata-dependent meta model classes. - :type model_metadata: dict - :return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider) - based on the conditions and metadata provided. - :rtype: type - """ if (bundle := get_active_bundle()) and bundle.is20hz: return Meta20hz - - return Meta # Default + return Meta diff --git a/openpilot/sunnypilot/modeld_v2/modeld.py b/openpilot/sunnypilot/modeld_v2/modeld.py index 2ab96934ef..683def6588 100755 --- a/openpilot/sunnypilot/modeld_v2/modeld.py +++ b/openpilot/sunnypilot/modeld_v2/modeld.py @@ -38,11 +38,18 @@ from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value from openpilot.selfdrive.modeld.modeld import ChestnutState +from openpilot.selfdrive.modeld.compile_modeld import ( + MODELD_INPUTS, + make_input_queues as make_stock_input_queues, +) from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output -from openpilot.sunnypilot.modeld_v2.constants import Plan +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper -from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues, make_supercombo_input_queues, WARP_INPUTS, POLICY_INPUTS +from openpilot.sunnypilot.modeld_v2.compile_modeld import (derive_frame_skip, make_split_input_queues, + make_supercombo_input_queues, nv12_copy_size, + WARP_INPUTS, POLICY_INPUTS) from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle @@ -112,29 +119,39 @@ class ModelState(ModelStateBase): jits = load_oob(open_file_chunked(pkl_path)) metadata = jits['metadata'] - self.WARP_DEV = metadata.get('warp_dev', 'QCOM' if COMMA_HARDWARE else 'CPU') - self.DEV = 'AMD' if self.chestnut else ('QCOM' if COMMA_HARDWARE else 'CPU') + self.WARP_DEV = metadata.get('warp_dev', 'QCOM') if COMMA_HARDWARE else 'CPU' + self.DEV = ('AMD' if self.chestnut else 'QCOM') if COMMA_HARDWARE else 'CPU' self.QUEUE_DEV = self.DEV - self.run_policy = jits['run_policy'] - self.warp = jits[(cam_w, cam_h)] + self.is_run_model = 'run_model' in jits - if 'model' in metadata: - model_metadata = metadata['model'] + nv12_info = get_nv12_info(cam_w, cam_h) + self.frame_copy_size = nv12_copy_size(*nv12_info[:3]) + self.full_frames: dict = {} + self._blob_cache: dict = {} + self.frame_buffers: dict = {} + + if self.is_run_model or 'model' in metadata: + model_metadata = metadata.get('model', metadata) + self.input_shapes = model_metadata['input_shapes'] self.vision_output_slices = model_metadata['output_slices'] self.policy_output_slices = {} self._policy_slices_list = [] self._combined_model_type = 'supercombo' - self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key] - frame_skip = derive_frame_skip({}, model_metadata['input_shapes']) - self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], - frame_skip, device=self.QUEUE_DEV) - else: - vision_metadata = metadata['vision'] - policy_keys = [k for k in metadata if k != 'vision'] - if policy_keys == ['policy']: - self._combined_model_type = 'split' + self._vision_input_names = [key for key in self.input_shapes if 'img' in key] + self.frame_skip = derive_frame_skip({}, self.input_shapes) + if self.is_run_model: + self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues( + self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size) + self.frame_views, self.npy = self.frame_buffers, self.numpy_inputs + self.run_model, self.run_policy, self.warp = jits['run_model'][(cam_w, cam_h)], None, None else: - self._combined_model_type = 'multi_policy' + self.input_queues, self.numpy_inputs = make_supercombo_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV) + self.run_model, self.run_policy, self.warp = None, jits['run_policy'], jits[(cam_w, cam_h)] + else: + self.run_model, self.run_policy, self.warp = None, jits['run_policy'], jits[(cam_w, cam_h)] + vision_metadata = metadata['vision'] + policy_keys = [k for k in metadata if k not in ('vision', 'warp_dev')] + self._combined_model_type = 'split' if policy_keys == ['policy'] else 'multi_policy' self.vision_output_slices = vision_metadata['output_slices'] self._policy_keys = policy_keys self._policy_slices_list = [metadata[k]['output_slices'] for k in policy_keys] @@ -150,50 +167,39 @@ class ModelState(ModelStateBase): self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire')) self._road_key = next(key for key in self._vision_input_names if 'big' not in key) self._wide_key = next(key for key in self._vision_input_names if 'big' in key) + self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info) is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy') if is_20hz: from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants self.constants = SplitModelConstants() else: - from openpilot.sunnypilot.modeld_v2.constants import ModelConstants self.constants = ModelConstants() - if self._combined_model_type != 'supercombo': - from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser - self.parser = SplitParser() - else: - from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser - self.parser = CombinedParser() - + self.parser = Parser() self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32) - self.full_frames: dict = {} - self._blob_cache: dict = {} - nv12_info = get_nv12_info(cam_w, cam_h) - self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info) - yuv_size = self.frame_buf_params[self._road_key][3] - frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() - big_frame_tensor = Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() - self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=frame_tensor, big_frame=big_frame_tensor) + if self.warp is not None: + self.full_frames = {k: Tensor(np.zeros(nv12_info[3], dtype=np.uint8), device=self.WARP_DEV).contiguous().realize() for k in self._vision_input_names} + self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key]) def warmup(self) -> None: - dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names} + dummy_size = self.frame_copy_size if self.is_run_model else self.frame_buf_params[self._road_key][3] + dummy_frames = {k: np.zeros(dummy_size, dtype=np.uint8) for k in self._vision_input_names} transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k} - - dummy_inputs = {} - for k, v in self.numpy_inputs.items(): - if k not in ['tfm', 'big_tfm', 'prev_feat']: - dummy_inputs[k] = np.zeros(v.shape, dtype=v.dtype) - - self.run(dummy_frames, transforms, dummy_inputs, prepare_only=False) - - for v in self.numpy_inputs.values(): - v[:] = 0 + dummy_inputs = {k: np.zeros(v.shape, dtype=v.dtype) for k, v in self.numpy_inputs.items() if k not in ['tfm', 'big_tfm', 'prev_feat']} + self.run(dummy_frames, transforms, dummy_inputs) + if self.is_run_model: + self.input_queues, self.numpy_inputs, self.frame_buffers = make_stock_input_queues( + self.input_shapes, self.frame_skip, device=self.DEV, frame_copy_size=self.frame_copy_size) + self.frame_views = self.frame_buffers + self.npy = self.numpy_inputs + else: + for v in self.numpy_inputs.values(): + v[:] = 0 + self.full_frames.clear() + self._blob_cache.clear() self.prev_desire[:] = 0 - self.full_frames.clear() - self._blob_cache.clear() - @property def mlsim(self) -> bool: @@ -208,34 +214,40 @@ class ModelState(ModelStateBase): return self._desire_key def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], - inputs: dict[str, np.ndarray], prepare_only: bool, + inputs: dict[str, np.ndarray], after_enqueue: Callable[[], None] | None = None) -> 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] - 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] + if self.is_run_model: + for key, buf in bufs.items(): + data = buf.data if hasattr(buf, 'data') else buf + np.copyto(self.frame_buffers[key], np.frombuffer(data, dtype=np.uint8, count=self.frame_copy_size)) + else: + for key, buf in bufs.items(): + ptr = np.frombuffer(buf.data, dtype=np.uint8).ctypes.data + cache_key = (key, ptr) + if cache_key not in self._blob_cache: + self._blob_cache[cache_key] = Tensor.from_blob(ptr, (self.frame_buf_params[key][3],), dtype='uint8', device=self.WARP_DEV) + self.full_frames[key] = self._blob_cache[cache_key] desire_key = self.desire_key inputs[desire_key][0] = 0 self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0) self.prev_desire[:] = inputs[desire_key] + for key in ('traffic_convention', 'lateral_control_params', 'action_t'): if key in self.numpy_inputs and key in inputs: self.numpy_inputs[key][:] = inputs[key] - road_key = self._road_key - wide_key = self._wide_key - self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3) - self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3) + self.numpy_inputs['tfm'][:, :] = transforms[self._road_key].reshape(3, 3) + self.numpy_inputs['big_tfm'][:, :] = transforms[self._wide_key].reshape(3, 3) + + if self.run_model is not None: + outs, = self.run_model(**{k: self.input_queues[k] for k in MODELD_INPUTS}) + raw_outputs = outs + else: + assert self.warp is not None and self.run_policy is not None + warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[self._road_key], big_frame=self.full_frames[self._wide_key]) + raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped) - if prepare_only: - self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - return None - warped = self.warp(**{k: self.input_queues[k] for k in WARP_INPUTS}, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key]) - raw_outputs = self.run_policy(**{k: self.input_queues[k] for k in POLICY_INPUTS if k in self.input_queues}, warped=warped) if after_enqueue is not None: after_enqueue() @@ -245,7 +257,7 @@ class ModelState(ModelStateBase): raise RuntimeError("model output not finite") sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()} outputs = self.parser.parse_outputs(sliced) - if 'prev_feat' in self.numpy_inputs: + if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices: self.numpy_inputs['prev_feat'][:] = model_output[self.vision_output_slices['hidden_state']] else: vision_output = raw_outputs[0].numpy().flatten() @@ -360,7 +372,11 @@ def main(demo=False): loader.start() loader.join(BIG_MODEL_TIMEOUT) model = big_model + if model is None: + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", model is not None) + if model is not None: + params.remove("ChestnutModelError") small_model = ModelState(cam_w=vipc_client_main.width, cam_h=vipc_client_main.height, chestnut=False) if model is None or CHESTNUT else None if model is None: @@ -474,9 +490,6 @@ def main(demo=False): run_count = run_count + 1 frame_drop_ratio = frames_dropped / (1 + frames_dropped) - prepare_only = vipc_dropped_frames > 0 - if prepare_only: - cloudlog.error(f"skipping model eval. Dropped {vipc_dropped_frames} frames") bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names} transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names} @@ -501,11 +514,12 @@ def main(demo=False): try: send_chestnut = (chestnut_state is not None and run_count % round(model.constants.MODEL_FREQ / SERVICE_LIST['chestnutState'].frequency) == 0) - model_output = model.run(bufs, transforms, inputs, prepare_only, chestnut_state.send if send_chestnut else None) + model_output = model.run(bufs, transforms, inputs, chestnut_state.send if send_chestnut else None) except Exception: if not params.get_bool("ChestnutActive"): raise cloudlog.exception("chestnut failed, falling back to small") + params.put_bool("ChestnutModelError", True) params.put_bool("ChestnutActive", False) assert small_model is not None model = small_model diff --git a/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py b/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py index 7a3adcc1fa..d6cfdbbc88 100644 --- a/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py +++ b/openpilot/sunnypilot/modeld_v2/parse_model_outputs.py @@ -115,22 +115,41 @@ class Parser: outs[name + '_stds'] = pred_std_final.reshape(final_shape) def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - # supercombo (4955 / 102) and newer variants (e.g. 990 / 144). - self.parse_mdn('plan', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) - self.parse_mdn('lane_lines', outs, out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('road_edges', outs, out_shape=(ModelConstants.NUM_ROAD_EDGES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('pose', outs, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('road_transform', outs, out_shape=(ModelConstants.POSE_WIDTH,)) + if 'plan' in outs: + self.parse_mdn('plan', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) + if 'planplus' in outs: + self.parse_mdn('planplus', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) + if 'lane_lines' in outs: + self.parse_mdn('lane_lines', outs, out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH)) + if 'road_edges' in outs: + self.parse_mdn('road_edges', outs, out_shape=(ModelConstants.NUM_ROAD_EDGES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH)) + if 'pose' in outs: + self.parse_mdn('pose', outs, out_shape=(ModelConstants.POSE_WIDTH,)) + if 'road_transform' in outs: + self.parse_mdn('road_transform', outs, out_shape=(ModelConstants.POSE_WIDTH,)) if 'sim_pose' in outs: self.parse_mdn('sim_pose', outs, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('wide_from_device_euler', outs, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) - self.parse_mdn('lead', outs, out_shape=(ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH)) + if 'wide_from_device_euler' in outs: + self.parse_mdn('wide_from_device_euler', outs, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) + if 'lead' in outs: + self.parse_mdn('lead', outs, out_shape=(ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH)) if 'lat_planner_solution' in outs: self.parse_mdn('lat_planner_solution', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) if 'desired_curvature' in outs: self.parse_mdn('desired_curvature', outs, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) + if 'action' in outs: + self.parse_mdn('action', outs, out_shape=(ModelConstants.ACTION_WIDTH,)) for k in ['lead_prob', 'lane_lines_prob', 'meta']: - self.parse_binary_crossentropy(k, outs) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN, ModelConstants.DESIRE_PRED_WIDTH)) + if k in outs: + self.parse_binary_crossentropy(k, outs) + if 'desire_state' in outs: + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) + if 'desire_pred' in outs: + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN, ModelConstants.DESIRE_PRED_WIDTH)) return outs + + def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + return self.parse_outputs(outs) + + def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + return self.parse_outputs(outs) diff --git a/openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py b/openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py deleted file mode 100644 index 3db47aee42..0000000000 --- a/openpilot/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ /dev/null @@ -1,159 +0,0 @@ -import numpy as np -from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants - - -def safe_exp(x, out=None): - # -11 is around 10**14, more causes float16 overflow - return np.exp(np.clip(x, -np.inf, 11), out=out) - - -def sigmoid(x): - return 1. / (1. + safe_exp(-x)) - - -def softmax(x, axis=-1): - x -= np.max(x, axis=axis, keepdims=True) - if x.dtype == np.float32 or x.dtype == np.float64: - safe_exp(x, out=x) - else: - x = safe_exp(x) - x /= np.sum(x, axis=axis, keepdims=True) - return x - - -class Parser: - def __init__(self, ignore_missing=False): - self.ignore_missing = ignore_missing - - def check_missing(self, outs, name): - if name not in outs and not self.ignore_missing: - raise ValueError(f"Missing output {name}") - return name not in outs - - def parse_categorical_crossentropy(self, name, outs, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - if out_shape is not None: - raw = raw.reshape((raw.shape[0],) + out_shape) - outs[name] = softmax(raw, axis=-1) - - def parse_binary_crossentropy(self, name, outs): - if self.check_missing(outs, name): - return - raw = outs[name] - outs[name] = sigmoid(raw) - - def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None): - if self.check_missing(outs, name): - return - raw = outs[name] - raw = raw.reshape((raw.shape[0], max(in_N, 1), -1)) - - n_values = (raw.shape[2] - out_N)//2 - pred_mu = raw[:,:,:n_values] - pred_std = safe_exp(raw[:,:,n_values: 2*n_values]) - - if in_N > 1: - weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype) - for i in range(out_N): - weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1) - - if out_N == 1: - for fidx in range(weights.shape[0]): - idxs = np.argsort(weights[fidx][:,0])[::-1] - weights[fidx] = weights[fidx][idxs] - pred_mu[fidx] = pred_mu[fidx][idxs] - pred_std[fidx] = pred_std[fidx][idxs] - assert out_shape is not None - full_shape = tuple([raw.shape[0], in_N] + list(out_shape)) - outs[name + '_weights'] = weights - outs[name + '_hypotheses'] = pred_mu.reshape(full_shape) - outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape) - - pred_mu_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - pred_std_final = np.zeros((raw.shape[0], out_N, n_values), dtype=raw.dtype) - for fidx in range(weights.shape[0]): - for hidx in range(out_N): - idxs = np.argsort(weights[fidx,:,hidx])[::-1] - pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]] - pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]] - else: - pred_mu_final = pred_mu - pred_std_final = pred_std - - if out_N > 1: - assert out_shape is not None - final_shape = tuple([raw.shape[0], out_N] + list(out_shape)) - else: - assert out_shape is not None - final_shape = tuple([raw.shape[0],] + list(out_shape)) - outs[name] = pred_mu_final.reshape(final_shape) - outs[name + '_stds'] = pred_std_final.reshape(final_shape) - - def is_mhp(self, outs, name, shape): - if self.check_missing(outs, name): - return False - if outs[name].shape[1] == 2 * shape: - return False - return True - - def parse_dynamic_outputs(self, outs: dict[str, np.ndarray]) -> None: - if 'lead' in outs: - lead_mhp = self.is_mhp(outs, 'lead', - SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH) - lead_in_N, lead_out_N = (SplitModelConstants.LEAD_MHP_N, SplitModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0) - lead_out_shape = (SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH) if lead_mhp else \ - (SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH) - self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape) - if 'plan' in outs: - plan_mhp = self.is_mhp(outs, 'plan', SplitModelConstants.IDX_N * SplitModelConstants.PLAN_WIDTH) - plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) - self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, - out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) - if 'planplus' in outs: - self.parse_mdn('planplus', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) - - def split_outputs(self, outs: dict[str, np.ndarray]) -> None: - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) - if 'desire_pred' in outs: - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) - if 'desire_state' in outs: - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) - if 'lane_lines' in outs: - self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) - if 'lane_lines_prob' in outs: - self.parse_binary_crossentropy('lane_lines_prob', outs) - if 'lead_prob' in outs: - self.parse_binary_crossentropy('lead_prob', outs) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'meta' in outs: - self.parse_binary_crossentropy('meta', outs) - if 'road_edges' in outs: - self.parse_mdn('road_edges', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) - if 'sim_pose' in outs: - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) - if 'action' in outs: - self.parse_mdn('action', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.ACTION_WIDTH,)) - - def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) - self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,)) - self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) - self.parse_dynamic_outputs(outs) - self.split_outputs(outs) - return outs - - def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_dynamic_outputs(outs) - self.split_outputs(outs) - return outs - - def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - outs = self.parse_vision_outputs(outs) - outs = self.parse_policy_outputs(outs) - return outs diff --git a/openpilot/sunnypilot/modeld_v2/tests/helpers.py b/openpilot/sunnypilot/modeld_v2/tests/helpers.py index 82e159a305..8f148f6789 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/helpers.py +++ b/openpilot/sunnypilot/modeld_v2/tests/helpers.py @@ -117,7 +117,7 @@ ARCHETYPES = { is_20hz=True, expected_model_type='split', expected_constants_class=SplitModelConstants, - expected_parser_module='parse_model_outputs_split', + expected_parser_module='parse_model_outputs', expected_desire_key='desire', ), 'vision_multi_policy': Archetype( @@ -130,7 +130,7 @@ ARCHETYPES = { is_20hz=True, expected_model_type='multi_policy', expected_constants_class=SplitModelConstants, - expected_parser_module='parse_model_outputs_split', + expected_parser_module='parse_model_outputs', expected_desire_key='desire', ), 'tri_policy': Archetype( @@ -144,7 +144,7 @@ ARCHETYPES = { is_20hz=True, expected_model_type='multi_policy', expected_constants_class=SplitModelConstants, - expected_parser_module='parse_model_outputs_split', + expected_parser_module='parse_model_outputs', expected_desire_key='desire', ), 'supercombo_non20hz': Archetype( diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py index e3a686a73f..88cee8c5b4 100644 --- a/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py +++ b/openpilot/sunnypilot/modeld_v2/tests/test_combined_pkl_loader.py @@ -103,6 +103,23 @@ class TestStockEquivalence(OpenpilotTestCase): assert state.vision_output_slices == arch.metadata_structure['vision']['output_slices'] assert state.policy_output_slices == arch.metadata_structure['policy']['output_slices'] + def test_unified_run_model(self, tmp_path, monkeypatch, patch_modeld): + from openpilot.common.hardware import hw + from openpilot.selfdrive.modeld.helpers import dump_oob + shapes = {'img': (1, 12, 128, 256), 'big_img': (1, 12, 128, 256), 'features_buffer': (1, 24, 32, 512), + 'desire_pulse': (1, 25, 8), 'traffic_convention': (1, 2), 'action_t': (1, 2)} + pkl_data = {'metadata': {'model': {'input_shapes': shapes, 'output_slices': {}}}, + 'run_model': {(CAM_W, CAM_H): tests_helpers._noop_jit}} + with open(tmp_path / 'driving_test_tinygrad.pkl', 'wb') as f: + dump_oob(pkl_data, f) + bundle = DummyBundle(models=[DummyModel('supercombo', 'driving_test_tinygrad.pkl')]) + patch_modeld(bundle) + monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path))) + state = ModelState(cam_w=CAM_W, cam_h=CAM_H) + assert state.is_run_model and state.run_model is not None + assert state.run_policy is None and state.warp is None + assert 'img' in state.frame_views and 'big_img' in state.frame_views + ARCHETYPE_NAMES = list(ARCHETYPES.keys()) diff --git a/openpilot/sunnypilot/modeld_v2/tests/test_parse_model_outputs.py b/openpilot/sunnypilot/modeld_v2/tests/test_parse_model_outputs.py new file mode 100644 index 0000000000..821043124a --- /dev/null +++ b/openpilot/sunnypilot/modeld_v2/tests/test_parse_model_outputs.py @@ -0,0 +1,81 @@ +import numpy as np +from openpilot.common.test import OpenpilotTestCase +from openpilot.sunnypilot.modeld_v2.constants import ModelConstants +from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser, _infer_mhp, sigmoid, softmax + + +class TestParseModelOutputs(OpenpilotTestCase): + def test_infer_mhp_lead(self): + in_hypotheses, out_selections = _infer_mhp(102, 24) + assert in_hypotheses == 2 + assert out_selections == 3 + + def test_infer_mhp_plan(self): + in_hypotheses, out_selections = _infer_mhp(4955, 495) + assert in_hypotheses == 5 + assert out_selections == 1 + + def test_infer_mhp_non_mdn(self): + in_hypotheses, out_selections = _infer_mhp(48, 24) + assert in_hypotheses == 1 + assert out_selections == 0 + + def test_check_missing_raises(self): + parser = Parser(ignore_missing=False) + with self.assertRaises(ValueError): + parser.check_missing({}, "missing_key") + + def test_check_missing_ignored(self): + parser = Parser(ignore_missing=True) + assert parser.check_missing({}, "missing_key") is True + + def test_binary_crossentropy(self): + parser = Parser() + raw_logits = np.array([[-10.0, 0.0, 10.0]], dtype=np.float32) + outs = {"meta": raw_logits.copy()} + parser.parse_binary_crossentropy("meta", outs) + expected_probabilities = sigmoid(raw_logits) + np.testing.assert_allclose(outs["meta"], expected_probabilities, rtol=1e-5, atol=1e-6) + + def test_categorical_crossentropy(self): + parser = Parser() + raw_logits = np.array([[1.0, 2.0, 3.0]], dtype=np.float32) + outs = {"desire_state": raw_logits.copy()} + parser.parse_categorical_crossentropy("desire_state", outs) + expected_probabilities = softmax(raw_logits) + np.testing.assert_allclose(outs["desire_state"], expected_probabilities, rtol=1e-5, atol=1e-6) + + def test_parse_vision_outputs(self): + parser = Parser() + pose_raw = np.zeros((1, ModelConstants.POSE_WIDTH * 2), dtype=np.float32) + road_transform_raw = np.zeros((1, ModelConstants.POSE_WIDTH * 2), dtype=np.float32) + lead_raw = np.zeros((1, 102), dtype=np.float32) + meta_raw = np.zeros((1, 55), dtype=np.float32) + vision_outputs = {"pose": pose_raw, "road_transform": road_transform_raw, "lead": lead_raw, "meta": meta_raw} + parsed = parser.parse_vision_outputs(vision_outputs) + assert "pose" in parsed + assert "road_transform" in parsed + assert "lead" in parsed + assert "meta" in parsed + assert parsed["pose"].shape == (1, ModelConstants.POSE_WIDTH) + assert parsed["lead"].shape == (1, ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH) + + def test_parse_policy_outputs(self): + parser = Parser() + plan_raw = np.zeros((1, 4955), dtype=np.float32) + desire_state_raw = np.zeros((1, ModelConstants.DESIRE_PRED_WIDTH), dtype=np.float32) + action_raw = np.zeros((1, ModelConstants.ACTION_WIDTH * 2), dtype=np.float32) + policy_outputs = {"plan": plan_raw, "desire_state": desire_state_raw, "action": action_raw} + parsed = parser.parse_policy_outputs(policy_outputs) + assert parsed["plan"].shape == (1, ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH) + assert parsed["action"].shape == (1, ModelConstants.ACTION_WIDTH) + assert parsed["desire_state"].shape == (1, ModelConstants.DESIRE_PRED_WIDTH) + + def test_parse_outputs_combined(self): + parser = Parser() + outputs = {"plan": np.zeros((1, 4955), dtype=np.float32), "pose": np.zeros((1, ModelConstants.POSE_WIDTH * 2), + dtype=np.float32), "meta": np.zeros((1, 55), dtype=np.float32)} + parsed = parser.parse_outputs(outputs) + assert parsed["plan"].shape == (1, ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH) + assert parsed["pose"].shape == (1, ModelConstants.POSE_WIDTH) + assert parsed["meta"].shape == (1, 55) diff --git a/openpilot/sunnypilot/models/constants.py b/openpilot/sunnypilot/models/constants.py deleted file mode 100644 index 18abc6c960..0000000000 --- a/openpilot/sunnypilot/models/constants.py +++ /dev/null @@ -1,121 +0,0 @@ -import numpy as np - -def index_function(idx, max_val=192, max_idx=32): - return max_val * ((idx/max_idx)**2) - - -class ModelConstants: - # time and distance indices - IDX_N = 33 - T_IDXS = [index_function(idx, max_val=10.0) for idx in range(IDX_N)] - X_IDXS = [index_function(idx, max_val=192.0) for idx in range(IDX_N)] - LEAD_T_IDXS = [0., 2., 4., 6., 8., 10.] - LEAD_T_OFFSETS = [0., 2., 4.] - META_T_IDXS = [2., 4., 6., 8., 10.] - - # model inputs constants - MODEL_FREQ = 20 - FEATURE_LEN = 512 - HISTORY_BUFFER_LEN = 99 - DESIRE_LEN = 8 - TRAFFIC_CONVENTION_LEN = 2 - NAV_FEATURE_LEN = 256 - NAV_INSTRUCTION_LEN = 150 - LAT_PLANNER_STATE_LEN = 4 - LATERAL_CONTROL_PARAMS_LEN = 2 - PREV_DESIRED_CURV_LEN = 1 - - # model outputs constants - FCW_THRESHOLDS_5MS2 = np.array([.05, .05, .15, .15, .15], dtype=np.float32) - FCW_THRESHOLDS_3MS2 = np.array([.7, .7], dtype=np.float32) - FCW_5MS2_PROBS_WIDTH = 5 - FCW_3MS2_PROBS_WIDTH = 2 - - DISENGAGE_WIDTH = 5 - POSE_WIDTH = 6 - WIDE_FROM_DEVICE_WIDTH = 3 - SIM_POSE_WIDTH = 6 - LEAD_WIDTH = 4 - LANE_LINES_WIDTH = 2 - ROAD_EDGES_WIDTH = 2 - PLAN_WIDTH = 15 - DESIRE_PRED_WIDTH = 8 - LAT_PLANNER_SOLUTION_WIDTH = 4 - DESIRED_CURV_WIDTH = 1 - - NUM_LANE_LINES = 4 - NUM_ROAD_EDGES = 2 - - LEAD_TRAJ_LEN = 6 - DESIRE_PRED_LEN = 4 - - PLAN_MHP_N = 5 - LEAD_MHP_N = 2 - PLAN_MHP_SELECTION = 1 - LEAD_MHP_SELECTION = 3 - - FCW_THRESHOLD_5MS2_HIGH = 0.15 - FCW_THRESHOLD_5MS2_LOW = 0.05 - FCW_THRESHOLD_3MS2 = 0.7 - - CONFIDENCE_BUFFER_LEN = 5 - RYG_GREEN = 0.01165 - RYG_YELLOW = 0.06157 - - POLY_PATH_DEGREE = 4 - - -# model outputs slices -class Plan: - POSITION = slice(0, 3) - VELOCITY = slice(3, 6) - ACCELERATION = slice(6, 9) - T_FROM_CURRENT_EULER = slice(9, 12) - ORIENTATION_RATE = slice(12, 15) - - -class Meta: - ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 31, 6) - BRAKE_DISENGAGE = slice(2, 31, 6) - STEER_OVERRIDE = slice(3, 31, 6) - HARD_BRAKE_3 = slice(4, 31, 6) - HARD_BRAKE_4 = slice(5, 31, 6) - HARD_BRAKE_5 = slice(6, 31, 6) - # next 0, 2, 4, 6, 8, 10 seconds - GAS_PRESS = slice(31, 55, 4) - BRAKE_PRESS = slice(32, 55, 4) - LEFT_BLINKER = slice(33, 55, 4) - RIGHT_BLINKER = slice(34, 55, 4) - - -class MetaTombRaider: - ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 41, 8) - BRAKE_DISENGAGE = slice(2, 41, 8) - STEER_OVERRIDE = slice(3, 41, 8) - HARD_BRAKE_3 = slice(4, 41, 8) - HARD_BRAKE_4 = slice(5, 41, 8) - HARD_BRAKE_5 = slice(6, 41, 8) - GAS_PRESS = slice(7, 41, 8) - BRAKE_PRESS = slice(8, 41, 8) - # next 0, 2, 4, 6, 8, 10 seconds - LEFT_BLINKER = slice(41, 53, 2) - RIGHT_BLINKER = slice(42, 53, 2) - - -class MetaSimPose: - ENGAGED = slice(0, 1) - # next 2, 4, 6, 8, 10 seconds - GAS_DISENGAGE = slice(1, 36, 7) - BRAKE_DISENGAGE = slice(2, 36, 7) - STEER_OVERRIDE = slice(3, 36, 7) - HARD_BRAKE_3 = slice(4, 36, 7) - HARD_BRAKE_4 = slice(5, 36, 7) - HARD_BRAKE_5 = slice(6, 36, 7) - GAS_PRESS = slice(7, 36, 7) - # next 0, 2, 4, 6, 8, 10 seconds - LEFT_BLINKER = slice(36, 48, 2) - RIGHT_BLINKER = slice(37, 48, 2) diff --git a/openpilot/sunnypilot/models/fetcher.py b/openpilot/sunnypilot/models/fetcher.py index 810b9b7e49..16fa378c4e 100644 --- a/openpilot/sunnypilot/models/fetcher.py +++ b/openpilot/sunnypilot/models/fetcher.py @@ -139,7 +139,7 @@ class ModelCache: class ModelFetcher: """Handles fetching and caching of model data from remote source""" MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v22.json" - MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v24.json" + MODEL_URL_CHESTNUT = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_chestnut_v25.json" MODEL_SOURCES = { "qcom": (MODEL_URL, ""), diff --git a/openpilot/sunnypilot/models/helpers.py b/openpilot/sunnypilot/models/helpers.py index ac69096597..a1a392c5ee 100644 --- a/openpilot/sunnypilot/models/helpers.py +++ b/openpilot/sunnypilot/models/helpers.py @@ -7,14 +7,11 @@ See the LICENSE.md file in the root directory for more details. import hashlib import os -import pickle -from pathlib import Path import numpy as np from openpilot.cereal import custom from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog -from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider from openpilot.common.hardware.hw import Paths from openpilot.selfdrive.modeld.helpers import chestnut_present @@ -22,7 +19,6 @@ from openpilot.selfdrive.modeld.helpers import chestnut_present REQUIRED_JSON_VERSION = 19 CUSTOM_MODEL_PATH = Paths.model_root() -METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl' ModelManager = custom.ModelManagerSP ACTIVE_BUNDLE_KEYS = { @@ -201,33 +197,6 @@ def _get_model(): return None -def load_metadata(): - metadata_path = METADATA_PATH - - with open(metadata_path, 'rb') as f: - return pickle.load(f) - - -def prepare_inputs(model_metadata: dict) -> dict[str, np.ndarray]: - return { - key: np.zeros(shape, dtype=np.float32).flatten() - for key, shape in model_metadata['input_shapes'].items() - if 'img' not in key - } - - -def load_meta_constants(model_metadata: dict): - """ Loads the appropriate meta model class based on key shapes""" - if 'sim_pose' in model_metadata['input_shapes']: - return MetaSimPose - - meta_slice = model_metadata['output_slices']['meta'] - if (meta_slice.start, meta_slice.stop, meta_slice.step) == (5868, 5921, None): - return MetaTombRaider - - return Meta - - # The following method(s) are modeld helper methods def plan_x_idxs_helper(constants, plan, model_output) -> list[float]: # times at X_IDXS according to plan.