Compare commits

..

6 Commits

Author SHA1 Message Date
discountchubbs df83374927 modeld_v2: Support eGpu 2026-08-05 13:51:29 -07:00
Nayan 5bdc0c23a9 modeld_v2: Support Deep Models (#1887)
* modeld_v2: safe model validation

* fix string

* numpy

* dumb

* god use full attribute names please

* modeld_v2: refactor compile_modeld

* redundant

* CREAM AND SUGAR

* gpu stuffs

* Update fetcher.py

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* summary

* Update compile_modeld.py

* Update compile_modeld.py

* Update compile_modeld.py

* i could lie say

* needed

* i could

* Update compile_modeld.py

* done done done

* bye metadata

* simplify

* deeeeep

* oopsie

* i don't know what i'm doing

* it's a supercombo

* wtf. ghostwriter

* i might be blind

* fuck. i AM blind. or dumb. or both.

* hmmmm

* read/open - what's the difference

* realize. that i don't know shit

* whatever

* fix paths

* fuckit. dynamic everything.

* simplify everything.

* fix macos cabana

* np.random.seed is deprecated, use Generator or default rng instead @nayan

* this is annoying me

* too much fluff

* no

* lint be crazy now, man i've been away a while

* back

* fix pkl loader test

* that comment was wrong, its still per model, just compiled at with the input/output shapes

* fix chunking

* dump and assign to cpu

* prev_feat in cpu to prevent corruption

* add todo-sp

* desire me?

* allow legacy

* lint

* shapey

* bye

---------

Co-authored-by: discountchubbs <alexgrant990@gmail.com>
Co-authored-by: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-05 12:30:28 -07:00
Amy Jeanes 1a07e47228 Tesla: MADS Screen Activation (#1808)
* Tesla: MADS Screen Button Settings

Adds a Tesla vehicle setting (vehicle bus required) to control how many
fingers activate the MADS screen button, or disable it entirely.

Rebased onto current master:
- Migrated the setting metadata from the deprecated params_metadata.json
  (removed in #1862) to the yaml SDUI system: added TeslaMadsScreenButton
  to settings_ui_src/pages/vehicle.yaml and recompiled settings_ui.json.
  Vehicle-bus gating uses the tesla_has_vehicle_bus capability visibility.
- Bumped opendbc_repo to the latest head of sunnypilot/opendbc#459.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JiSH2KAAueUmDe29xvpuAd

* bump

* Tesla: address MADS Screen Activation review feedback

Default TeslaMadsScreenButton to Off for fresh installs and add a param
migration that seeds existing Tesla installs with 3-Finger, preserving the
previous always-on behaviour. Brand resolves from CarPlatformBundle, falling
back to CarParamsPersistent so auto-fingerprinted Teslas are covered too.

Rename the setting to "MADS Screen Activation", hyphenate the finger-count
labels, and reword the description to use <br> (descriptions render as HTML)
with a note that a higher finger count may reduce accidental activations.
Applied both on-device and in sunnylink.

Also fix test_tesla_with_vehicle_bus_uses_param, which broke once
get_mads_limited_brands started reading TeslaMadsScreenButton from the same
blanket params mock, and add coverage for the screen-button-Off path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5icDp7zZ49gpyC2mpCfF1

* bump opendbc

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-08-01 23:40:52 -04:00
Jason Wen 50b860c928 Reapply "plannerd: check all services for validity (#38341)"
This reverts commit 0265ae5f76.
2026-08-01 23:27:36 -04:00
Jason Wen 978ec800fe plannerd & selfdriveStateSP: poll modelV2, relay button state via bitmask (#1893)
* selfdrived: continuous button state and release counters in selfdriveStateSP

* tests and more
2026-08-01 23:05:03 -04:00
Jason Wen 3a05c03079 ci: no more docker (#1886) 2026-07-25 15:34:36 -04:00
37 changed files with 774 additions and 417 deletions
@@ -34,6 +34,14 @@ on:
required: false
default: true
type: boolean
target_hardware:
description: 'Hardware target to compile for'
required: false
type: choice
default: 'qcom'
options:
- qcom
- usbgpu
workflow_dispatch:
inputs:
upstream_branch:
@@ -81,9 +89,17 @@ on:
description: 'Minimum selector version'
required: false
type: string
target_hardware:
description: 'Hardware target to compile for'
required: false
type: choice
default: 'qcom'
options:
- qcom
- usbgpu
env:
RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }}
JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json
JSON_FILE: docs/docs/driving_models_${{ inputs.target_hardware == 'usbgpu' && 'usbgpu_v' || 'v' }}${{ inputs.json_version }}.json
jobs:
build_model:
@@ -93,6 +109,7 @@ jobs:
custom_name: ${{ inputs.custom_name || inputs.upstream_branch }}
is_20hz: ${{ inputs.is_20hz }}
artifact_suffix: ${{ inputs.artifact_suffix }}
target_hardware: ${{ inputs.target_hardware }}
secrets: inherit
publish_model:
-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"
@@ -191,7 +191,7 @@ jobs:
if [ "${{ inputs.target_hardware }}" == "usbgpu" ]; then
echo "USBGPU build"
export USBGPU=1
TG_FLAGS="DEV=AMD USBGPU=1 IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1"
TG_FLAGS="DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV=QCOM FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0"
OUTPUT_PKL="${{ env.MODELS_DIR }}/big_driving_tinygrad.pkl"
else
echo "QCOM build"
+2
View File
@@ -69,6 +69,8 @@ struct LeadData {
struct SelfdriveStateSP @0x81c2f05a394cf4af {
mads @0 :ModularAssistiveDrivingSystem;
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
buttonsPressed @2 :UInt16;
buttonsReleaseToggle @3 :UInt16;
enum AudibleAlert {
none @0;
+3
View File
@@ -197,7 +197,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT}},
{"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}},
{"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_LastSyncTime_USBGPU", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}},
{"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}},
{"ModelManager_ModelsCache_USBGPU", {PERSISTENT | BACKUP, JSON}},
// Neural Network Lateral Control
{"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -222,6 +224,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -158,7 +158,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
def publish(self, sm, pm):
plan_send = messaging.new_message('longitudinalPlan')
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState'])
plan_send.valid = sm.all_checks()
longitudinalPlan = plan_send.longitudinalPlan
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
+4 -4
View File
@@ -29,19 +29,19 @@ def main():
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
'liveMapDataSP', 'carStateSP', gps_location_service],
poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service],
poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
while True:
sm.update()
longitudinal_planner.sla.update_car_state(sm['carState'])
longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle)
if sm.updated['modelV2']:
longitudinal_planner.update(sm)
longitudinal_planner.publish(sm, pm)
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
msg = messaging.new_message('driverAssistance')
msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters'])
msg.valid = sm.all_checks()
msg.driverAssistance.leftLaneDeparture = ldw.left
msg.driverAssistance.rightLaneDeparture = ldw.right
pm.send('driverAssistance', msg)
@@ -3,7 +3,6 @@ import argparse
import atexit
import math
import os
import pickle
import tempfile
import time
import shutil
@@ -13,7 +12,6 @@ from collections import namedtuple
import numpy as np
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
def _patch_tinygrad_fetch_fw():
import hashlib
@@ -31,22 +29,6 @@ def _patch_tinygrad_fetch_fw():
helpers.fetch_fw = fetch_fw
_patch_tinygrad_fetch_fw()
def _patch_tinygrad_buffer_reduce():
from tinygrad.device import Buffer
def __reduce_ex__(self, protocol):
buf = None
if self._base is not None:
return self.__class__, (self.device, self.size, self.dtype, None, None, None, 0, self.base, self.offset, self.is_allocated())
if self.device == "NPY":
return self.__class__, (self.device, self.size, self.dtype, self._buf, self.options, None, self.uop_refcount)
if self.is_allocated():
buf = bytearray(self.nbytes)
self.copyout(memoryview(buf))
if protocol >= 5:
buf = pickle.PickleBuffer(buf)
return self.__class__, (self.device, self.size, self.dtype, None, self.options, buf, self.uop_refcount)
Buffer.__reduce_ex__ = __reduce_ex__
_patch_tinygrad_buffer_reduce()
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
@@ -312,9 +294,6 @@ if __name__ == "__main__":
p.add_argument('--frame-skip', type=int, required=True)
args = p.parse_args()
if 'USB+AMD' in os.environ.get('DEV', ''):
wait_usbgpu_link()
model_path = read_file_chunked_to_disk(args.onnx)
model_w, model_h = args.model_size
+12 -12
View File
@@ -6,10 +6,11 @@ import struct
import tempfile
from pathlib import Path
from openpilot.common.file_chunker import get_manifest_path
from openpilot.common.hardware.usb import CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID, USB_DEVICES_PATH
MODELS_DIR = Path(__file__).resolve().parent / 'models'
TG_INPUT_DEVICES_PATH = MODELS_DIR / 'tg_input_devices.json'
USBGPU_VID = 0xADD1
USBGPU_PID = 0x0001
def get_tg_input_devices(process_name: str, usbgpu: bool):
@@ -38,22 +39,21 @@ def dump_oob(obj, f):
def load_oob(f):
opcodes = f.read(struct.unpack('<q', f.read(8))[0])
def buffers():
prev = None
while (h := f.read(8)):
if prev is not None:
prev.release()
buf = bytearray(struct.unpack('<q', h)[0])
f.readinto(buf)
prev = pickle.PickleBuffer(buf)
yield prev
pb = pickle.PickleBuffer(bytearray(struct.unpack('<q', h)[0]))
f.readinto(pb)
yield pb
return pickle.load(io.BytesIO(opcodes), buffers=buffers())
def usbgpu_present() -> bool:
for d in Path("/sys/bus/usb/devices").glob("*"):
for d in USB_DEVICES_PATH.glob("*"):
try:
if int((d / "idVendor").read_text(), 16) == USBGPU_VID and \
int((d / "idProduct").read_text(), 16) == USBGPU_PID:
usb_id = (int((d / "idVendor").read_text(), 16), int((d / "idProduct").read_text(), 16))
if usb_id == (CHESTNUT_VENDOR_ID, CHESTNUT_PRODUCT_ID):
return True
except Exception:
pass
return False
def usbgpu_compiled() -> bool:
return Path(get_manifest_path(modeld_pkl_path(usbgpu=True))).is_file()
+59 -32
View File
@@ -2,6 +2,7 @@
import os
os.environ['GMMU'] = '0' # for usbgpu fast loading, noop for qcom
from tinygrad.tensor import Tensor
import threading
import time
import numpy as np
import openpilot.cereal.messaging as messaging
@@ -18,17 +19,13 @@ from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, should_stop, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues, WARP_INPUTS, POLICY_INPUTS
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_driving_model_data, fill_pose_msg, PublishState
from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
from openpilot.common.file_chunker import open_file_chunked
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
from openpilot.selfdrive.modeld.helpers import usbgpu_present, usbgpu_compiled, modeld_pkl_path, get_tg_input_devices, load_oob
PROCESS_NAME = "openpilot.selfdrive.modeld.modeld"
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
@@ -36,16 +33,17 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3
BIG_MODEL_TIMEOUT = 60
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
if 'action' not in model_output:
plan = model_output['plan'][0]
desired_accel, should_stop = get_accel_from_plan(plan[:,Plan.VELOCITY][:,0],
plan[:,Plan.ACCELERATION][:,0],
ModelConstants.T_IDXS,
action_t=long_action_t)
desired_accel = get_accel_from_plan(plan[:,Plan.VELOCITY][:,0],
plan[:,Plan.ACCELERATION][:,0],
ModelConstants.T_IDXS,
action_t=long_action_t)
desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2],
plan[:,Plan.ORIENTATION_RATE][:,2],
ModelConstants.T_IDXS,
@@ -54,7 +52,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
else:
desired_accel = model_output['action'][0,1]
desired_curvature = model_output['action'][0,0] / (max(1.0, v_ego))**2
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
stop = should_stop(v_ego, desired_accel)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS)
if v_ego > MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS)
@@ -63,7 +61,7 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.
return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),
desiredAcceleration=float(desired_accel),
shouldStop=bool(should_stop))
shouldStop=bool(stop))
class FrameMeta:
@@ -76,12 +74,10 @@ class FrameMeta:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
class ModelState(ModelStateBase):
class ModelState:
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
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)))
@@ -138,16 +134,24 @@ class ModelState(ModelStateBase):
outputs_dict['raw_pred'] = model_output.copy()
return outputs_dict
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self.vision_input_names}
eye = np.eye(3, dtype=np.float32)
dims = {'desire_pulse': ModelConstants.DESIRE_LEN, 'traffic_convention': 2, 'action_t': 2}
self.run(dummy_frames, dict.fromkeys(self.vision_input_names, eye), {k: np.zeros(v, dtype=np.float32) for k, v in dims.items()})
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
def main(demo=False):
cloudlog.warning("modeld init")
_present = usbgpu_present()
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
USBGPU = _present and _compiled
USBGPU = usbgpu_present() and usbgpu_compiled()
params = Params()
params.put_bool("UsbGpuPresent", _present)
params.put_bool("UsbGpuCompiled", _compiled)
params.put_bool("UsbGpuLoading", USBGPU)
params.remove("UsbGpuActive")
config_realtime_process(7, 54)
@@ -174,15 +178,33 @@ def main(demo=False):
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
if USBGPU:
wait_usbgpu_link()
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
model = None
if USBGPU:
big_model = None
def load_big():
nonlocal big_model
try:
m = ModelState(vipc_client_main.width, vipc_client_main.height, True)
m.warmup()
big_model = m
except Exception:
cloudlog.exception("big model load failed")
loader = threading.Thread(target=load_big, daemon=True)
loader.start()
loader.join(BIG_MODEL_TIMEOUT)
model = big_model
params.put_bool("UsbGpuActive", model is not None)
small_model = ModelState(vipc_client_main.width, vipc_client_main.height, False) if model is None or USBGPU else None
if model is None:
model = small_model
params.put_bool("UsbGpuLoading", False)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
publish_state = PublishState()
@@ -252,9 +274,7 @@ def main(demo=False):
is_rhd = sm["driverMonitoringState"].isRHD
frame_id = sm["roadCameraState"].frameId
v_ego = max(sm["carState"].vEgo, 0.)
if sm.frame % 60 == 0:
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS
lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
@@ -293,7 +313,17 @@ def main(demo=False):
}
mt1 = time.perf_counter()
model_output = model.run(bufs, transforms, inputs)
try:
model_output = model.run(bufs, transforms, inputs)
except Exception:
if not params.get_bool("UsbGpuActive"):
raise
# fallback to small model
cloudlog.exception("big model failed, fall back to small")
params.put_bool("UsbGpuActive", False)
model = small_model
run_count = 0
model_output = None
mt2 = time.perf_counter()
model_execution_time = mt2 - mt1
@@ -301,7 +331,6 @@ def main(demo=False):
modelv2_send = messaging.new_message('modelV2')
drivingdata_send = messaging.new_message('drivingModelData')
posenet_send = messaging.new_message('cameraOdometry')
mdv2sp_send = messaging.new_message('modelDataV2SP')
action = get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
prev_action = action
@@ -316,14 +345,12 @@ def main(demo=False):
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob)
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
fill_driving_model_data(drivingdata_send, modelv2_send)
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen)
pm.send('modelV2', modelv2_send)
pm.send('drivingModelData', drivingdata_send)
pm.send('cameraOdometry', posenet_send)
pm.send('modelDataV2SP', mdv2sp_send)
last_vipc_frame_id = meta_main.frame_id
@@ -30,6 +30,7 @@ from openpilot.sunnypilot import get_sanitize_int_param
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
REPLAY = "REPLAY" in os.environ
@@ -177,6 +178,7 @@ class SelfdriveD(CruiseHelper):
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
CruiseHelper.__init__(self, self.CP)
self.button_state_tracker = ButtonStateTracker()
def update_events(self, CS):
"""Compute onroadEvents from carState"""
@@ -597,6 +599,8 @@ class SelfdriveD(CruiseHelper):
icbm.sendButton = self.icbm.cruise_button
icbm.vTarget = self.icbm.v_target
self.button_state_tracker.publish(ss_sp)
self.pm.send('selfdriveStateSP', ss_sp_msg)
# onroadEventsSP - logged every second or on change
@@ -616,6 +620,7 @@ class SelfdriveD(CruiseHelper):
self.mads.update(CS)
self.update_alerts(CS)
self.button_state_tracker.update(CS)
self.publish_selfdriveState(CS)
self.CS_prev = CS
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
from collections.abc import Callable
import pyray as rl
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
from openpilot.system.ui.lib.multilang import tr, tr_noop
@@ -96,7 +96,10 @@ class MadsSettingsLayout(Widget):
if brand == "rivian":
return True
elif brand == "tesla":
return not (ui_state.CP_SP is not None and ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
return True
screen_button = int(ui_state.params.get("TeslaMadsScreenButton", return_default=True))
return screen_button == MadsScreenButtonType.OFF
return False
def _update_steering_mode_description(self, button_index: int):
@@ -4,10 +4,11 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp
COOP_STEERING_MIN_KMH = 23
OEM_STEERING_MIN_KMH = 48
@@ -18,7 +19,14 @@ class TeslaSettings(BrandSettings):
def __init__(self):
super().__init__()
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
self.items = [self.coop_steering_toggle]
self.mads_screen_button = multiple_button_item_sp(
title=lambda: tr("MADS Screen Activation"),
description="",
buttons=[lambda: tr("Off"), lambda: tr("3-Finger"), lambda: tr("4-Finger"), lambda: tr("5-Finger")],
param="TeslaMadsScreenButton",
inline=False,
)
self.items = [self.coop_steering_toggle, self.mads_screen_button]
def update_settings(self):
is_metric = ui_state.is_metric
@@ -41,3 +49,18 @@ class TeslaSettings(BrandSettings):
self.coop_steering_toggle.set_description(coop_steering_desc)
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
has_vehicle_bus = ui_state.CP_SP is not None and bool(ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
self.mads_screen_button.set_visible(has_vehicle_bus)
mads_screen_button_desc = (
f"{tr('Use a multi-finger press on the infotainment screen to toggle MADS.')} " +
f"{tr('This allows the use of full MADS functionality when enabled.')}<br><br>" +
f"{tr('Selecting a higher finger count may reduce accidental activations.')}<br><br>" +
f"<b>{tr('Note: Setting this to Off will reset your MADS settings to default.')}</b>"
)
if not ui_state.is_offroad():
mads_screen_button_disabled_msg = tr("Enable \"Always Offroad\" in Device panel, or turn vehicle off to change.")
mads_screen_button_desc = f"<b>{mads_screen_button_disabled_msg}</b><br><br>{mads_screen_button_desc}"
self.mads_screen_button.set_description(mads_screen_button_desc)
self.mads_screen_button.action_item.set_enabled(ui_state.is_offroad())
+8 -5
View File
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
from opendbc.car import structs
from opendbc.safety import ALTERNATIVE_EXPERIENCE
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla")
@@ -21,17 +21,20 @@ class MadsSteeringModeOnBrake:
DISENGAGE = 2
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> bool:
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params) -> bool:
if CP.brand == 'rivian':
return True
if CP.brand == 'tesla':
return not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS
if not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
return True
screen_button = int(params.get("TeslaMadsScreenButton", return_default=True))
return screen_button == MadsScreenButtonType.OFF
return False
def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
if get_mads_limited_brands(CP, CP_SP):
if get_mads_limited_brands(CP, CP_SP, params):
return MadsSteeringModeOnBrake.DISENGAGE
return params.get("MadsSteeringMode", return_default=True)
@@ -63,7 +66,7 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p
# MADS is currently partially supported for these platforms due to lack of consistent states to engage controls
# Only MadsSteeringModeOnBrake.DISENGAGE is supported for these platforms
# TODO-SP: To enable MADS full support for Rivian and most Tesla, identify consistent signals for MADS toggling
mads_partial_support = get_mads_limited_brands(CP, CP_SP)
mads_partial_support = get_mads_limited_brands(CP, CP_SP, params)
if mads_partial_support:
params.put("MadsSteeringMode", 2, block=True)
params.put_bool("MadsUnifiedEngagementMode", True, block=True)
@@ -13,7 +13,7 @@ from openpilot.selfdrive.selfdrived.events import Events
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
EventName = log.OnroadEvent.EventName
@@ -38,6 +38,12 @@ def make_panda_state(mocker, controls_allowed_lateral=True):
return ps
def make_params_mock(mocker, values):
params = mocker.MagicMock()
params.get = mocker.MagicMock(side_effect=lambda k, **kwargs: values[k])
return params
def make_mads(mocker, steering_mode):
sd = mocker.MagicMock()
sd.CP = structs.CarParams()
@@ -223,15 +229,27 @@ class TestBrandSteeringModeRestrictions:
params = mocker.MagicMock()
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
def test_tesla_with_vehicle_bus_uses_param(self, mocker):
@pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER,
MadsScreenButtonType.FOUR_FINGER,
MadsScreenButtonType.FIVE_FINGER])
def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button):
CP = structs.CarParams()
CP.brand = "tesla"
CP_SP = structs.CarParamsSP()
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
params = mocker.MagicMock()
params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE)
params = make_params_mock(mocker, {"TeslaMadsScreenButton": screen_button,
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.REMAIN_ACTIVE
def test_tesla_with_vehicle_bus_screen_button_off_forced_to_disengage(self, mocker):
CP = structs.CarParams()
CP.brand = "tesla"
CP_SP = structs.CarParamsSP()
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
params = make_params_mock(mocker, {"TeslaMadsScreenButton": MadsScreenButtonType.OFF,
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
@pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"])
def test_other_brands_use_param(self, mocker, brand):
CP = structs.CarParams()
+40 -32
View File
@@ -1,9 +1,12 @@
import os
import glob
import sys
import subprocess
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
from openpilot.common.hardware import HARDWARE, PC
from openpilot.selfdrive.modeld.helpers import usbgpu_present
Import('env', 'arch', 'release')
lenv = env.Clone()
@@ -22,14 +25,21 @@ def get_camera_configs():
CAMERA_CONFIGS = get_camera_configs()
tg_flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")}',
}.get(arch, 'DEV=CPU:LLVM')
def probe_devices():
return set(subprocess.run(
[sys.executable, '-c', 'from tinygrad import Device\nprint("\\n".join(Device.get_available_devices()))'],
capture_output=True, text=True, check=True).stdout.strip().splitlines())
image_flag = {
'larch64': 'IMAGE=2',
}.get(arch, 'IMAGE=0')
available = probe_devices()
if 'CUDA' in available:
tg_backend = 'CUDA'
tg_flags = f'DEV={tg_backend}'
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'
else:
tg_backend = 'CPU'
tg_flags = f'DEV=CPU HOME={os.path.expanduser("~")}' if arch == 'Darwin' else 'DEV=CPU:LLVM'
model_w, model_h = MEDMODEL_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants
@@ -41,17 +51,30 @@ compile_modeld_script = File("compile_modeld.py").abspath
upstream_compile_script = File(Dir("#openpilot/selfdrive/modeld").File("compile_modeld.py").abspath)
script_deps = [File("compile_modeld.py"), upstream_compile_script]
USBGPU = usbgpu_present()
if USBGPU:
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
usbgpu_lock = File("models/.usb_gpu.lock").abspath
def compile_combined(model_type, onnx_args, output_name):
output_pkl = File(f"models/{output_name}").abspath
cmd = (f'{pythonpath_string} {tg_flags} {image_flag} python3 {compile_modeld_script} '
f'--model-type {model_type} '
f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} '
f'{onnx_args} '
f'--frame-skip {frame_skip} '
f'--output {output_pkl}')
onnx_files = [f for f in onnx_args.split() if f.endswith('.onnx')]
return lenv.Command(output_pkl, tinygrad_files + script_deps + [File(f) for f in onnx_files if os.path.isfile(f)], cmd)
for usbgpu in ([False, True] if USBGPU else [False]):
prefix = 'big_' if usbgpu else ('big_' if os.getenv('BIG_INTO_SMALL') else '')
final_output_name = prefix + output_name
output_pkl = File(f"models/{final_output_name}").abspath
active_tg_flags = usbgpu_tg_flags if usbgpu else tg_flags
cmd = (f'{pythonpath_string} {active_tg_flags} python3 {compile_modeld_script} '
f'--model-type {model_type} '
f'--model-size {model_w}x{model_h} '
f'--camera-resolutions {camera_res_args} '
f'{onnx_args} '
f'--frame-skip {frame_skip} '
f'--output {output_pkl}')
onnx_files = [f for f in onnx_args.split() if f.endswith('.onnx')]
node = lenv.Command(output_pkl, tinygrad_files + script_deps + [File(f) for f in onnx_files if os.path.isfile(f)], cmd)
if usbgpu:
lenv.SideEffect(usbgpu_lock, node)
# Vision + Policy (stock default model)
vision_onnx = File("models/driving_vision.onnx").abspath
@@ -82,18 +105,3 @@ if os.path.isfile(supercombo_onnx):
compile_combined('supercombo',
f'--supercombo-onnx {supercombo_onnx}',
'driving_combined_supercombo_tinygrad.pkl')
if PC:
inputs = tinygrad_files + [File(Dir("#openpilot/sunnypilot/modeld_v2").File("install_models_pc.py").abspath)]
outputs = []
model_dir = Dir("models").abspath
cmd = f'python3 {Dir("#openpilot/sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
if File(f"models/{model_name}.onnx").exists():
inputs.append(File(f"models/{model_name}.onnx"))
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
outputs.append(File(f"models/{model_name}_metadata.pkl"))
if outputs:
lenv.Command(outputs, inputs, cmd)
@@ -8,10 +8,10 @@ See the LICENSE.md file in the root directory for more details.
import argparse
import os
import pickle
import time
import tempfile
from collections import defaultdict
from functools import partial
from openpilot.selfdrive.modeld.helpers import dump_oob, load_oob
import numpy as np
os.environ['GMMU'] = '0'
@@ -57,7 +57,26 @@ def derive_frame_skip(vision_input_shapes: dict, policy_input_shapes: dict) -> i
return 1 if not features_buffer or features_buffer[1] >= 99 else 4
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
def get_policy_npy_shapes(input_shapes: dict, is_supercombo: bool = False) -> tuple[dict, list[int]]:
desire_key = _detect_desire_key(input_shapes)
shapes = {}
if desire_key:
shapes['desire'] = (input_shapes[desire_key][2],)
if is_supercombo and 'features_buffer' in input_shapes:
fb = input_shapes['features_buffer']
shapes['prev_feat'] = (fb[0], fb[2])
for key, shape in input_shapes.items():
if key not in (desire_key, 'features_buffer') and 'img' not in key:
shapes[key] = tuple(shape)
sizes = [int(np.prod(size)) for size in shapes.values()]
return shapes, sizes
def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT,
is_supercombo: bool = False) -> tuple[dict, dict]:
road_key, _ = _detect_vision_keys(input_shapes)
if not road_key:
raise ValueError("Vision road key missing from input shapes.")
@@ -74,36 +93,43 @@ def generate_queues_and_npy(input_shapes: dict, frame_skip: int, device: str = D
features_buffer = input_shapes.get('features_buffer')
npy_arrays = {
'desire': np.zeros(desire_shape[2], dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32)
}
for key, shape in input_shapes.items():
if key not in npy_arrays and 'img' not in key and key not in ('features_buffer', desire_key):
npy_arrays[key] = np.zeros(shape, dtype=np.float32)
shapes, sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
packed_npy_inputs = np.zeros(sum(sizes), dtype=np.float32)
split_indices = np.cumsum(sizes[:-1]) if len(sizes) > 1 else []
split_views = np.split(packed_npy_inputs, split_indices) if len(sizes) > 0 else []
for (k, s), v in zip(shapes.items(), split_views, strict=True):
npy_arrays[k] = v.reshape(s)
queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'desire_q': Tensor(np.zeros((frame_skip * desire_shape[1], desire_shape[0], desire_shape[2]),
dtype=np.float32), device=device).contiguous().realize()
dtype=np.float32), device=device).contiguous().realize(),
'packed_npy_inputs': Tensor(packed_npy_inputs, device='NPY').realize(),
}
if features_buffer:
queues['feat_q'] = Tensor(np.zeros((frame_skip * (features_buffer[1] - 1) + 1, features_buffer[0], features_buffer[2]),
dtype=np.float32), device=device).contiguous().realize()
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items()})
queues.update({key: Tensor(value, device='NPY').realize() for key, value in npy_arrays.items() if key in ('tfm', 'big_tfm')})
return queues, npy_arrays
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device)
def make_split_input_queues(vision_input_shapes: dict, policy_input_shapes: dict,
frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
return generate_queues_and_npy({**vision_input_shapes, **policy_input_shapes}, frame_skip, device, is_supercombo=False)
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int, device: str = Device.DEFAULT) -> tuple[dict, dict]:
return generate_queues_and_npy(input_shapes, frame_skip, device)
def make_supercombo_input_queues(input_shapes: dict, frame_skip: int,
device: str = Device.DEFAULT) -> tuple[dict, dict]:
return generate_queues_and_npy(input_shapes, frame_skip, device, is_supercombo=True)
def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, model_size: tuple[int, int],
@@ -118,22 +144,17 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode
if not desire_key or not road_key or not wide_key:
raise ValueError("Missing required vision or desire keys in input shapes.")
extra_keys = [key for key in input_shapes if key not in (desire_key, 'features_buffer', 'traffic_convention') and 'img' not in key]
is_supercombo = vision_runner is None
npy_shapes, npy_sizes = get_policy_npy_shapes(input_shapes, is_supercombo=is_supercombo)
def runner(img_q, big_img_q, feat_q, frame, big_frame, tfm, big_tfm, **kwargs):
def runner(img_q, big_img_q, feat_q, packed_npy_inputs, frame, big_frame, tfm, big_tfm, **kwargs):
desire_q = kwargs['desire_q']
desire = kwargs['desire']
traffic_convention = kwargs.get('traffic_convention')
npys = [tfm.to(Device.DEFAULT), big_tfm.to(Device.DEFAULT), desire.to(Device.DEFAULT)]
if traffic_convention is not None:
npys.append(traffic_convention.to(Device.DEFAULT))
packed_npy_inputs_dev = packed_npy_inputs.to(Device.DEFAULT)
tfm_dev = tfm.to(Device.DEFAULT)
big_tfm_dev = big_tfm.to(Device.DEFAULT)
extra_tensors = {key: kwargs[key].to(Device.DEFAULT) for key in extra_keys if key in kwargs}
Tensor.realize(*npys, *extra_tensors.values())
tfm_dev, big_tfm_dev, desire_dev = npys[:3]
traffic_conv_dev = npys[3] if traffic_convention is not None else None
Tensor.realize(packed_npy_inputs_dev, tfm_dev, big_tfm_dev)
img = shift_and_sample(img_q, frame_prepare(frame, tfm_dev).unsqueeze(0), sample_skip_fn).realize()
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm_dev).unsqueeze(0), sample_skip_fn).realize()
@@ -141,22 +162,37 @@ def create_jit_runner(vision_runner, policy_runners: list, nv12: NV12Frame, mode
if prepare_only:
return img, big_img
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize()
inputs = {desire_key: desire_buf, **extra_tensors}
unpacked_tensors = [tensor.reshape(shape) for tensor, shape in zip(packed_npy_inputs_dev.split(npy_sizes), npy_shapes.values(), strict=True)]
unpacked_dict = dict(zip(npy_shapes.keys(), unpacked_tensors, strict=True))
if traffic_conv_dev is not None:
inputs['traffic_convention'] = traffic_conv_dev
desire_dev = unpacked_dict['desire']
desire_buf = shift_and_sample(desire_q, desire_dev.reshape(1, 1, -1), sample_desire_fn).realize()
inputs = {desire_key: desire_buf}
for key, tensor_val in unpacked_dict.items():
if key not in ('desire', 'prev_feat'):
inputs[key] = tensor_val
if 'prev_feat' in unpacked_dict:
prev_feat_dev = unpacked_dict['prev_feat']
inputs['features_buffer'] = shift_and_sample(feat_q, prev_feat_dev.reshape(1, 1, -1), sample_skip_fn).realize()
if vision_runner:
vision_out_cast = next(iter(vision_runner({road_key: img, wide_key: big_img}).values())).cast('float32').realize()
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
if 'features_buffer' not in inputs:
new_feat = vision_out_cast[:, features_slice].reshape(1, -1).unsqueeze(0)
inputs['features_buffer'] = shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
policy_outs = [next(iter(pol_runner(inputs).values())).cast('float32').realize() for pol_runner in policy_runners]
return (vision_out_cast, *policy_outs) if len(policy_outs) > 1 else (vision_out_cast, policy_outs[0])
inputs.update({road_key: img, wide_key: big_img, 'features_buffer': sample_skip_fn(feat_q)})
inputs.update({road_key: img, wide_key: big_img})
if 'features_buffer' not in inputs:
inputs['features_buffer'] = sample_skip_fn(feat_q)
policy_out = next(iter(policy_runners[0](inputs).values())).cast('float32').realize()
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
if 'features_buffer' not in inputs and features_slice is not None:
new_feat = policy_out[:, features_slice].reshape(1, -1).unsqueeze(0)
shift_and_sample(feat_q, new_feat, sample_skip_fn).realize()
return policy_out
return runner
@@ -172,27 +208,34 @@ def compile_and_warmup(nv12: NV12Frame, model_size: tuple[int, int], prepare_onl
raise ValueError("Could not find vision, model, or policy metadata.")
features_slice = feat_meta['output_slices']['hidden_state']
WARP_DEV = 'CPU' if "USBGPU" in os.environ else Device.DEFAULT
WARP_DEV = os.getenv('WARP_DEV', Device.DEFAULT)
is_supercombo = vision_runner is None
run_func = create_jit_runner(vision_runner, policy_runners, nv12, model_size, features_slice, frame_skip, all_shapes, prepare_only)
run_jit = TinyJit(run_func, prune=True)
queues, npy_arrays = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT)
for i in range(3):
np.random.seed(42 + i)
def run_once(seed):
queues, npy = generate_queues_and_npy(all_shapes, frame_skip, Device.DEFAULT, is_supercombo=is_supercombo)
rng = np.random.default_rng(seed)
frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype=dtypes.uint8, device=WARP_DEV).realize()
for arr in npy_arrays.values():
arr[:] = np.random.randn(*arr.shape).astype(arr.dtype)
for value in npy.values():
value[:] = rng.standard_normal(value.shape).astype(value.dtype)
Device.default.synchronize()
start_time = time.perf_counter()
run_jit(**queues, frame=frame, big_frame=big_frame)
mid_time = time.perf_counter()
outs = run_jit(**queues, frame=frame, big_frame=big_frame)
Device.default.synchronize()
print(f" [{i + 1}/3] enqueue {(mid_time - start_time) * 1e3:6.2f} ms -- total {(time.perf_counter() - start_time) * 1e3:6.2f} ms")
return [np.copy(value.numpy()) for value in (outs if isinstance(outs, tuple) else [outs])] if outs is not None else []
return pickle.loads(pickle.dumps(run_jit)) if not prepare_only else run_jit
for i in range(3):
run_once(42 + i)
if not prepare_only:
baseline = run_once(42)
with tempfile.TemporaryFile(dir=".") as f:
dump_oob(run_jit, f)
f.seek(0)
run_jit = load_oob(f)
assert all(np.array_equal(baseline, deserialized) for baseline, deserialized in zip(baseline, run_once(42), strict=True)), "OOB pickling regression"
return run_jit
def _parse_size(size_str: str) -> tuple[int, int]:
@@ -289,7 +332,7 @@ if __name__ == "__main__":
vision_runner, policy_runners, output_data['metadata']))
with open(args.output, "wb") as file:
pickle.dump(output_data, file)
dump_oob(output_data, file)
pkl_size = os.path.getsize(args.output)
print(f"Saved combined JIT to {args.output} ({pkl_size / 1e6:.2f} MB)")
@@ -1,75 +0,0 @@
#!/usr/bin/env python3
import sys
import shutil
import pickle
import codecs
from pathlib import Path
from openpilot.common.hardware.hw import Paths
from openpilot.sunnypilot.modeld_v2.get_model_metadata import MetadataOnnxPBParser, get_name_and_shape, get_metadata_value_by_name
def generate_metadata_pkl(model_path, output_path):
try:
model = MetadataOnnxPBParser(model_path).parse()
output_slices = get_metadata_value_by_name(model, 'output_slices')
if not output_slices:
return False
metadata = {
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
'input_shapes': dict(get_name_and_shape(x) for x in model["graph"]["input"]),
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
}
with open(output_path, 'wb') as f:
pickle.dump(metadata, f)
return True
except Exception:
return False
def install_models(model_dir):
model_dir = Path(model_dir)
models = ["driving_off_policy", "driving_on_policy", "driving_vision"]
found_models = []
for model in models:
if (model_dir / f"{model}.onnx").exists():
found_models.append(model)
if not found_models:
return
try:
custom_name = input(f"Found models ({', '.join(found_models)}). Enter model short name (e.g. wmiv4): ").strip()
except EOFError:
return
if not custom_name:
print("No name provided, skipping installation.")
return
dest_dir = Path(Paths.model_root())
dest_dir.mkdir(parents=True, exist_ok=True)
for model in found_models:
onnx_path = model_dir / f"{model}.onnx"
tinygrad_pkl = model_dir / f"{model}_tinygrad.pkl"
metadata_pkl = model_dir / f"{model}_metadata.pkl"
if not metadata_pkl.exists():
generate_metadata_pkl(onnx_path, metadata_pkl)
dest_tinygrad = dest_dir / f"{model}_{custom_name}_tinygrad.pkl"
dest_metadata = dest_dir / f"{model}_{custom_name}_metadata.pkl"
if tinygrad_pkl.exists():
shutil.move(str(tinygrad_pkl), str(dest_tinygrad))
if metadata_pkl.exists():
shutil.move(str(metadata_pkl), str(dest_metadata))
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: install_models_pc.py <model_dir>")
sys.exit(1)
install_models(sys.argv[1])
+63 -23
View File
@@ -10,11 +10,14 @@ import os
os.environ['GMMU'] = '0'
from openpilot.common.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
USBGPU = "USBGPU" in os.environ
from openpilot.selfdrive.modeld.helpers import usbgpu_present, load_oob
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
USBGPU = usbgpu_present()
if USBGPU:
os.environ['DEV'] = 'AMD'
os.environ['AMD_IFACE'] = 'USB'
import pickle
import time
import numpy as np
import openpilot.cereal.messaging as messaging
@@ -24,6 +27,11 @@ from setproctitle import setproctitle
from openpilot.cereal.messaging import PubMaster, SubMaster
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
from opendbc.car.car_helpers import get_demo_car_params
from tinygrad.tensor import Tensor
from tinygrad.device import Device
from openpilot.common.file_chunker import open_file_chunked
from openpilot.common.swaglog import cloudlog
from openpilot.common.params import Params
from openpilot.common.filter_simple import FirstOrderFilter
@@ -31,6 +39,7 @@ from openpilot.common.realtime import config_realtime_process, DT_MDL
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.common.transformations.model import get_warp_matrix
from openpilot.system import sentry
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value
@@ -38,6 +47,7 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p
from openpilot.sunnypilot.modeld_v2.constants import Plan
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
@@ -100,31 +110,29 @@ class ModelState(ModelStateBase):
self._init_combined(pkl_path, cam_w, cam_h, model_bundle)
def _init_combined(self, pkl_path, cam_w, cam_h, bundle):
from tinygrad.tensor import Tensor
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
from tinygrad.device import Device
from openpilot.common.file_chunker import open_file_chunked
cloudlog.warning(f"loading combined pkl: {pkl_path}")
jits = pickle.load(open_file_chunked(pkl_path))
jits = load_oob(open_file_chunked(pkl_path))
self.DEV = Device.DEFAULT
self.WARP_DEV = 'CPU' if USBGPU else self.DEV
self.WARP_DEV = ('QCOM' if TICI else 'CPU') if USBGPU else self.DEV
self.QUEUE_DEV = self.DEV
metadata = jits['metadata']
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
if 'model' in metadata:
model_metadata = metadata['model']
self.vision_output_slices = model_metadata['output_slices']
self.policy_output_slices = {}
self._policy_slices_list = []
self._combined_model_type = 'supercombo'
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
self._vision_input_names = [key for key in model_metadata['input_shapes'] if 'img' in key]
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.QUEUE_DEV)
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'],
frame_skip, device=self.QUEUE_DEV)
else:
vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k != 'vision']
@@ -142,16 +150,13 @@ class ModelState(ModelStateBase):
policy_input_shapes = first_policy_metadata['input_shapes']
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.QUEUE_DEV)
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes,
frame_skip, device=self.QUEUE_DEV)
self._desire_key = next(key for key in self.numpy_inputs if key.startswith('desire'))
self._road_key = next(key for key in self._vision_input_names if 'big' not in key)
self._wide_key = next(key for key in self._vision_input_names if 'big' in key)
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
self.parser = SplitParser() if self._combined_model_type != 'supercombo' else CombinedParser()
is_20hz = bundle.is20hz if bundle else self._combined_model_type in ('split', 'multi_policy')
if is_20hz:
from openpilot.sunnypilot.models.split_model_constants import SplitModelConstants
@@ -160,20 +165,45 @@ class ModelState(ModelStateBase):
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
self.constants = ModelConstants()
if self._combined_model_type != 'supercombo':
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
self.parser = SplitParser()
else:
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
self.parser = CombinedParser()
self.prev_desire = np.zeros(self.constants.DESIRE_LEN, dtype=np.float32)
self.full_frames: dict = {}
self._blob_cache: dict = {}
nv12_info = get_nv12_info(cam_w, cam_h)
self.frame_buf_params = dict.fromkeys(self._vision_input_names, nv12_info)
self._run_policy = jits[(cam_w, cam_h)]['run_policy']
self._warp_enqueue = jits[(cam_w, cam_h)]['warp_enqueue']
yuv_size = self.frame_buf_params[self._road_key][3]
self._warp_enqueue(
**self.input_queues,
frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize(),
big_frame=Tensor(np.zeros(yuv_size, dtype=np.uint8), device=self.WARP_DEV).contiguous().realize())
if USBGPU:
self.warmup()
def warmup(self) -> None:
dummy_frames = {k: np.zeros(self.frame_buf_params[k][3], dtype=np.uint8) for k in self._vision_input_names}
transforms = {k: np.eye(3, dtype=np.float32) for k in [self._road_key, self._wide_key] if k}
dummy_inputs = {}
for k, v in self.numpy_inputs.items():
if k not in ['tfm', 'big_tfm', 'prev_feat']:
dummy_inputs[k] = np.zeros(v.shape, dtype=v.dtype)
self.run(dummy_frames, transforms, dummy_inputs, prepare_only=False)
for v in self.numpy_inputs.values():
v[:] = 0
self.prev_desire[:] = 0
self.full_frames.clear()
self._blob_cache.clear()
@property
def mlsim(self) -> bool:
@@ -189,8 +219,6 @@ class ModelState(ModelStateBase):
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
from tinygrad.tensor import Tensor
for key in bufs.keys():
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
yuv_size = self.frame_buf_params[key][3]
@@ -222,11 +250,16 @@ class ModelState(ModelStateBase):
model_output = raw_outputs.numpy().flatten()
sliced = {k: model_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_outputs(sliced)
if 'prev_feat' in self.numpy_inputs:
self.numpy_inputs['prev_feat'][:] = model_output[self.vision_output_slices['hidden_state']]
else:
vision_output = raw_outputs[0].numpy().flatten()
vision_sliced = {k: vision_output[np.newaxis, v] for k, v in self.vision_output_slices.items()}
outputs = self.parser.parse_vision_outputs(vision_sliced)
if 'prev_feat' in self.numpy_inputs and 'hidden_state' in self.vision_output_slices:
self.numpy_inputs['prev_feat'][:] = vision_output[self.vision_output_slices['hidden_state']]
for i, policy_slices in enumerate(self._policy_slices_list):
policy_output = raw_outputs[i + 1].numpy().flatten()
policy_sliced = {k: policy_output[np.newaxis, v] for k, v in policy_slices.items()}
@@ -255,7 +288,6 @@ class ModelState(ModelStateBase):
plan = model_output['plan'][0]
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
action_t=long_action_t)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
curvature_plan = (plan + (self.PLANPLUS_CONTROL - 1.0) * model_output['planplus'][0]
if 'planplus' in model_output and self.PLANPLUS_CONTROL != 1.0 else plan)
@@ -265,6 +297,8 @@ class ModelState(ModelStateBase):
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
should_stop = (v_ego < 0.3 and desired_accel < 0.1)
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models
if v_ego > self.MIN_LAT_CONTROL_SPEED:
desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS)
@@ -282,6 +316,9 @@ def main(demo=False):
setproctitle(PROCESS_NAME)
config_realtime_process(7, 54)
if USBGPU:
wait_usbgpu_link()
# visionipc clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
@@ -316,6 +353,9 @@ def main(demo=False):
publish_state = PublishState()
params = Params()
params.put_bool("UsbGpuPresent", USBGPU)
params.put_bool("UsbGpuCompiled", USBGPU)
# setup filter to track dropped frames
frame_dropped_filter = FirstOrderFilter(0., 10., 1. / model.constants.MODEL_FREQ)
frame_id = 0
@@ -1,13 +1,16 @@
import numpy as np
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
def safe_exp(x, out=None):
# -11 is around 10**14, more causes float16 overflow
return np.exp(np.clip(x, -np.inf, 11), out=out)
def sigmoid(x):
return 1. / (1. + safe_exp(-x))
def softmax(x, axis=-1):
x -= np.max(x, axis=axis, keepdims=True)
if x.dtype == np.float32 or x.dtype == np.float64:
@@ -17,6 +20,19 @@ def softmax(x, axis=-1):
x /= np.sum(x, axis=axis, keepdims=True)
return x
def _infer_mhp(slice_size: int, prod_out_shape: int, max_in_n: int = 16, max_out_n: int = 6) -> tuple[int, int]:
for out_n in range(max_out_n + 1):
per = 2 * prod_out_shape + out_n
if per <= 0:
continue
if slice_size % per == 0:
in_n = slice_size // per
if 1 <= in_n <= max_in_n:
return in_n, out_n
return 1, 0 # single hypothesis, no weights — matches a non-MDN output
class Parser:
def __init__(self, ignore_missing=False):
self.ignore_missing = ignore_missing
@@ -40,17 +56,22 @@ class Parser:
raw = outs[name]
outs[name] = sigmoid(raw)
def parse_mdn(self, name, outs, in_N=0, out_N=1, out_shape=None):
def parse_mdn(self, name, outs, out_shape, in_N=0, out_N=0):
if self.check_missing(outs, name):
return
raw = outs[name]
raw = raw.reshape((raw.shape[0], max(in_N, 1), -1))
if in_N == 0 and out_N == 0:
prod = int(np.prod(out_shape))
in_N, out_N = _infer_mhp(raw.shape[1], prod)
raw = raw.reshape((raw.shape[0], in_N, -1))
n_values = (raw.shape[2] - out_N)//2
pred_mu = raw[:,:,:n_values]
pred_std = safe_exp(raw[:,:,n_values: 2*n_values])
if in_N > 1:
if in_N > 1 and out_N > 0:
weights = np.zeros((raw.shape[0], in_N, out_N), dtype=raw.dtype)
for i in range(out_N):
weights[:,:,i - out_N] = softmax(raw[:,:,i - out_N], axis=-1)
@@ -61,7 +82,6 @@ class Parser:
weights[fidx] = weights[fidx][idxs]
pred_mu[fidx] = pred_mu[fidx][idxs]
pred_std[fidx] = pred_std[fidx][idxs]
assert out_shape is not None
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_weights'] = weights
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
@@ -74,37 +94,43 @@ class Parser:
idxs = np.argsort(weights[fidx,:,hidx])[::-1]
pred_mu_final[fidx, hidx] = pred_mu[fidx, idxs[0]]
pred_std_final[fidx, hidx] = pred_std[fidx, idxs[0]]
elif in_N > 1 and out_N == 0:
# MHP without weights: keep every hypothesis intact, surface them as
# ``*_hypotheses`` and propagate the full multi-hypothesis tensor.
full_shape = tuple([raw.shape[0], in_N] + list(out_shape))
outs[name + '_hypotheses'] = pred_mu.reshape(full_shape)
outs[name + '_stds_hypotheses'] = pred_std.reshape(full_shape)
pred_mu_final = pred_mu
pred_std_final = pred_std
else:
pred_mu_final = pred_mu
pred_std_final = pred_std
if out_N > 1:
assert out_shape is not None
final_shape = tuple([raw.shape[0], out_N] + list(out_shape))
if out_N > 1 or (in_N > 1 and out_N == 0):
n_selections = out_N if out_N > 1 else in_N
final_shape = tuple([raw.shape[0], n_selections] + list(out_shape))
else:
assert out_shape is not None
final_shape = tuple([raw.shape[0],] + list(out_shape))
outs[name] = pred_mu_final.reshape(final_shape)
outs[name + '_stds'] = pred_std_final.reshape(final_shape)
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION,
out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
# supercombo (4955 / 102) and newer variants (e.g. 990 / 144).
self.parse_mdn('plan', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
self.parse_mdn('lane_lines', outs, out_shape=(ModelConstants.NUM_LANE_LINES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('road_edges', outs, out_shape=(ModelConstants.NUM_ROAD_EDGES, ModelConstants.IDX_N, ModelConstants.LANE_LINES_WIDTH))
self.parse_mdn('pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('road_transform', outs, out_shape=(ModelConstants.POSE_WIDTH,))
if 'sim_pose' in outs:
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION,
out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH))
self.parse_mdn('sim_pose', outs, out_shape=(ModelConstants.POSE_WIDTH,))
self.parse_mdn('wide_from_device_euler', outs, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
self.parse_mdn('lead', outs, out_shape=(ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH))
if 'lat_planner_solution' in outs:
self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
self.parse_mdn('lat_planner_solution', outs, out_shape=(ModelConstants.IDX_N, ModelConstants.LAT_PLANNER_SOLUTION_WIDTH))
if 'desired_curvature' in outs:
self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
self.parse_mdn('desired_curvature', outs, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,))
for k in ['lead_prob', 'lane_lines_prob', 'meta']:
self.parse_binary_crossentropy(k, outs)
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,))
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN, ModelConstants.DESIRE_PRED_WIDTH))
return outs
@@ -123,7 +123,7 @@ class Parser:
self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,))
if 'lane_lines' in outs:
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'lane_lines_prob' in outs:
self.parse_binary_crossentropy('lane_lines_prob', outs)
if 'lead_prob' in outs:
@@ -134,7 +134,7 @@ class Parser:
self.parse_binary_crossentropy('meta', outs)
if 'road_edges' in outs:
self.parse_mdn('road_edges', outs, in_N=0, out_N=0,
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH))
if 'sim_pose' in outs:
self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,))
if 'action' in outs:
@@ -67,16 +67,12 @@ class TestStockEquivalence:
state = model_state_factory(ARCHETYPES['vision_policy_split'])
frame_skip = derive_frame_skip(SPLIT_VISION_INPUT_SHAPES, SPLIT_POLICY_INPUT_SHAPES)
# action_t is a deep-model prerequisite the SP loader doesn't provide yet; see skip_keys below
stock_shapes = {**SPLIT_VISION_INPUT_SHAPES, **SPLIT_POLICY_INPUT_SHAPES, 'action_t': (1, 2)}
stock_queues, stock_npy = make_input_queues(stock_shapes, frame_skip, device='NPY')
# TODO-SP: remove action_t skip once SP adds prerequisite for deep models (action_t input queue)
skip_keys = {'action_t'}
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
assert set(state.input_queues.keys()) == set(stock_queues.keys())
assert {'desire', 'traffic_convention'} <= set(state.numpy_inputs.keys())
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - {'action_t', 'prev_feat'}
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
+13 -12
View File
@@ -13,6 +13,7 @@ from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.common.hardware.hw import Paths
from openpilot.sunnypilot.models.helpers import is_bundle_version_compatible
from openpilot.selfdrive.modeld.helpers import usbgpu_present
from openpilot.cereal import custom
@@ -64,8 +65,6 @@ class ModelParser:
model.type = model_data.get("type")
model.artifact = ModelParser._parse_artifact(model_data.get("artifact", {}))
if metadata := model_data.get("metadata"):
model.metadata = ModelParser._parse_artifact(metadata)
return model
@staticmethod
@@ -105,11 +104,11 @@ class ModelParser:
class ModelCache:
"""Handles caching of model data to avoid frequent remote fetches"""
def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9)):
def __init__(self, params: Params, cache_timeout: int = int(3600 * 1e9), suffix: str = ""):
self.params = params
self.cache_timeout = cache_timeout
self._LAST_SYNC_KEY = "ModelManager_LastSyncTime"
self._CACHE_KEY = "ModelManager_ModelsCache"
self._LAST_SYNC_KEY = f"ModelManager_LastSyncTime{suffix}"
self._CACHE_KEY = f"ModelManager_ModelsCache{suffix}"
def _is_expired(self) -> bool:
"""Checks if the cache has expired"""
@@ -141,24 +140,28 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
def __init__(self, params: Params):
self.params = params
self.model_cache = ModelCache(params)
self.model_parser = ModelParser()
if usbgpu_present():
self.model_cache = ModelCache(params, suffix="_USBGPU")
self.model_url = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_usbgpu_v18.json"
else:
self.model_cache = ModelCache(params)
self.model_url = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None:
"""Fetches fresh model data from remote and updates cache.
Returns None on transport errors. Raises on 404 and other fatal HTTP errors.
"""
try:
response = requests.get(self.MODEL_URL, timeout=10)
response = requests.get(self.model_url, timeout=10)
# Explicitly handle 404 differently
if response.status_code == 404:
cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}")
raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response)
cloudlog.error(f"Models URL returned 404 Not Found: {self.model_url}")
raise HTTPError(f"404 Not Found: {self.model_url}", response=response)
# Raise for any other 4xx/5xx
response.raise_for_status()
@@ -211,5 +214,3 @@ if __name__ == "__main__":
# Print metadata details
if model.artifact.chunks:
print(f"Contains {len(model.artifact.chunks)} chunks.")
if hasattr(model, 'metadata') and model.metadata and model.metadata.fileName:
print(f"Metadata: {model.metadata.fileName}, Download URI: {model.metadata.downloadUri.uri}")
+15 -8
View File
@@ -18,7 +18,7 @@ from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRai
from openpilot.common.hardware.hw import Paths
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 15
REQUIRED_JSON_VERSION = 16
CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
@@ -56,12 +56,20 @@ def is_bundle_version_compatible(bundle: dict) -> bool:
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
artifacts = []
from openpilot.common.file_chunker import get_chunk_name
for model in getattr(bundle, 'models', []) or []:
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)):
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None):
sha256 = getattr(artifact.downloadUri, 'sha256', None)
if sha256:
artifacts.append((artifact.fileName, sha256))
for artifact in (getattr(model, 'artifact', None),):
if artifact and getattr(artifact, 'fileName', None):
if len(artifact.chunks) > 0:
for i, chunk in enumerate(artifact.chunks):
chunk_name = get_chunk_name(artifact.fileName, i, len(artifact.chunks))
if getattr(chunk, 'sha256', None):
artifacts.append((chunk_name, chunk.sha256))
else:
if getattr(artifact, 'downloadUri', None):
sha256 = getattr(artifact.downloadUri, 'sha256', None)
if sha256:
artifacts.append((artifact.fileName, sha256))
return artifacts
@@ -156,8 +164,7 @@ def _get_model():
def load_metadata():
model = _get_model()
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
metadata_path = METADATA_PATH
with open(metadata_path, 'rb') as f:
return pickle.load(f)
+38 -21
View File
@@ -38,11 +38,11 @@ class ModelManagerSP:
if not self.selected_bundle:
return
for model in self.selected_bundle.models:
for artifact in (model.artifact, model.metadata):
if artifact is not source_artifact and artifact.fileName == source_artifact.fileName:
artifact.downloadProgress.status = source_artifact.downloadProgress.status
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
artifact = model.artifact
if artifact is not source_artifact and artifact.fileName == source_artifact.fileName:
artifact.downloadProgress.status = source_artifact.downloadProgress.status
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
def _calculate_eta(self, filename: str, progress: float) -> int:
"""Calculate ETA based on elapsed time and current progress"""
@@ -136,7 +136,22 @@ class ModelManagerSP:
full_path = os.path.join(destination_path, filename)
try:
if await verify_file(full_path, expected_hash):
is_cached = False
if len(artifact.chunks) > 0:
from openpilot.common.file_chunker import get_chunk_name
chunks_valid = True
for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
if not await verify_file(chunk_path, chunk.sha256):
chunks_valid = False
break
if chunks_valid and len(artifact.chunks) > 0:
is_cached = True
else:
if await verify_file(full_path, expected_hash):
is_cached = True
if is_cached:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
@@ -146,11 +161,15 @@ class ModelManagerSP:
if len(artifact.chunks) > 0:
await self._download_chunked(url, full_path, artifact)
from openpilot.common.file_chunker import get_chunk_name
for i, chunk in enumerate(artifact.chunks):
chunk_path = get_chunk_name(full_path, i, len(artifact.chunks))
if not await verify_file(chunk_path, chunk.sha256):
raise ValueError(f"Hash validation failed for chunk {i+1} of {filename}")
else:
await self._download_file(url, full_path, artifact)
if not await verify_file(full_path, expected_hash):
raise ValueError(f"Hash validation failed for {filename}")
if not await verify_file(full_path, expected_hash):
raise ValueError(f"Hash validation failed for {filename}")
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
artifact.downloadProgress.progress = 100
@@ -198,16 +217,16 @@ class ModelManagerSP:
try:
seen_artifacts: set[str] = set()
for model in self.selected_bundle.models:
for artifact in (model.metadata, model.artifact):
if not artifact.fileName:
continue
if artifact.fileName in seen_artifacts:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
else:
seen_artifacts.add(artifact.fileName)
await self._process_artifact(artifact, destination_path)
artifact = model.artifact
if not artifact.fileName:
continue
if artifact.fileName in seen_artifacts:
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
artifact.downloadProgress.progress = 100
artifact.downloadProgress.eta = 0
else:
seen_artifacts.add(artifact.fileName)
await self._process_artifact(artifact, destination_path)
self.active_bundle = self.selected_bundle
self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded
@@ -268,8 +287,6 @@ class ModelManagerSP:
for model in self.active_bundle.models:
if hasattr(model, 'artifact') and model.artifact.fileName:
active_files.append(model.artifact.fileName)
if hasattr(model, 'metadata') and model.metadata.fileName:
active_files.append(model.metadata.fileName)
# Remove all files except active ones (including their chunk files)
model_dir = Paths.model_root()
@@ -1,11 +1,12 @@
import requests
from openpilot.common.params import Params
from openpilot.sunnypilot.models.tinygrad_ref import get_tinygrad_ref
from openpilot.sunnypilot.models.fetcher import ModelFetcher
def fetch_tinygrad_ref():
response = requests.get(ModelFetcher.MODEL_URL, timeout=10)
fetcher = ModelFetcher(Params())
response = requests.get(fetcher.model_url, timeout=10)
response.raise_for_status()
json_data = response.json()
return json_data.get("tinygrad_ref")
@@ -123,6 +123,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
# tesla
keys.extend([
"TeslaCoopSteering",
"TeslaMadsScreenButton",
])
# toyota
@@ -91,7 +91,7 @@ class SpeedLimitAssist:
self._plus_hold = 0.
self._minus_hold = 0.
self._last_carstate_ts = 0.
self._release_toggle_prev = 0
# TODO-SP: SLA's own output_a_target for planner
# Solution functions mapped to respective states
@@ -146,16 +146,16 @@ class SpeedLimitAssist:
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
def update_car_state(self, CS: car.CarState) -> None:
def update_buttons(self, release_toggle: int) -> None:
released = self._release_toggle_prev ^ release_toggle
self._release_toggle_prev = release_toggle
if not released:
return
now = time.monotonic()
self._last_carstate_ts = now
for b in CS.buttonEvents:
if not b.pressed:
if b.type in CRUISE_BUTTONS_PLUS:
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
elif b.type in CRUISE_BUTTONS_MINUS:
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
if any((released >> b) & 1 for b in CRUISE_BUTTONS_PLUS):
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
if any((released >> b) & 1 for b in CRUISE_BUTTONS_MINUS):
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool:
now = time.monotonic()
@@ -5,11 +5,14 @@ This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import time
import pytest
from openpilot.cereal import custom
from opendbc.car.car_helpers import interfaces
from opendbc.car.rivian.values import CAR as RIVIAN
from opendbc.car.structs import car
from opendbc.car.tesla.values import CAR as TESLA
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.constants import CV
@@ -21,9 +24,13 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
@@ -276,3 +283,86 @@ class TestSpeedLimitAssist:
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
elif initial_state in ACTIVE_STATES:
assert self.sla.state in ACTIVE_STATES
class TestButtonStateTrackerSLAIntegration:
def setup_method(self, method):
self.tracker = ButtonStateTracker()
self.params = Params()
self.params.put("IsReleaseSpBranch", True, block=True)
self.params.put("SpeedLimitMode", int(Mode.assist), block=True)
self.params.put_bool("IsMetric", False, block=True)
self.params.put("SpeedLimitOffsetType", 0, block=True)
self.params.put("SpeedLimitValueOffset", 0, block=True)
CarInterface = interfaces[DEFAULT_CAR]
CP = CarInterface.get_non_essential_params(DEFAULT_CAR)
CP.openpilotLongitudinalControl = True
CP_SP = CarInterface.get_non_essential_params_sp(CP, DEFAULT_CAR)
self.sla = SpeedLimitAssist(CP, CP_SP)
def _make_cs(self, events=None) -> car.CarState:
CS = car.CarState()
CS.buttonEvents = events or []
return CS
def _run_ctrl_frames(self, frames: list[car.CarState]) -> None:
for cs in frames:
self.tracker.update(cs)
def test_button_confirm_via_tracker(self) -> None:
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
self._make_cs(),
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
self._make_cs(),
self._make_cs(),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=True, req_minus=False)
def test_rapid_press_release_between_polls(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]),
self._make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=False)]),
self._make_cs(),
self._make_cs(),
self._make_cs(),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=False, req_minus=True)
def test_multiple_releases_between_polls(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self._run_ctrl_frames([
self._make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
]),
self._make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=False),
ButtonEvent(type=ButtonType.decelCruise, pressed=False),
]),
])
self.sla.update_buttons(self.tracker.release_toggle)
assert self.sla._get_button_release(req_plus=True, req_minus=False)
assert self.sla._get_button_release(req_plus=False, req_minus=True)
def test_no_false_positive_same_toggle(self) -> None:
self.sla.update_buttons(self.tracker.release_toggle)
self.sla.update_buttons(self.tracker.release_toggle)
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
assert not self.sla._get_button_release(req_plus=False, req_minus=True)
def test_button_confirm_expires(self) -> None:
self._run_ctrl_frames([
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]),
self._make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]),
])
self.sla.update_buttons(self.tracker.release_toggle)
time.sleep(CRUISE_BUTTON_CONFIRM_HOLD + 0.1)
assert not self.sla._get_button_release(req_plus=True, req_minus=False)
@@ -0,0 +1,26 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car import structs
class ButtonStateTracker:
def __init__(self) -> None:
self.pressed: int = 0
self.release_toggle: int = 0
def update(self, CS: structs.CarState) -> None:
for b in CS.buttonEvents:
bit = 1 << b.type.raw
if b.pressed:
self.pressed |= bit
else:
self.pressed &= ~bit
self.release_toggle ^= bit
def publish(self, ss_sp) -> None:
ss_sp.buttonsPressed = self.pressed
ss_sp.buttonsReleaseToggle = self.release_toggle
@@ -0,0 +1,67 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
from opendbc.car.structs import car
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
ButtonEvent = car.CarState.ButtonEvent
ButtonType = car.CarState.ButtonEvent.Type
class TestButtonStateTracker:
def setup_method(self) -> None:
self.tracker = ButtonStateTracker()
def make_cs(self, events: list) -> car.CarState:
CS = car.CarState()
CS.buttonEvents = events
return CS
def test_initial_state(self) -> None:
assert self.tracker.pressed == 0
assert self.tracker.release_toggle == 0
def test_press_sets_bit(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
assert self.tracker.pressed == (1 << ButtonType.accelCruise)
assert self.tracker.release_toggle == 0
def test_release_clears_and_toggles(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
assert self.tracker.pressed == 0
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
def test_multiple_buttons(self) -> None:
self.tracker.update(self.make_cs([
ButtonEvent(type=ButtonType.accelCruise, pressed=True),
ButtonEvent(type=ButtonType.decelCruise, pressed=True),
]))
assert self.tracker.pressed == (1 << ButtonType.accelCruise) | (1 << ButtonType.decelCruise)
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
assert self.tracker.pressed == (1 << ButtonType.decelCruise)
assert self.tracker.release_toggle == (1 << ButtonType.accelCruise)
def test_release_toggle_flips(self) -> None:
for _ in range(2):
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.gapAdjustCruise, pressed=False)]))
assert self.tracker.release_toggle == 0
def test_publish(self) -> None:
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.decelCruise, pressed=True)]))
self.tracker.update(self.make_cs([ButtonEvent(type=ButtonType.accelCruise, pressed=False)]))
class MockSP:
buttonsPressed = 0
buttonsReleaseToggle = 0
sp = MockSP()
self.tracker.publish(sp)
assert sp.buttonsPressed == self.tracker.pressed
assert sp.buttonsReleaseToggle == self.tracker.release_toggle
@@ -2161,6 +2161,42 @@
"type": "offroad_only"
}
]
},
{
"key": "TeslaMadsScreenButton",
"widget": "multiple_button",
"title": "MADS Screen Activation",
"description": "Use a multi-finger press on the infotainment screen to toggle MADS. This allows the use of full MADS functionality when enabled. Selecting a higher finger count may reduce accidental activations. Note: Setting this to Off will reset your MADS settings to default.",
"options": [
{
"value": 0,
"label": "Off"
},
{
"value": 1,
"label": "3-Finger"
},
{
"value": 2,
"label": "4-Finger"
},
{
"value": 3,
"label": "5-Finger"
}
],
"visibility": [
{
"type": "capability",
"field": "tesla_has_vehicle_bus",
"equals": true
}
],
"enablement": [
{
"type": "offroad_only"
}
]
}
]
},
@@ -56,6 +56,28 @@ sections:
title: Cooperative Steering (Beta)
enablement:
- $ref: '#/macros/offroad'
- key: TeslaMadsScreenButton
widget: multiple_button
title: MADS Screen Activation
description: 'Use a multi-finger press on the infotainment screen to toggle MADS.
This allows the use of full MADS functionality when enabled. Selecting a higher
finger count may reduce accidental activations. Note: Setting this to Off will
reset your MADS settings to default.'
options:
- value: 0
label: 'Off'
- value: 1
label: 3-Finger
- value: 2
label: 4-Finger
- value: 3
label: 5-Finger
visibility:
- type: capability
field: tesla_has_vehicle_bus
equals: true
enablement:
- $ref: '#/macros/offroad'
- id: toyota
title: Toyota / Lexus Settings
description: ''
@@ -17,6 +17,26 @@ ONROAD_BRIGHTNESS_TIMER_VALUES = {0: 3, 1: 5, 2: 7, 3: 10, 4: 15, 5: 30, **{i: (
VALID_TIMER_VALUES = set(ONROAD_BRIGHTNESS_TIMER_VALUES.values())
def _resolve_brand(_params) -> str:
bundle = _params.get("CarPlatformBundle")
if isinstance(bundle, dict) and bundle.get("brand"):
return str(bundle["brand"])
# Auto-fingerprinted cars have no bundle, fall back to the last known CarParams.
CP_bytes = _params.get("CarParamsPersistent")
if CP_bytes is None:
return ""
# Never raises: callers rely on "" to mean "brand unknown, skip the migration".
try:
from openpilot.cereal import messaging # lazy: avoids heavy import at module level
from opendbc.car.structs import car
return str(messaging.log_from_bytes(CP_bytes, car.CarParams).brand)
except Exception as e:
cloudlog.exception(f"params_migration: failed to resolve brand from CarParamsPersistent: {e}")
return ""
def _migrate_car_platform_bundle(_params):
bundle = _params.get("CarPlatformBundle")
if bundle is None:
@@ -47,6 +67,23 @@ def _migrate_car_platform_bundle(_params):
cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}")
def _migrate_tesla_mads_screen_button(_params):
# TeslaMadsScreenButton defaults to Off for fresh installs, but the screen button was previously always
# active on Teslas with a vehicle bus. Seed existing Tesla installs with 3-finger to preserve that.
try:
if _params.get("TeslaMadsScreenButton") is not None:
return
if _resolve_brand(_params) != "tesla":
return
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType # lazy: avoids heavy import at module level
_params.put("TeslaMadsScreenButton", MadsScreenButtonType.THREE_FINGER, block=True)
cloudlog.info("params_migration: seeded TeslaMadsScreenButton with 3-finger to preserve existing behavior")
except Exception as e:
cloudlog.exception(f"Error migrating TeslaMadsScreenButton: {e}")
def run_migration(_params):
# migrate OnroadScreenOffBrightness
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
@@ -80,3 +117,6 @@ def run_migration(_params):
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")
_migrate_car_platform_bundle(_params)
# seed TeslaMadsScreenButton for existing Tesla installs
_migrate_tesla_mads_screen_button(_params)
-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