Compare commits

..

3 Commits

Author SHA1 Message Date
Jason Wen 3a05c03079 ci: no more docker (#1886) 2026-07-25 15:34:36 -04:00
Eitan fd22de1c9a SCC-M: fix operator precedence in quadratic roots (#1816)
* controls/scc: fix operator precedence in map controller quadratic roots

* not async

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-07-25 09:37:18 -04:00
Christopher Haucke ee3583df33 [tizi/tici] ui: Camera offset controls (#1813)
* Camera offset controls

* final

---------

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