Compare commits

..

15 Commits

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