mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-03 10:49:25 +08:00
Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 735da70382 | |||
| 843f16da23 | |||
| d63c7915e1 | |||
| ba1dbade5a | |||
| c977dc3f76 | |||
| 6cb803060f | |||
| 8bb615867f | |||
| 0c25503036 | |||
| eef84e2b30 | |||
| 212392696d | |||
| 3263e00c33 | |||
| 872d25d7cb | |||
| 372c3ff32c | |||
| 3e30962bd3 | |||
| 3274043063 | |||
| c624550d2e | |||
| 6759038671 | |||
| aa636e75c8 | |||
| 5ff3c3bd8d | |||
| 1e11b52290 | |||
| 76e0204025 | |||
| cc88f2cbd6 | |||
| ee0ac199a4 | |||
| d5ed828eaa | |||
| 9fca585f2a | |||
| bb1259303e | |||
| e135051ca8 | |||
| 12bef55d8a | |||
| 2f7a45e6c8 | |||
| 936ebfc12b | |||
| aa0c9dc0eb | |||
| 7476a866e7 | |||
| 610d857e33 | |||
| d2f47407d0 | |||
| db75ec76ea | |||
| 24066465d7 | |||
| a7abbd6e25 | |||
| 878982447c | |||
| 576527a36b | |||
| ef8c35da24 | |||
| 85688b1040 | |||
| 0fb2199130 | |||
| d48d756c1d | |||
| 2ed298a0c9 | |||
| d68f038949 | |||
| 7231571e57 | |||
| b37f1419d3 | |||
| cd85a66790 | |||
| 305ea87daf | |||
| 4bbfc793e0 | |||
| d5d983676e | |||
| de8a96a398 | |||
| 0cbf45f699 | |||
| 0d68a3a2ab | |||
| 9e85a85059 | |||
| 0373c327c0 | |||
| efe9e5c200 | |||
| 8a249a45dc | |||
| bdbefe67f6 | |||
| 675bb166ad | |||
| 1b717a7e88 | |||
| 86f55a8ba9 | |||
| 629392d2f7 | |||
| bc414bdc8b | |||
| 7ca5649f2c | |||
| 641ee8fa87 | |||
| 56c276158c | |||
| c65308a8bd | |||
| 994e526460 | |||
| 1defae36b7 | |||
| 8f029fd0ef | |||
| ddb46284dc | |||
| 9effc754d9 | |||
| e49ffc2a2d | |||
| 2cacd0b3e5 | |||
| c4b8859dff | |||
| 8fb0953205 | |||
| 63d1c8835f | |||
| 17a185606d | |||
| da10131392 | |||
| 7107c2ba14 | |||
| 95b6e877ac | |||
| eb02c6570e | |||
| 1be8ae31c4 | |||
| 04dcd38856 | |||
| 22ccf0d72f | |||
| 3c969bb627 | |||
| 20f8011feb | |||
| 9cf17e74a1 | |||
| 2c4efdf557 | |||
| 4cd3d3c16c | |||
| 637f3ae9c8 | |||
| 464ee80f71 | |||
| 2743a04613 | |||
| 7f9978d001 | |||
| 4b83961c67 | |||
| c00eaf428a | |||
| 0a9993e8d4 | |||
| 0af214a985 | |||
| af43385e3a | |||
| 0ab2b8c590 | |||
| 67ab18a0de | |||
| e87dc15b30 | |||
| 192d08516c | |||
| 3cf001c59c | |||
| f2ccd021da | |||
| c9fc900f64 | |||
| 3c37c5ce5d | |||
| 7c45889e4e | |||
| 2aabb7aee8 | |||
| 3859e9962f | |||
| 810efbab72 | |||
| ec27bec326 | |||
| 250d553157 | |||
| cea54a0ca8 | |||
| 8e72d783bd | |||
| 1b0dc103dc | |||
| 6c364d292b | |||
| bcdec2ce84 | |||
| 3deaeb3759 | |||
| c669f0984a | |||
| 46dd946740 | |||
| 9da4b3653e | |||
| 4e21ae7c50 | |||
| bb91e92237 | |||
| 14b4c4f85b | |||
| 0660b542c3 | |||
| 2b893b90c9 | |||
| f5139178ed | |||
| fb43b755f2 | |||
| 07f5b967d8 | |||
| ea19c7d3bb | |||
| e461842cbb | |||
| a73c9659d5 | |||
| cb796fbc76 | |||
| 6bf75fc557 | |||
| 9a1fc28819 | |||
| 0741d05e92 | |||
| 1ad008107d | |||
| feebd9df93 | |||
| c2e5ced3e5 | |||
| 15e5d2efb9 | |||
| a3929d0b54 | |||
| 794f8f9991 | |||
| 68fa5e3f21 | |||
| 86c6cc1f48 | |||
| eb7ffbf093 | |||
| 3919095752 | |||
| 74d63be1c3 | |||
| 8894486a1a | |||
| 810599315d | |||
| 6f3ab810c8 | |||
| 230f78b8d3 | |||
| f1affec088 | |||
| 97d8ef242c | |||
| a63fff9b45 | |||
| cb3893daaa | |||
| 29f60df74b | |||
| c6c072e1f4 | |||
| d101cbb83e | |||
| 1536d59633 | |||
| dc99b865ae | |||
| e59bc027ff | |||
| cf7e5efaca | |||
| 4b44f2eb31 | |||
| 107d2ab400 | |||
| 5432d9062c | |||
| f533f6c843 | |||
| 58e9ac763c | |||
| cb50d54169 | |||
| bd5de4ed0a | |||
| 0d4073fadb | |||
| ebc70dcb52 | |||
| 4d0426999e | |||
| 286da42573 | |||
| 8a836710a9 | |||
| 5d515bcf33 | |||
| 1c7f6d5133 | |||
| 05d57c7aeb | |||
| e4b0eaf352 | |||
| a710276472 | |||
| af086db671 | |||
| 0d9eb0e25e | |||
| 0616caed6d | |||
| 095337b3c1 | |||
| 1edec2d22c | |||
| affabb9ee0 | |||
| dc27e8711c | |||
| cf7329a264 | |||
| 5ee5ecd820 | |||
| b064f730dd |
@@ -0,0 +1,39 @@
|
|||||||
|
name: prebuilt
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
BUILD: release/ci/docker_build_sp.sh
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_prebuilt:
|
||||||
|
name: build prebuilt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: github.repository == 'sunnypilot/sunnypilot'
|
||||||
|
env:
|
||||||
|
PUSH_IMAGE: true
|
||||||
|
permissions:
|
||||||
|
checks: read
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
steps:
|
||||||
|
- name: Wait for green check mark
|
||||||
|
if: ${{ github.event_name != 'workflow_dispatch' }}
|
||||||
|
uses: lewagon/wait-on-check-action@ccfb013c15c8afb7bf2b7c028fb74dc5a068cccc
|
||||||
|
with:
|
||||||
|
ref: master
|
||||||
|
wait-interval: 30
|
||||||
|
running-workflow-name: 'build prebuilt'
|
||||||
|
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
check-regexp: ^((?!.*(build master-ci|create badges).*).)*$
|
||||||
|
- uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
- run: git lfs pull
|
||||||
|
- name: Build and Push docker image
|
||||||
|
run: |
|
||||||
|
$DOCKER_LOGIN
|
||||||
|
eval "$BUILD"
|
||||||
+1
-1
Submodule opendbc_repo updated: 4c64e8a95b...b129bf1d71
@@ -69,8 +69,6 @@ struct LeadData {
|
|||||||
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
struct SelfdriveStateSP @0x81c2f05a394cf4af {
|
||||||
mads @0 :ModularAssistiveDrivingSystem;
|
mads @0 :ModularAssistiveDrivingSystem;
|
||||||
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
|
intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement;
|
||||||
buttonsPressed @2 :UInt16;
|
|
||||||
buttonsReleaseToggle @3 :UInt16;
|
|
||||||
|
|
||||||
enum AudibleAlert {
|
enum AudibleAlert {
|
||||||
none @0;
|
none @0;
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ struct OnroadEvent @0xc4fa6047f024e718 {
|
|||||||
userBookmark @95;
|
userBookmark @95;
|
||||||
excessiveActuation @96;
|
excessiveActuation @96;
|
||||||
audioFeedback @97;
|
audioFeedback @97;
|
||||||
|
|
||||||
soundsUnavailableDEPRECATED @47;
|
soundsUnavailableDEPRECATED @47;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,6 +134,9 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"UsbGpuPresent", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
{"UsbGpuCompiled", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}},
|
||||||
{"Version", {PERSISTENT, STRING}},
|
{"Version", {PERSISTENT, STRING}},
|
||||||
|
{"WgpuEnabled", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||||
|
{"WgpuModelName", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION | DEVELOPMENT_ONLY, STRING}},
|
||||||
|
{"WgpuReady", {CLEAR_ON_MANAGER_START | DEVELOPMENT_ONLY, BOOL}},
|
||||||
|
|
||||||
// --- sunnypilot params --- //
|
// --- sunnypilot params --- //
|
||||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||||
@@ -222,7 +225,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
|
||||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
|||||||
def publish(self, sm, pm):
|
def publish(self, sm, pm):
|
||||||
plan_send = messaging.new_message('longitudinalPlan')
|
plan_send = messaging.new_message('longitudinalPlan')
|
||||||
|
|
||||||
plan_send.valid = sm.all_checks()
|
plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState'])
|
||||||
|
|
||||||
longitudinalPlan = plan_send.longitudinalPlan
|
longitudinalPlan = plan_send.longitudinalPlan
|
||||||
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2']
|
||||||
|
|||||||
@@ -29,19 +29,19 @@ def main():
|
|||||||
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
longitudinal_planner = LongitudinalPlanner(CP, CP_SP)
|
||||||
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
pm = messaging.PubMaster(['longitudinalPlan', 'driverAssistance', 'longitudinalPlanSP'])
|
||||||
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
sm = messaging.SubMaster(['carControl', 'carState', 'controlsState', 'liveParameters', 'radarState', 'modelV2', 'selfdriveState',
|
||||||
'liveMapDataSP', 'carStateSP', 'selfdriveStateSP', gps_location_service],
|
'liveMapDataSP', 'carStateSP', gps_location_service],
|
||||||
poll='modelV2', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
poll='carState', ignore_alive=ignore_services, ignore_avg_freq=ignore_services, ignore_valid=ignore_services)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
sm.update()
|
sm.update()
|
||||||
longitudinal_planner.sla.update_buttons(sm['selfdriveStateSP'].buttonsReleaseToggle)
|
longitudinal_planner.sla.update_car_state(sm['carState'])
|
||||||
if sm.updated['modelV2']:
|
if sm.updated['modelV2']:
|
||||||
longitudinal_planner.update(sm)
|
longitudinal_planner.update(sm)
|
||||||
longitudinal_planner.publish(sm, pm)
|
longitudinal_planner.publish(sm, pm)
|
||||||
|
|
||||||
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
|
ldw.update(sm.frame, sm['modelV2'], sm['carState'], sm['carControl'])
|
||||||
msg = messaging.new_message('driverAssistance')
|
msg = messaging.new_message('driverAssistance')
|
||||||
msg.valid = sm.all_checks()
|
msg.valid = sm.all_checks(['carState', 'carControl', 'modelV2', 'liveParameters'])
|
||||||
msg.driverAssistance.leftLaneDeparture = ldw.left
|
msg.driverAssistance.leftLaneDeparture = ldw.left
|
||||||
msg.driverAssistance.rightLaneDeparture = ldw.right
|
msg.driverAssistance.rightLaneDeparture = ldw.right
|
||||||
pm.send('driverAssistance', msg)
|
pm.send('driverAssistance', msg)
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ available = probe_devices()
|
|||||||
if 'CUDA' in available:
|
if 'CUDA' in available:
|
||||||
tg_backend = 'CUDA'
|
tg_backend = 'CUDA'
|
||||||
tg_flags = f'DEV={tg_backend}'
|
tg_flags = f'DEV={tg_backend}'
|
||||||
|
elif 'METAL' in available:
|
||||||
|
tg_backend = 'METAL'
|
||||||
|
tg_flags = f'DEV={tg_backend} FLOAT16=1'
|
||||||
elif 'QCOM' in available:
|
elif 'QCOM' in available:
|
||||||
tg_backend = 'QCOM'
|
tg_backend = 'QCOM'
|
||||||
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
tg_flags = f'DEV={tg_backend} IMAGE=1 FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 OPENPILOT_HACKS=1'
|
||||||
@@ -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
|
USBGPU = usbgpu_present() # or release # TODO always build big model on release
|
||||||
|
WGPU = os.getenv('WGPU') == '1'
|
||||||
if USBGPU:
|
if USBGPU:
|
||||||
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
|
usbgpu_tg_flags = f'DEBUG=2 DEV=USB+AMD:LLVM WARP_DEV={tg_backend} FLOAT16=1 JIT_BATCH_SIZE=0 GMMU=0'
|
||||||
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
# the USB+AMD GPU takes an exclusive flock; serialize all targets that touch it
|
||||||
@@ -84,13 +88,13 @@ compile_modeld_script = [
|
|||||||
model_w, model_h = MEDMODEL_INPUT_SIZE
|
model_w, model_h = MEDMODEL_INPUT_SIZE
|
||||||
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
|
|
||||||
for usbgpu in [False, True] if USBGPU else [False]:
|
for usbgpu in [False, True] if USBGPU or WGPU else [False]:
|
||||||
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
target_pkl_path = File(modeld_pkl_path(usbgpu)).abspath
|
||||||
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
# BIG_INTO_SMALL=1 builds the default target from the big model, e.g. to test it without a USB GPU
|
||||||
file_prefix, cmd_flags = ('big_', usbgpu_tg_flags) if usbgpu else ('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)
|
driving_onnx_deps = get_existing_chunks(File(f"models/{file_prefix}driving_supercombo.onnx").abspath)
|
||||||
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
camera_res_args = ' '.join(f'{cw}x{ch}' for cw, ch in CAMERA_CONFIGS)
|
||||||
cmd = (f'{cmd_flags} {mac_brew_string} 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'--model-size {model_w}x{model_h} '
|
||||||
f'--camera-resolutions {camera_res_args} '
|
f'--camera-resolutions {camera_res_args} '
|
||||||
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
f'--onnx {File(f"models/{file_prefix}driving_supercombo.onnx").abspath} '
|
||||||
@@ -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],
|
tinygrad_files + compile_modeld_script + driving_onnx_deps + [Value(chunk_targets), chunker_file],
|
||||||
[cmd, Action(do_chunk, " [CHUNK] $TARGET")],
|
[cmd, Action(do_chunk, " [CHUNK] $TARGET")],
|
||||||
)
|
)
|
||||||
if usbgpu:
|
if usbgpu and USBGPU:
|
||||||
lenv.SideEffect(usbgpu_lock, node)
|
lenv.SideEffect(usbgpu_lock, node)
|
||||||
|
|
||||||
# get model metadata
|
# get model metadata
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ from openpilot.common.file_chunker import open_file_chunked, get_manifest_path
|
|||||||
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
|
||||||
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
from openpilot.selfdrive.modeld.helpers import usbgpu_present, modeld_pkl_path, get_tg_input_devices, load_oob
|
||||||
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
from openpilot.selfdrive.modeld.usbgpu_link import wait_usbgpu_link
|
||||||
|
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubMaster, ZmqSubSocket
|
||||||
|
|
||||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||||
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||||
@@ -76,26 +77,44 @@ class FrameMeta:
|
|||||||
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
|
||||||
|
|
||||||
|
|
||||||
|
def copy_nv12_to_venus(buf: VisionBuf, dst: np.ndarray, nv12: tuple[int, int, int, int]) -> None:
|
||||||
|
stride, y_height, uv_height, _ = nv12
|
||||||
|
if buf.stride < buf.width:
|
||||||
|
raise ValueError(f"invalid VisionIPC stride {buf.stride} for width {buf.width}")
|
||||||
|
|
||||||
|
src = np.frombuffer(buf.data, dtype=np.uint8)
|
||||||
|
src_size = buf.uv_offset + buf.stride * (buf.height // 2)
|
||||||
|
if src.size < src_size:
|
||||||
|
raise ValueError(f"VisionIPC buffer has {src.size} bytes, expected at least {src_size}")
|
||||||
|
|
||||||
|
dst[:stride * y_height].reshape(y_height, stride)[:buf.height, :buf.width] = \
|
||||||
|
src[:buf.stride * buf.height].reshape(buf.height, buf.stride)[:, :buf.width]
|
||||||
|
dst[stride * y_height:stride * (y_height + uv_height)].reshape(uv_height, stride)[:buf.height // 2, :buf.width] = \
|
||||||
|
src[buf.uv_offset:src_size].reshape(buf.height // 2, buf.stride)[:, :buf.width]
|
||||||
|
|
||||||
|
|
||||||
class ModelState(ModelStateBase):
|
class ModelState(ModelStateBase):
|
||||||
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
prev_desire: np.ndarray # for tracking the rising edge of the pulse
|
||||||
|
|
||||||
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool):
|
def __init__(self, cam_w: int, cam_h: int, usbgpu: bool, big_model: bool = False):
|
||||||
ModelStateBase.__init__(self)
|
ModelStateBase.__init__(self)
|
||||||
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
|
||||||
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
input_devices = get_tg_input_devices(PROCESS_NAME, usbgpu)
|
||||||
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
self.WARP_DEV, self.QUEUE_DEV = input_devices['WARP_DEV'], input_devices['QUEUE_DEV']
|
||||||
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu)))
|
jits = load_oob(open_file_chunked(modeld_pkl_path(usbgpu or big_model)))
|
||||||
metadata = jits['metadata']
|
metadata = jits['metadata']
|
||||||
self.input_shapes = metadata['input_shapes']
|
self.input_shapes = metadata['input_shapes']
|
||||||
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
self.vision_input_names = [k for k in self.input_shapes if 'img' in k]
|
||||||
self.output_slices = metadata['output_slices']
|
self.output_slices = metadata['output_slices']
|
||||||
|
|
||||||
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
|
||||||
|
self.copy_vision_buffers = self.WARP_DEV.split(":")[0] == "METAL"
|
||||||
|
|
||||||
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
|
||||||
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
self.input_queues, self.npy = make_input_queues(self.input_shapes, self.frame_skip, device=self.QUEUE_DEV)
|
||||||
self.full_frames: dict[str, Tensor] = {}
|
self.full_frames: dict[str, Tensor] = {}
|
||||||
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
self._blob_cache: dict[tuple[str, int], Tensor] = {}
|
||||||
|
self._vision_staging: dict[str, np.ndarray] = {}
|
||||||
self.parser = Parser()
|
self.parser = Parser()
|
||||||
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
self.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
|
||||||
self.run_policy = jits['run_policy']
|
self.run_policy = jits['run_policy']
|
||||||
@@ -108,13 +127,22 @@ class ModelState(ModelStateBase):
|
|||||||
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
|
||||||
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
inputs: dict[str, np.ndarray]) -> dict[str, np.ndarray] | None:
|
||||||
for key in bufs.keys():
|
for key in bufs.keys():
|
||||||
ptr = np.frombuffer(bufs[key].data, dtype=np.uint8).ctypes.data
|
nv12 = self.frame_buf_params[key]
|
||||||
yuv_size = self.frame_buf_params[key][3]
|
yuv_size = nv12[3]
|
||||||
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
|
if self.copy_vision_buffers:
|
||||||
cache_key = (key, ptr)
|
# VisionIPC supplies a CPU pointer. Metal's external_ptr expects an MTLBuffer object,
|
||||||
if cache_key not in self._blob_cache:
|
# so wrap its NV12 pixels in the Venus layout expected by the compiled warp, then copy.
|
||||||
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8', device=self.WARP_DEV)
|
if key not in self._vision_staging:
|
||||||
self.full_frames[key] = self._blob_cache[cache_key]
|
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
|
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
|
||||||
inputs['desire_pulse'][0] = 0
|
inputs['desire_pulse'][0] = 0
|
||||||
@@ -139,18 +167,32 @@ class ModelState(ModelStateBase):
|
|||||||
return outputs_dict
|
return outputs_dict
|
||||||
|
|
||||||
|
|
||||||
def main(demo=False):
|
def main(demo=False, remote_addr: str | None = None, big_model: bool = False):
|
||||||
cloudlog.warning("modeld init")
|
cloudlog.warning("modeld init")
|
||||||
|
|
||||||
_present = usbgpu_present()
|
_present = usbgpu_present()
|
||||||
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
|
_compiled = os.path.isfile(get_manifest_path(modeld_pkl_path(usbgpu=True)))
|
||||||
USBGPU = _present and _compiled
|
USBGPU = _present and _compiled
|
||||||
|
if big_model and not _compiled:
|
||||||
|
raise FileNotFoundError(f"big model is not compiled: {modeld_pkl_path(usbgpu=True)}")
|
||||||
params = Params()
|
params = Params()
|
||||||
params.put_bool("UsbGpuPresent", _present)
|
params.put_bool("UsbGpuPresent", _present)
|
||||||
params.put_bool("UsbGpuCompiled", _compiled)
|
params.put_bool("UsbGpuCompiled", _compiled)
|
||||||
|
|
||||||
config_realtime_process(7, 54)
|
config_realtime_process(7, 54)
|
||||||
|
|
||||||
|
remote_CP = None
|
||||||
|
if remote_addr is not None:
|
||||||
|
# Do not attach to VisionIPC until all startup prerequisites are available.
|
||||||
|
# Otherwise its notification queue grows while waiting for the infrequent
|
||||||
|
# bridged carParams message and reconnect starts tens of seconds behind.
|
||||||
|
cloudlog.warning("waiting for remote carParams")
|
||||||
|
car_params_socket = ZmqSubSocket(WGPU_CAR_PARAMS, remote_addr, conflate=True)
|
||||||
|
raw_car_params = car_params_socket.receive()
|
||||||
|
assert raw_car_params is not None
|
||||||
|
remote_CP = messaging.log_from_bytes(raw_car_params, car.CarParams)
|
||||||
|
cloudlog.info("modeld got remote CarParams: %s", remote_CP.brand)
|
||||||
|
|
||||||
# visionipc clients
|
# visionipc clients
|
||||||
while True:
|
while True:
|
||||||
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
available_streams = VisionIpcClient.available_streams("camerad", block=False)
|
||||||
@@ -178,12 +220,14 @@ def main(demo=False):
|
|||||||
wait_usbgpu_link()
|
wait_usbgpu_link()
|
||||||
st = time.monotonic()
|
st = time.monotonic()
|
||||||
cloudlog.warning("loading model")
|
cloudlog.warning("loading model")
|
||||||
model = ModelState(vipc_client_main.width, vipc_client_main.height, USBGPU)
|
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")
|
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
|
||||||
|
|
||||||
# messaging
|
# messaging
|
||||||
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
|
output_services = ["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]
|
||||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])
|
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()
|
publish_state = PublishState()
|
||||||
params = Params()
|
params = Params()
|
||||||
@@ -203,6 +247,9 @@ def main(demo=False):
|
|||||||
|
|
||||||
if demo:
|
if demo:
|
||||||
CP = get_demo_car_params()
|
CP = get_demo_car_params()
|
||||||
|
elif remote_addr is not None:
|
||||||
|
assert remote_CP is not None
|
||||||
|
CP = remote_CP
|
||||||
else:
|
else:
|
||||||
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams)
|
||||||
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
cloudlog.info("modeld got CarParams: %s", CP.brand)
|
||||||
@@ -332,7 +379,9 @@ if __name__ == "__main__":
|
|||||||
import argparse
|
import argparse
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
|
parser.add_argument('--demo', action='store_true', help='A boolean for demo mode.')
|
||||||
|
parser.add_argument('--remote', metavar='ADDRESS', help='Run against a remote device over the cereal ZMQ bridge.')
|
||||||
|
parser.add_argument('--big-model', action='store_true', help='Use the locally compiled big driving model.')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
main(demo=args.demo)
|
main(demo=args.demo, remote_addr=args.remote, big_model=args.big_model)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
cloudlog.warning("got SIGINT")
|
cloudlog.warning("got SIGINT")
|
||||||
|
|||||||
@@ -722,7 +722,6 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = {
|
|||||||
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
ET.NO_ENTRY: NoEntryAlert("Driving Model Lagging"),
|
||||||
ET.PERMANENT: modeld_lagging_alert,
|
ET.PERMANENT: modeld_lagging_alert,
|
||||||
},
|
},
|
||||||
|
|
||||||
# Besides predicting the path, lane lines and lead car data the model also
|
# Besides predicting the path, lane lines and lead car data the model also
|
||||||
# predicts the current velocity and rotation speed of the car. If the model is
|
# predicts the current velocity and rotation speed of the car. If the model is
|
||||||
# very uncertain about the current velocity while the car is moving, this
|
# very uncertain about the current velocity while the car is moving, this
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ from openpilot.sunnypilot import get_sanitize_int_param
|
|||||||
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP
|
||||||
from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper
|
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.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
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
|
|
||||||
REPLAY = "REPLAY" in os.environ
|
REPLAY = "REPLAY" in os.environ
|
||||||
@@ -112,6 +111,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.is_metric = self.params.get_bool("IsMetric")
|
self.is_metric = self.params.get_bool("IsMetric")
|
||||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
|
|
||||||
car_recognized = self.CP.brand != 'mock'
|
car_recognized = self.CP.brand != 'mock'
|
||||||
|
|
||||||
@@ -178,7 +178,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
|
self.car_events_sp = CarSpecificEventsSP(self.CP, self.CP_SP)
|
||||||
|
|
||||||
CruiseHelper.__init__(self, self.CP)
|
CruiseHelper.__init__(self, self.CP)
|
||||||
self.button_state_tracker = ButtonStateTracker()
|
|
||||||
|
|
||||||
def update_events(self, CS):
|
def update_events(self, CS):
|
||||||
"""Compute onroadEvents from carState"""
|
"""Compute onroadEvents from carState"""
|
||||||
@@ -400,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))
|
has_disable_events = self.events.contains(ET.NO_ENTRY) and (self.events.contains(ET.SOFT_DISABLE) or self.events.contains(ET.IMMEDIATE_DISABLE))
|
||||||
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
|
no_system_errors = (not has_disable_events) or (len(self.events) == num_events)
|
||||||
if not self.sm.all_checks() and no_system_errors:
|
if not self.sm.all_checks() and no_system_errors:
|
||||||
if not self.sm.all_alive():
|
# if not self.sm.all_alive():
|
||||||
self.events.add(EventName.commIssue)
|
# self.events.add(EventName.commIssue)
|
||||||
elif not self.sm.all_freq_ok():
|
# elif not self.sm.all_freq_ok():
|
||||||
self.events.add(EventName.commIssueAvgFreq)
|
# self.events.add(EventName.commIssueAvgFreq)
|
||||||
else:
|
# else:
|
||||||
self.events.add(EventName.commIssue)
|
# self.events.add(EventName.commIssue)
|
||||||
|
|
||||||
logs = {
|
logs = {
|
||||||
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
'invalid': [s for s, valid in self.sm.valid.items() if not valid],
|
||||||
@@ -418,13 +417,13 @@ class SelfdriveD(CruiseHelper):
|
|||||||
else:
|
else:
|
||||||
self.logged_comm_issue = None
|
self.logged_comm_issue = None
|
||||||
|
|
||||||
if not self.CP.notCar:
|
# if not self.CP.notCar:
|
||||||
if not self.sm['livePose'].posenetOK:
|
# if not self.sm['livePose'].posenetOK:
|
||||||
self.events.add(EventName.posenetInvalid)
|
# self.events.add(EventName.posenetInvalid)
|
||||||
if not self.sm['livePose'].inputsOK:
|
# if not self.sm['livePose'].inputsOK:
|
||||||
self.events.add(EventName.locationdTemporaryError)
|
# self.events.add(EventName.locationdTemporaryError)
|
||||||
if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
# if not self.sm['liveParameters'].valid and cal_status == log.LiveCalibrationData.Status.calibrated and not TESTING_CLOSET and (not SIMULATION or REPLAY):
|
||||||
self.events.add(EventName.paramsdTemporaryError)
|
# self.events.add(EventName.paramsdTemporaryError)
|
||||||
|
|
||||||
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
# conservative HW alert. if the data or frequency are off, locationd will throw an error
|
||||||
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
if any((self.sm.frame - self.sm.recv_frame[s])*DT_CTRL > 10. for s in self.sensor_packets):
|
||||||
@@ -469,9 +468,9 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
self.distance_traveled += abs(CS.vEgo) * DT_CTRL
|
||||||
|
|
||||||
# TODO: fix simulator
|
# TODO: fix simulator
|
||||||
if not SIMULATION or REPLAY:
|
# if not SIMULATION or REPLAY:
|
||||||
if self.sm['modelV2'].frameDropPerc > 1:
|
# if self.sm['modelV2'].frameDropPerc > 1 and not self.wgpu_enabled:
|
||||||
self.events.add(EventName.modeldLagging)
|
# self.events.add(EventName.modeldLagging)
|
||||||
|
|
||||||
# mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state
|
# mute canBusMissing event if in Park, as it sometimes may trigger a false alarm with MADS in Paused state
|
||||||
if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled:
|
if CS.gearShifter == car.CarState.GearShifter.park and self.mads.enabled:
|
||||||
@@ -599,8 +598,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
icbm.sendButton = self.icbm.cruise_button
|
icbm.sendButton = self.icbm.cruise_button
|
||||||
icbm.vTarget = self.icbm.v_target
|
icbm.vTarget = self.icbm.v_target
|
||||||
|
|
||||||
self.button_state_tracker.publish(ss_sp)
|
|
||||||
|
|
||||||
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
self.pm.send('selfdriveStateSP', ss_sp_msg)
|
||||||
|
|
||||||
# onroadEventsSP - logged every second or on change
|
# onroadEventsSP - logged every second or on change
|
||||||
@@ -620,7 +617,6 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.mads.update(CS)
|
self.mads.update(CS)
|
||||||
self.update_alerts(CS)
|
self.update_alerts(CS)
|
||||||
|
|
||||||
self.button_state_tracker.update(CS)
|
|
||||||
self.publish_selfdriveState(CS)
|
self.publish_selfdriveState(CS)
|
||||||
|
|
||||||
self.CS_prev = CS
|
self.CS_prev = CS
|
||||||
@@ -630,6 +626,7 @@ class SelfdriveD(CruiseHelper):
|
|||||||
self.is_metric = self.params.get_bool("IsMetric")
|
self.is_metric = self.params.get_bool("IsMetric")
|
||||||
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled")
|
||||||
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl
|
||||||
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
self.personality = self.params.get("LongitudinalPersonality", return_default=True)
|
||||||
|
|
||||||
|
|||||||
@@ -248,8 +248,10 @@ class MiciHomeLayout(Widget):
|
|||||||
|
|
||||||
# ***** Center-aligned bottom section icons *****
|
# ***** Center-aligned bottom section icons *****
|
||||||
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
self._experimental_icon.set_visible(ui_state.experimental_mode)
|
||||||
self._egpu_icon.set_visible(ui_state.usbgpu and 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_gray.set_visible(ui_state.usbgpu and not ui_state.usbgpu_compiled)
|
self._egpu_icon.set_visible((ui_state.usbgpu and ui_state.usbgpu_compiled) or wgpu_running)
|
||||||
|
self._egpu_icon_gray.set_visible((ui_state.usbgpu and not ui_state.usbgpu_compiled) or
|
||||||
|
(ui_state.wgpu_ready and not wgpu_running))
|
||||||
self._mic_icon.set_visible(ui_state.recording_audio)
|
self._mic_icon.set_visible(ui_state.recording_audio)
|
||||||
self._body_icon.set_visible(bool(ui_state.is_body))
|
self._body_icon.set_visible(bool(ui_state.is_body))
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.cereal import log
|
from openpilot.cereal import log
|
||||||
@@ -160,6 +162,20 @@ class AugmentedRoadView(CameraView):
|
|||||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||||
|
self._model_status_key_labels = [
|
||||||
|
UnifiedLabel("", 21, FontWeight.ROMAN, text_color=rl.Color(210, 210, 210, 220),
|
||||||
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT,
|
||||||
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
|
wrap_text=False)
|
||||||
|
for _ in range(6)
|
||||||
|
]
|
||||||
|
self._model_status_value_labels = [
|
||||||
|
UnifiedLabel("", 22, FontWeight.SEMI_BOLD,
|
||||||
|
alignment=rl.GuiTextAlignment.TEXT_ALIGN_RIGHT,
|
||||||
|
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE,
|
||||||
|
wrap_text=False)
|
||||||
|
for _ in range(6)
|
||||||
|
]
|
||||||
|
|
||||||
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
self._fade_texture = gui_app.texture("icons_mici/onroad/onroad_fade.png")
|
||||||
|
|
||||||
@@ -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.set_position(self._rect.x + 16, self._rect.y + 10)
|
||||||
self._driver_state_renderer.render()
|
self._driver_state_renderer.render()
|
||||||
|
|
||||||
|
self._render_model_status()
|
||||||
|
|
||||||
self._hud_renderer.set_can_draw_top_icons(alert_to_render is None)
|
self._hud_renderer.set_can_draw_top_icons(alert_to_render is None)
|
||||||
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
|
self._hud_renderer.set_wheel_critical_icon(alert_to_render is not None and not not_animating_out and
|
||||||
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
|
alert_to_render.visual_alert == car.CarControl.HUDControl.VisualAlert.steerRequired)
|
||||||
@@ -248,6 +266,58 @@ class AugmentedRoadView(CameraView):
|
|||||||
|
|
||||||
self._bookmark_icon.render(self.rect)
|
self._bookmark_icon.render(self.rect)
|
||||||
|
|
||||||
|
def _render_model_status(self):
|
||||||
|
model = ui_state.sm["modelV2"]
|
||||||
|
if ui_state.sm.seen["modelV2"] and model.timestampEof:
|
||||||
|
model_age_ms = max(0., (time.monotonic_ns() - model.timestampEof) / 1e6)
|
||||||
|
execution_ms = max(0., model.modelExecutionTime * 1e3)
|
||||||
|
io_queue_ms = max(0., model_age_ms - execution_ms)
|
||||||
|
frame_drop = model.frameDropPerc
|
||||||
|
age_text = f"{model_age_ms:.0f} ms"
|
||||||
|
execution_text = f"{execution_ms:.0f} ms"
|
||||||
|
io_queue_text = f"{io_queue_ms:.0f} ms"
|
||||||
|
frame_drop_text = f"{frame_drop:.1f}%"
|
||||||
|
else:
|
||||||
|
model_age_ms = float("inf")
|
||||||
|
age_text = execution_text = io_queue_text = "-- ms"
|
||||||
|
frame_drop_text = "--%"
|
||||||
|
|
||||||
|
if ui_state.wgpu_enabled:
|
||||||
|
source, model_name = "WGPU", ui_state.wgpu_model_name
|
||||||
|
else:
|
||||||
|
source, model_name = "LOCAL", "DEVICE"
|
||||||
|
|
||||||
|
if model_age_ms < 200:
|
||||||
|
color = rl.Color(100, 255, 120, 230)
|
||||||
|
elif model_age_ms < 500:
|
||||||
|
color = rl.Color(255, 210, 80, 230)
|
||||||
|
else:
|
||||||
|
color = rl.Color(255, 120, 80, 230)
|
||||||
|
|
||||||
|
panel_w, panel_h = 250, 178
|
||||||
|
status_rect = rl.Rectangle(self._content_rect.x + self._content_rect.width - panel_w - 18,
|
||||||
|
self._content_rect.y + 18, panel_w, panel_h)
|
||||||
|
rl.draw_rectangle_rounded(status_rect, 0.14, 8, rl.Color(0, 0, 0, 175))
|
||||||
|
|
||||||
|
rows = (
|
||||||
|
("SOURCE", source),
|
||||||
|
("MODEL", model_name),
|
||||||
|
("AGE", age_text),
|
||||||
|
("EXEC", execution_text),
|
||||||
|
("IO/QUEUE", io_queue_text),
|
||||||
|
("DROPPED", frame_drop_text),
|
||||||
|
)
|
||||||
|
row_h = 26
|
||||||
|
for i, ((key, value), key_label, value_label) in enumerate(
|
||||||
|
zip(rows, self._model_status_key_labels, self._model_status_value_labels, strict=True)
|
||||||
|
):
|
||||||
|
row_rect = rl.Rectangle(status_rect.x + 14, status_rect.y + 11 + i * row_h, status_rect.width - 28, row_h)
|
||||||
|
key_label.set_text(key)
|
||||||
|
key_label.render(rl.Rectangle(row_rect.x, row_rect.y, 105, row_rect.height))
|
||||||
|
value_label.set_text(value)
|
||||||
|
value_label.set_text_color(color)
|
||||||
|
value_label.render(rl.Rectangle(row_rect.x + 108, row_rect.y, row_rect.width - 108, row_rect.height))
|
||||||
|
|
||||||
def _switch_stream_if_needed(self, sm):
|
def _switch_stream_if_needed(self, sm):
|
||||||
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
if sm['selfdriveState'].experimentalMode and WIDE_CAM in self.available_streams:
|
||||||
v_ego = sm['carState'].vEgo
|
v_ego = sm['carState'].vEgo
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class ModelsLayout(Widget):
|
|||||||
self._initialize_items()
|
self._initialize_items()
|
||||||
|
|
||||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay"), (self.camera_offset, "CameraOffset")]:
|
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||||
|
|
||||||
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
self._scroller = Scroller(self.items, line_separator=True, spacing=0)
|
||||||
@@ -93,14 +93,9 @@ class ModelsLayout(Widget):
|
|||||||
|
|
||||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||||
|
|
||||||
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
|
||||||
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
|
||||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
|
||||||
lambda v: f"{v / 100:.2f} m")
|
|
||||||
|
|
||||||
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
self.items = [self.current_model_item, self.cancel_download_item, self.supercombo_label, self.vision_label,
|
||||||
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item,
|
self.policy_label, self.off_policy_label, self.on_policy_label, self.refresh_item, self.clear_cache_item, self.lane_turn_desire_toggle,
|
||||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
self.lane_turn_value_control, self.lagd_toggle, self.delay_control]
|
||||||
|
|
||||||
def _update_lagd_description(self, lagd_toggle: bool):
|
def _update_lagd_description(self, lagd_toggle: bool):
|
||||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||||
@@ -237,7 +232,6 @@ class ModelsLayout(Widget):
|
|||||||
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
advanced_controls: bool = ui_state.params.get_bool("ShowAdvancedControls")
|
||||||
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
turn_desire: bool = ui_state.params.get_bool("LaneTurnDesire")
|
||||||
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
live_delay: bool = ui_state.params.get_bool("LagdToggle")
|
||||||
camera_offset: bool = ui_state.params.get("ModelManager_ActiveBundle") is not None
|
|
||||||
|
|
||||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||||
@@ -246,7 +240,6 @@ class ModelsLayout(Widget):
|
|||||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||||
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||||
self.lane_turn_value_control.action_item.value_change_step = new_step
|
self.lane_turn_value_control.action_item.value_change_step = new_step
|
||||||
self.camera_offset.set_visible(camera_offset)
|
|
||||||
|
|
||||||
self._update_lagd_description(live_delay)
|
self._update_lagd_description(live_delay)
|
||||||
self.model_manager = ui_state.sm["modelManagerSP"]
|
self.model_manager = ui_state.sm["modelManagerSP"]
|
||||||
|
|||||||
+2
-5
@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
|
|
||||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
|
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake
|
||||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||||
@@ -96,10 +96,7 @@ class MadsSettingsLayout(Widget):
|
|||||||
if brand == "rivian":
|
if brand == "rivian":
|
||||||
return True
|
return True
|
||||||
elif brand == "tesla":
|
elif brand == "tesla":
|
||||||
if ui_state.CP_SP is None or not ui_state.CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
|
return not (ui_state.CP_SP is not None and 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
|
return False
|
||||||
|
|
||||||
def _update_steering_mode_description(self, button_index: int):
|
def _update_steering_mode_description(self, button_index: int):
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|||||||
This file is part of sunnypilot and is licensed under the MIT License.
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
|
||||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
from openpilot.system.ui.lib.multilang import tr
|
from openpilot.system.ui.lib.multilang import tr
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import multiple_button_item_sp, toggle_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||||
|
|
||||||
COOP_STEERING_MIN_KMH = 23
|
COOP_STEERING_MIN_KMH = 23
|
||||||
OEM_STEERING_MIN_KMH = 48
|
OEM_STEERING_MIN_KMH = 48
|
||||||
@@ -19,14 +18,7 @@ class TeslaSettings(BrandSettings):
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
self.coop_steering_toggle = toggle_item_sp(tr("Cooperative Steering (Beta)"), "", param="TeslaCoopSteering")
|
||||||
self.mads_screen_button = multiple_button_item_sp(
|
self.items = [self.coop_steering_toggle]
|
||||||
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):
|
def update_settings(self):
|
||||||
is_metric = ui_state.is_metric
|
is_metric = ui_state.is_metric
|
||||||
@@ -49,18 +41,3 @@ class TeslaSettings(BrandSettings):
|
|||||||
|
|
||||||
self.coop_steering_toggle.set_description(coop_steering_desc)
|
self.coop_steering_toggle.set_description(coop_steering_desc)
|
||||||
self.coop_steering_toggle.action_item.set_enabled(ui_state.is_offroad())
|
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())
|
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ class UIState(UIStateSP):
|
|||||||
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
self.experimental_mode: bool = self.params.get_bool("ExperimentalMode")
|
||||||
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
self.usbgpu: bool = self.params.get_bool("UsbGpuPresent")
|
||||||
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
self.usbgpu_compiled: bool = self.params.get_bool("UsbGpuCompiled")
|
||||||
|
self.wgpu_enabled: bool = self.params.get_bool("WgpuEnabled")
|
||||||
|
self.wgpu_model_name: str = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||||
|
self.wgpu_ready: bool = self.params.get_bool("WgpuReady")
|
||||||
self.started: bool = False
|
self.started: bool = False
|
||||||
self.ignition: bool = False
|
self.ignition: bool = False
|
||||||
self.recording_audio: bool = False
|
self.recording_audio: bool = False
|
||||||
@@ -213,6 +216,9 @@ class UIState(UIStateSP):
|
|||||||
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
self.experimental_mode = self.params.get_bool("ExperimentalMode")
|
||||||
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
self.usbgpu = self.params.get_bool("UsbGpuPresent")
|
||||||
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
self.usbgpu_compiled = self.params.get_bool("UsbGpuCompiled")
|
||||||
|
self.wgpu_enabled = self.params.get_bool("WgpuEnabled")
|
||||||
|
self.wgpu_model_name = self.params.get("WgpuModelName") or "UNKNOWN"
|
||||||
|
self.wgpu_ready = self.params.get_bool("WgpuReady")
|
||||||
|
|
||||||
UIStateSP.update_params(self)
|
UIStateSP.update_params(self)
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from openpilot.common.params import Params
|
|||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
from opendbc.safety import ALTERNATIVE_EXPERIENCE
|
||||||
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
|
from opendbc.sunnypilot.car.hyundai.values import HyundaiFlagsSP, HyundaiSafetyFlagsSP
|
||||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||||
|
|
||||||
|
|
||||||
MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla")
|
MADS_NO_ACC_MAIN_BUTTON = ("rivian", "tesla")
|
||||||
@@ -21,20 +21,17 @@ class MadsSteeringModeOnBrake:
|
|||||||
DISENGAGE = 2
|
DISENGAGE = 2
|
||||||
|
|
||||||
|
|
||||||
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params) -> bool:
|
def get_mads_limited_brands(CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> bool:
|
||||||
if CP.brand == 'rivian':
|
if CP.brand == 'rivian':
|
||||||
return True
|
return True
|
||||||
if CP.brand == 'tesla':
|
if CP.brand == 'tesla':
|
||||||
if not CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS:
|
return 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
|
return False
|
||||||
|
|
||||||
|
|
||||||
def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
|
def read_steering_mode_param(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params):
|
||||||
if get_mads_limited_brands(CP, CP_SP, params):
|
if get_mads_limited_brands(CP, CP_SP):
|
||||||
return MadsSteeringModeOnBrake.DISENGAGE
|
return MadsSteeringModeOnBrake.DISENGAGE
|
||||||
|
|
||||||
return params.get("MadsSteeringMode", return_default=True)
|
return params.get("MadsSteeringMode", return_default=True)
|
||||||
@@ -66,7 +63,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
|
# 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
|
# 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
|
# 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, params)
|
mads_partial_support = get_mads_limited_brands(CP, CP_SP)
|
||||||
if mads_partial_support:
|
if mads_partial_support:
|
||||||
params.put("MadsSteeringMode", 2, block=True)
|
params.put("MadsSteeringMode", 2, block=True)
|
||||||
params.put_bool("MadsUnifiedEngagementMode", True, 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.selfdrive.selfdrived.events import EventsSP
|
||||||
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
|
from openpilot.sunnypilot.mads.helpers import MadsSteeringModeOnBrake, read_steering_mode_param
|
||||||
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
|
from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem
|
||||||
from opendbc.sunnypilot.car.tesla.values import MadsScreenButtonType, TeslaFlagsSP
|
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||||
|
|
||||||
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
State = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||||
EventName = log.OnroadEvent.EventName
|
EventName = log.OnroadEvent.EventName
|
||||||
@@ -38,12 +38,6 @@ def make_panda_state(mocker, controls_allowed_lateral=True):
|
|||||||
return ps
|
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):
|
def make_mads(mocker, steering_mode):
|
||||||
sd = mocker.MagicMock()
|
sd = mocker.MagicMock()
|
||||||
sd.CP = structs.CarParams()
|
sd.CP = structs.CarParams()
|
||||||
@@ -229,27 +223,15 @@ class TestBrandSteeringModeRestrictions:
|
|||||||
params = mocker.MagicMock()
|
params = mocker.MagicMock()
|
||||||
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
|
assert read_steering_mode_param(CP, CP_SP, params) == MadsSteeringModeOnBrake.DISENGAGE
|
||||||
|
|
||||||
@pytest.mark.parametrize("screen_button", [MadsScreenButtonType.THREE_FINGER,
|
def test_tesla_with_vehicle_bus_uses_param(self, mocker):
|
||||||
MadsScreenButtonType.FOUR_FINGER,
|
|
||||||
MadsScreenButtonType.FIVE_FINGER])
|
|
||||||
def test_tesla_with_vehicle_bus_uses_param(self, mocker, screen_button):
|
|
||||||
CP = structs.CarParams()
|
CP = structs.CarParams()
|
||||||
CP.brand = "tesla"
|
CP.brand = "tesla"
|
||||||
CP_SP = structs.CarParamsSP()
|
CP_SP = structs.CarParamsSP()
|
||||||
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
|
CP_SP.flags = TeslaFlagsSP.HAS_VEHICLE_BUS
|
||||||
params = make_params_mock(mocker, {"TeslaMadsScreenButton": screen_button,
|
params = mocker.MagicMock()
|
||||||
"MadsSteeringMode": MadsSteeringModeOnBrake.REMAIN_ACTIVE})
|
params.get = mocker.MagicMock(return_value=MadsSteeringModeOnBrake.REMAIN_ACTIVE)
|
||||||
assert read_steering_mode_param(CP, CP_SP, params) == 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"])
|
@pytest.mark.parametrize("brand", ["hyundai", "toyota", "honda", "gm"])
|
||||||
def test_other_brands_use_param(self, mocker, brand):
|
def test_other_brands_use_param(self, mocker, brand):
|
||||||
CP = structs.CarParams()
|
CP = structs.CarParams()
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
|||||||
# tesla
|
# tesla
|
||||||
keys.extend([
|
keys.extend([
|
||||||
"TeslaCoopSteering",
|
"TeslaCoopSteering",
|
||||||
"TeslaMadsScreenButton",
|
|
||||||
])
|
])
|
||||||
|
|
||||||
# toyota
|
# toyota
|
||||||
|
|||||||
@@ -151,8 +151,8 @@ class SmartCruiseControlMap:
|
|||||||
a = 0.5 * TARGET_JERK
|
a = 0.5 * TARGET_JERK
|
||||||
b = self.a_ego
|
b = self.a_ego
|
||||||
c = self.v_ego - tv
|
c = self.v_ego - tv
|
||||||
t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / (2 * a)
|
t_a = -1 * ((b**2 - 4 * a * c) ** 0.5 + b) / 2 * a
|
||||||
t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / (2 * a)
|
t_b = ((b**2 - 4 * a * c) ** 0.5 - b) / 2 * a
|
||||||
if not isinstance(t_a, complex) and t_a > 0:
|
if not isinstance(t_a, complex) and t_a > 0:
|
||||||
t = t_a
|
t = t_a
|
||||||
else:
|
else:
|
||||||
|
|||||||
+1
-18
@@ -4,17 +4,13 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
|||||||
This file is part of sunnypilot and is licensed under the MIT License.
|
This file is part of sunnypilot and is licensed under the MIT License.
|
||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
import json
|
|
||||||
import math
|
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import R, SmartCruiseControlMap
|
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.map_controller import SmartCruiseControlMap
|
||||||
|
|
||||||
MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
|
MapState = VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.MapState
|
||||||
|
|
||||||
@@ -59,17 +55,4 @@ class TestSmartCruiseControlMap:
|
|||||||
self.scc_m.update(True, False, 0., 0., 0.)
|
self.scc_m.update(True, False, 0., 0., 0.)
|
||||||
assert self.scc_m.state == VisionState.enabled
|
assert self.scc_m.state == VisionState.enabled
|
||||||
|
|
||||||
def test_moderate_curve(self):
|
|
||||||
# Regression: `... / 2 * a` parsed as `(.../2)*a` instead of `.../(2*a)`,
|
|
||||||
# making max_d ~11x too small so the moderate-curve branch never tripped.
|
|
||||||
# v_ego=25, a_ego=0, tv=24: fixed max_d≈45m vs buggy ≈4m at a 40m waypoint.
|
|
||||||
waypoint_lon_deg = (40.0 / R) * (180.0 / math.pi)
|
|
||||||
self.mem_params.put("LastGPSPosition", json.dumps({"latitude": 0.0, "longitude": 0.0}), block=True)
|
|
||||||
self.mem_params.put("MapTargetVelocities",
|
|
||||||
json.dumps([{"latitude": 0.0, "longitude": waypoint_lon_deg, "velocity": 24.0}]), block=True)
|
|
||||||
|
|
||||||
self.scc_m.update(True, False, 25.0, 0.0, 30.0)
|
|
||||||
|
|
||||||
assert self.scc_m.v_target == pytest.approx(24.0)
|
|
||||||
|
|
||||||
# TODO-SP: mock data from modelV2 to test other states
|
# TODO-SP: mock data from modelV2 to test other states
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ class SpeedLimitAssist:
|
|||||||
|
|
||||||
self._plus_hold = 0.
|
self._plus_hold = 0.
|
||||||
self._minus_hold = 0.
|
self._minus_hold = 0.
|
||||||
self._release_toggle_prev = 0
|
self._last_carstate_ts = 0.
|
||||||
|
|
||||||
# TODO-SP: SLA's own output_a_target for planner
|
# TODO-SP: SLA's own output_a_target for planner
|
||||||
# Solution functions mapped to respective states
|
# Solution functions mapped to respective states
|
||||||
@@ -146,16 +146,16 @@ class SpeedLimitAssist:
|
|||||||
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
|
||||||
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
|
||||||
|
|
||||||
def update_buttons(self, release_toggle: int) -> None:
|
def update_car_state(self, CS: car.CarState) -> None:
|
||||||
released = self._release_toggle_prev ^ release_toggle
|
|
||||||
self._release_toggle_prev = release_toggle
|
|
||||||
if not released:
|
|
||||||
return
|
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
if any((released >> b) & 1 for b in CRUISE_BUTTONS_PLUS):
|
self._last_carstate_ts = now
|
||||||
self._plus_hold = max(self._plus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
|
||||||
if any((released >> b) & 1 for b in CRUISE_BUTTONS_MINUS):
|
for b in CS.buttonEvents:
|
||||||
self._minus_hold = max(self._minus_hold, now + CRUISE_BUTTON_CONFIRM_HOLD)
|
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)
|
||||||
|
|
||||||
def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool:
|
def _get_button_release(self, req_plus: bool, req_minus: bool) -> bool:
|
||||||
now = time.monotonic()
|
now = time.monotonic()
|
||||||
|
|||||||
+1
-91
@@ -5,14 +5,11 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
|||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
from opendbc.car.car_helpers import interfaces
|
from opendbc.car.car_helpers import interfaces
|
||||||
from opendbc.car.rivian.values import CAR as RIVIAN
|
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.tesla.values import CAR as TESLA
|
||||||
from opendbc.car.toyota.values import CAR as TOYOTA
|
from opendbc.car.toyota.values import CAR as TOYOTA
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
@@ -24,13 +21,9 @@ 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 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.common import Mode
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
|
||||||
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES, CRUISE_BUTTON_CONFIRM_HOLD
|
PRE_ACTIVE_GUARD_PERIOD, ACTIVE_STATES
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.button_state_tracker import ButtonStateTracker
|
|
||||||
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
|
||||||
|
|
||||||
ButtonEvent = car.CarState.ButtonEvent
|
|
||||||
ButtonType = car.CarState.ButtonEvent.Type
|
|
||||||
|
|
||||||
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
|
||||||
|
|
||||||
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
|
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
|
||||||
@@ -283,86 +276,3 @@ class TestSpeedLimitAssist:
|
|||||||
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
|
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
|
||||||
elif initial_state in ACTIVE_STATES:
|
elif initial_state in ACTIVE_STATES:
|
||||||
assert self.sla.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)
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
"""
|
|
||||||
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
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
"""
|
|
||||||
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,42 +2161,6 @@
|
|||||||
"type": "offroad_only"
|
"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,28 +56,6 @@ sections:
|
|||||||
title: Cooperative Steering (Beta)
|
title: Cooperative Steering (Beta)
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/offroad'
|
- $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
|
- id: toyota
|
||||||
title: Toyota / Lexus Settings
|
title: Toyota / Lexus Settings
|
||||||
description: ''
|
description: ''
|
||||||
|
|||||||
@@ -17,26 +17,6 @@ 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())
|
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):
|
def _migrate_car_platform_bundle(_params):
|
||||||
bundle = _params.get("CarPlatformBundle")
|
bundle = _params.get("CarPlatformBundle")
|
||||||
if bundle is None:
|
if bundle is None:
|
||||||
@@ -67,23 +47,6 @@ def _migrate_car_platform_bundle(_params):
|
|||||||
cloudlog.info(f"params_migration: CarPlatformBundle migrated {old_platform!r} -> {new_platform!r}")
|
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):
|
def run_migration(_params):
|
||||||
# migrate OnroadScreenOffBrightness
|
# migrate OnroadScreenOffBrightness
|
||||||
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
|
if _params.get("OnroadScreenOffBrightnessMigrated") != ONROAD_BRIGHTNESS_MIGRATION_VERSION:
|
||||||
@@ -117,6 +80,3 @@ def run_migration(_params):
|
|||||||
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")
|
cloudlog.exception(f"Error migrating OnroadScreenOffTimer: {e}")
|
||||||
|
|
||||||
_migrate_car_platform_bundle(_params)
|
_migrate_car_platform_bundle(_params)
|
||||||
|
|
||||||
# seed TeslaMadsScreenButton for existing Tesla installs
|
|
||||||
_migrate_tesla_mads_screen_button(_params)
|
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ def is_stock_model(started, params, CP: car.CarParams) -> bool:
|
|||||||
"""Check if the active model runner is stock."""
|
"""Check if the active model runner is stock."""
|
||||||
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock)
|
return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock)
|
||||||
|
|
||||||
|
def not_wgpu(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||||
|
return not params.get_bool("WgpuEnabled")
|
||||||
|
|
||||||
def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
|
def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool:
|
||||||
return bool(os.path.exists(Paths.mapd_root()))
|
return bool(os.path.exists(Paths.mapd_root()))
|
||||||
|
|
||||||
@@ -128,7 +131,7 @@ procs = [
|
|||||||
PythonProcess("micd", "openpilot.system.micd", iscar),
|
PythonProcess("micd", "openpilot.system.micd", iscar),
|
||||||
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
PythonProcess("timed", "openpilot.system.timed", always_run, enabled=not PC),
|
||||||
|
|
||||||
PythonProcess("modeld", "openpilot.selfdrive.modeld.modeld", and_(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("dmonitoringmodeld", "openpilot.selfdrive.modeld.dmonitoringmodeld", driverview, enabled=(WEBCAM or not PC)),
|
||||||
|
|
||||||
PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC),
|
PythonProcess("sensord", "openpilot.system.sensord.sensord", only_onroad, enabled=not PC),
|
||||||
@@ -177,7 +180,7 @@ procs = [
|
|||||||
procs += [
|
procs += [
|
||||||
# Models
|
# Models
|
||||||
PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad),
|
PythonProcess("models_manager", "openpilot.sunnypilot.models.manager", only_offroad),
|
||||||
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(only_onroad, is_tinygrad_model)),
|
NativeProcess("modeld_tinygrad", "openpilot/sunnypilot/modeld_v2", ["./modeld"], and_(and_(only_onroad, is_tinygrad_model), not_wgpu)),
|
||||||
|
|
||||||
# Backup
|
# Backup
|
||||||
PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
PythonProcess("backup_manager", "openpilot.sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
|
||||||
import argparse
|
import argparse
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import time
|
import time
|
||||||
@@ -10,6 +9,7 @@ from collections import deque
|
|||||||
import openpilot.cereal.messaging as messaging
|
import openpilot.cereal.messaging as messaging
|
||||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||||
from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError
|
from openpilot.tools.camerastream.ffmpeg_decoder import Decoder, FFmpegError
|
||||||
|
from openpilot.tools.wgpu.zmq import ZmqSubMaster, ZmqSubSocket
|
||||||
|
|
||||||
V4L2_BUF_FLAG_KEYFRAME = 8
|
V4L2_BUF_FLAG_KEYFRAME = 8
|
||||||
|
|
||||||
@@ -30,10 +30,7 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
|
|
||||||
codec = Decoder("hevc")
|
codec = Decoder("hevc")
|
||||||
|
|
||||||
os.environ["ZMQ"] = "1"
|
sock = ZmqSubSocket(sock_name, addr)
|
||||||
messaging.reset_context()
|
|
||||||
sock = messaging.sub_sock(sock_name, None, addr=addr, conflate=False)
|
|
||||||
cnt = 0
|
|
||||||
last_idx = -1
|
last_idx = -1
|
||||||
seen_iframe = False
|
seen_iframe = False
|
||||||
|
|
||||||
@@ -46,8 +43,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
time_q.clear()
|
time_q.clear()
|
||||||
|
|
||||||
while 1:
|
while 1:
|
||||||
msgs = messaging.drain_sock(sock, wait_for_one=True)
|
msgs = sock.drain(wait_for_one=True)
|
||||||
for evt in msgs:
|
for raw in msgs:
|
||||||
|
evt = messaging.log_from_bytes(raw)
|
||||||
evta = getattr(evt, evt.which())
|
evta = getattr(evt, evt.which())
|
||||||
if last_idx != -1 and evta.idx.encodeId != (last_idx + 1):
|
if last_idx != -1 and evta.idx.encodeId != (last_idx + 1):
|
||||||
if debug:
|
if debug:
|
||||||
@@ -94,8 +92,9 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
frame_start_time = time_q.popleft()
|
frame_start_time = time_q.popleft()
|
||||||
vipc_server.send(vst, img_yuv.data, cnt, int(frame_start_time*1e9), int(time.monotonic()*1e9))
|
# Preserve the device camera metadata so remote model outputs line up with
|
||||||
cnt += 1
|
# 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
|
pc_latency = (time.monotonic()-frame_start_time)*1000
|
||||||
if debug:
|
if debug:
|
||||||
@@ -105,25 +104,30 @@ def decoder(addr, vipc_server, vst, W, H, debug=False):
|
|||||||
|
|
||||||
class CompressedVipc:
|
class CompressedVipc:
|
||||||
def __init__(self, addr, vision_streams, server_name, debug=False):
|
def __init__(self, addr, vision_streams, server_name, debug=False):
|
||||||
print("getting frame sizes")
|
print("waiting for remote camera stream metadata", flush=True)
|
||||||
os.environ["ZMQ"] = "1"
|
sm = ZmqSubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr)
|
||||||
messaging.reset_context()
|
|
||||||
sm = messaging.SubMaster([ENCODE_SOCKETS[s] for s in vision_streams], addr=addr)
|
|
||||||
while min(sm.recv_frame.values()) == 0:
|
while min(sm.recv_frame.values()) == 0:
|
||||||
sm.update(100)
|
sm.update(100)
|
||||||
os.environ.pop("ZMQ")
|
|
||||||
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)
|
self.vipc_server = VisionIpcServer(server_name)
|
||||||
for vst in vision_streams:
|
for vst in vision_streams:
|
||||||
ed = sm[ENCODE_SOCKETS[vst]]
|
width, height = stream_dimensions[vst]
|
||||||
self.vipc_server.create_buffers(vst, 4, ed.width, ed.height)
|
self.vipc_server.create_buffers(vst, 4, width, height)
|
||||||
self.vipc_server.start_listener()
|
self.vipc_server.start_listener()
|
||||||
|
|
||||||
self.procs = []
|
self.procs = []
|
||||||
|
process_context = multiprocessing.get_context("fork")
|
||||||
for vst in vision_streams:
|
for vst in vision_streams:
|
||||||
ed = sm[ENCODE_SOCKETS[vst]]
|
width, height = stream_dimensions[vst]
|
||||||
p = multiprocessing.Process(target=decoder, args=(addr, self.vipc_server, vst, ed.width, ed.height, debug))
|
p = process_context.Process(target=decoder, args=(addr, self.vipc_server, vst, width, height, debug))
|
||||||
p.start()
|
p.start()
|
||||||
self.procs.append(p)
|
self.procs.append(p)
|
||||||
|
|
||||||
|
|||||||
@@ -46,10 +46,23 @@ def _bind(fn, restype, *argtypes):
|
|||||||
return fn
|
return fn
|
||||||
|
|
||||||
|
|
||||||
|
def _library_path(name: str, major: int) -> str:
|
||||||
|
candidates = (
|
||||||
|
f"lib{name}.so.{major}",
|
||||||
|
f"lib{name}.{major}.dylib",
|
||||||
|
f"lib{name}.dylib",
|
||||||
|
)
|
||||||
|
for candidate in candidates:
|
||||||
|
path = os.path.join(ffmpeg.LIB_DIR, candidate)
|
||||||
|
if os.path.isfile(path):
|
||||||
|
return path
|
||||||
|
raise FileNotFoundError(f"FFmpeg library not found in {ffmpeg.LIB_DIR}: {', '.join(candidates)}")
|
||||||
|
|
||||||
|
|
||||||
def _load_libraries():
|
def _load_libraries():
|
||||||
avutil = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavutil.so.59"), mode=ctypes.RTLD_GLOBAL)
|
avutil = ctypes.CDLL(_library_path("avutil", 59), mode=ctypes.RTLD_GLOBAL)
|
||||||
avcodec = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libavcodec.so.61"), mode=ctypes.RTLD_GLOBAL)
|
avcodec = ctypes.CDLL(_library_path("avcodec", 61), mode=ctypes.RTLD_GLOBAL)
|
||||||
swscale = ctypes.CDLL(os.path.join(ffmpeg.LIB_DIR, "libswscale.so.8"), 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_int, c_char_p, c_void_p, c_size_t = ctypes.c_int, ctypes.c_char_p, ctypes.c_void_p, ctypes.c_size_t
|
||||||
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)
|
c_uint8_p = ctypes.POINTER(ctypes.c_uint8)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Wireless modeld proof of concept
|
||||||
|
|
||||||
|
This runs driving `modeld` on a laptop and returns its cereal outputs to a comma
|
||||||
|
device over the existing Wi-Fi network. It reuses the existing HEVC camera
|
||||||
|
stream, VisionIPC decoder, and cereal ZMQ bridge.
|
||||||
|
|
||||||
|
This is for controlled bench testing only. Wi-Fi has no deterministic latency
|
||||||
|
or availability guarantee. The device-side helper switches only after receiving
|
||||||
|
a fresh remote model and after manager has stopped the local model publisher.
|
||||||
|
It restores local `modeld` if the remote model is missing for one second.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Use the same commit on the laptop and comma device. Build the cereal bridge on
|
||||||
|
the device:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
scons -u openpilot/cereal/messaging/bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and test the normal model on the laptop first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PATH="$PWD/.venv/bin:$PATH" scons -u
|
||||||
|
```
|
||||||
|
|
||||||
|
To compile the big external-GPU model for the laptop's local tinygrad backend:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PATH="$PWD/.venv/bin:$PATH" WGPU=1 scons -u \
|
||||||
|
openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl.chunkmanifest
|
||||||
|
```
|
||||||
|
|
||||||
|
On macOS, the build selects tinygrad's Metal backend when it is available.
|
||||||
|
On an 8-GPU-core M5 MacBook Air, the small model's compiled policy pass measured
|
||||||
|
about 6–11 ms, while the big model measured about 79–81 ms. The latter already
|
||||||
|
misses the 50 ms model cadence before network and codec latency, so start with
|
||||||
|
the small model on that class of laptop.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
Find the laptop's LAN IP address that the comma device can reach. The helper can
|
||||||
|
start while onroad: it forwards camera/state while local `modeld` remains active,
|
||||||
|
then performs an exclusive publisher handoff after the laptop produces a fresh
|
||||||
|
valid model:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /data/openpilot
|
||||||
|
python3 -m openpilot.tools.wgpu.device LAPTOP_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep that terminal open. On the laptop, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /path/to/openpilot
|
||||||
|
python3 -m openpilot.tools.wgpu.host COMMA_IP
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `--big-model` after `COMMA_IP` to use the locally compiled big model.
|
||||||
|
|
||||||
|
The device helper sends cached `carParams` over a dedicated startup channel, so
|
||||||
|
the host does not attach to the camera streams and accumulate stale frames while
|
||||||
|
waiting for the periodic state bridge. Stop either side with Ctrl+C. A host
|
||||||
|
disconnect automatically stops remote publication and restores local `modeld`
|
||||||
|
after a one-second timeout; the device helper then stays running and waits for
|
||||||
|
the next host session. Stop the device helper itself with Ctrl+C before changing
|
||||||
|
branches or rebooting.
|
||||||
|
|
||||||
|
While WGPU is active, model lag does not create an engagement-blocking alert.
|
||||||
|
The fixed-column diagnostics panel at the top-right of the mici onroad UI shows
|
||||||
|
the active source (`LOCAL` or `WGPU`), remote model size (`SMALL` or `BIG`),
|
||||||
|
total model-frame age, execution time, remaining I/O and queue time, and dropped
|
||||||
|
frames. The home GPU icon is gray while the device helper is ready and waiting,
|
||||||
|
and green while a valid remote model is active.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import openpilot.cereal.messaging as messaging
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.tools.wgpu.zmq import WGPU_CAR_PARAMS, ZmqPubMaster, ZmqSubSocket
|
||||||
|
|
||||||
|
|
||||||
|
MODEL_OUTPUTS = "modelV2,drivingModelData,cameraOdometry,modelDataV2SP"
|
||||||
|
MODEL_PROCESSES = {"modeld", "modeld_tinygrad"}
|
||||||
|
REMOTE_MODEL_TIMEOUT = 1.0
|
||||||
|
WGPU_STATUS = "wgpuStatus"
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
BRIDGE = ROOT / "openpilot/cereal/messaging/bridge"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_process(proc: subprocess.Popen) -> None:
|
||||||
|
proc.terminate()
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
proc.kill()
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_sigterm(*_) -> None:
|
||||||
|
raise KeyboardInterrupt
|
||||||
|
|
||||||
|
|
||||||
|
def receive_model(sock: ZmqSubSocket) -> tuple[bool, float]:
|
||||||
|
raw = sock.receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
return False, float("inf")
|
||||||
|
event = messaging.log_from_bytes(raw)
|
||||||
|
if event.which() != "modelV2" or not event.valid:
|
||||||
|
return True, float("inf")
|
||||||
|
model_age = (time.monotonic_ns() - event.modelV2.timestampEof) / 1e9
|
||||||
|
return True, model_age
|
||||||
|
|
||||||
|
|
||||||
|
def receive_model_name(sock: ZmqSubSocket) -> str | None:
|
||||||
|
raw = sock.receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
return None
|
||||||
|
model_name = raw.decode(errors="replace").upper()
|
||||||
|
return model_name if model_name in ("SMALL", "BIG") else None
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_local_modeld_stop(timeout: float = 10.) -> None:
|
||||||
|
sm = messaging.SubMaster(["managerState"])
|
||||||
|
deadline = time.monotonic() + timeout
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
sm.update(100)
|
||||||
|
if sm.seen["managerState"]:
|
||||||
|
running = {p.name for p in sm["managerState"].processes if p.running}
|
||||||
|
if not running.intersection(MODEL_PROCESSES):
|
||||||
|
return
|
||||||
|
raise RuntimeError("timed out waiting for local modeld publisher to stop")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Route modeld traffic between this device and a wireless host.")
|
||||||
|
parser.add_argument("host", help="Laptop IP address reachable from this device")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not BRIDGE.is_file():
|
||||||
|
raise FileNotFoundError(f"build the cereal bridge first: {BRIDGE}")
|
||||||
|
|
||||||
|
params = Params()
|
||||||
|
forward: subprocess.Popen | None = None
|
||||||
|
reverse: subprocess.Popen | None = None
|
||||||
|
wgpu_enabled = False
|
||||||
|
try:
|
||||||
|
forward = subprocess.Popen([str(BRIDGE)])
|
||||||
|
params.put_bool("WgpuReady", True, block=True)
|
||||||
|
car_params_pub = ZmqPubMaster([WGPU_CAR_PARAMS])
|
||||||
|
remote_model = ZmqSubSocket("modelV2", args.host, conflate=True)
|
||||||
|
remote_status = ZmqSubSocket(WGPU_STATUS, args.host, conflate=True)
|
||||||
|
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
# Keep local modeld publishing while a host connects and warms up.
|
||||||
|
print(f"forwarding camera/state to {args.host}; waiting for a fresh remote model")
|
||||||
|
model_name = None
|
||||||
|
while True:
|
||||||
|
if car_params is None:
|
||||||
|
car_params = params.get("CarParams") or params.get("CarParamsPersistent")
|
||||||
|
if car_params is not None:
|
||||||
|
# This dedicated conflated startup channel lets remote modeld obtain CP
|
||||||
|
# before it connects to VisionIPC and accumulates stale frame metadata.
|
||||||
|
car_params_pub.send_raw(WGPU_CAR_PARAMS, car_params)
|
||||||
|
received, model_age = receive_model(remote_model)
|
||||||
|
model_name = receive_model_name(remote_status) or model_name
|
||||||
|
if received and 0 <= model_age < REMOTE_MODEL_TIMEOUT and model_name is not None:
|
||||||
|
break
|
||||||
|
if forward.poll() is not None:
|
||||||
|
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
# Stop the local publisher before attaching the reverse bridge. msgq permits
|
||||||
|
# only one publisher for each model service.
|
||||||
|
params.put("WgpuModelName", model_name, block=True)
|
||||||
|
params.put_bool("WgpuEnabled", True, block=True)
|
||||||
|
wgpu_enabled = True
|
||||||
|
wait_for_local_modeld_stop()
|
||||||
|
reverse = subprocess.Popen([str(BRIDGE), args.host, MODEL_OUTPUTS])
|
||||||
|
print("wgpu active; Ctrl+C or loss of the remote model restores local modeld")
|
||||||
|
|
||||||
|
last_remote_model = time.monotonic()
|
||||||
|
while True:
|
||||||
|
received, _ = receive_model(remote_model)
|
||||||
|
if received:
|
||||||
|
last_remote_model = time.monotonic()
|
||||||
|
if forward.poll() is not None:
|
||||||
|
raise RuntimeError(f"forward bridge exited with status {forward.returncode}")
|
||||||
|
if reverse.poll() is not None:
|
||||||
|
print(f"reverse bridge exited with status {reverse.returncode}; restoring local modeld")
|
||||||
|
break
|
||||||
|
if time.monotonic() - last_remote_model > REMOTE_MODEL_TIMEOUT:
|
||||||
|
print("remote model timed out; restoring local modeld")
|
||||||
|
break
|
||||||
|
time.sleep(0.05)
|
||||||
|
|
||||||
|
stop_process(reverse)
|
||||||
|
reverse = None
|
||||||
|
params.put_bool("WgpuEnabled", False, block=True)
|
||||||
|
wgpu_enabled = False
|
||||||
|
params.remove("WgpuModelName")
|
||||||
|
print("wgpu disabled; local modeld restored; ready for the next host run")
|
||||||
|
finally:
|
||||||
|
# Stop remote publication before allowing the local publisher to restart.
|
||||||
|
if reverse is not None and reverse.poll() is None:
|
||||||
|
stop_process(reverse)
|
||||||
|
if wgpu_enabled:
|
||||||
|
params.put_bool("WgpuEnabled", False, block=True)
|
||||||
|
params.put_bool("WgpuReady", False, block=True)
|
||||||
|
params.remove("WgpuModelName")
|
||||||
|
if forward is not None and forward.poll() is None:
|
||||||
|
stop_process(forward)
|
||||||
|
print("wgpu disabled; local modeld restored")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
signal.signal(signal.SIGTERM, handle_sigterm)
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from openpilot.tools.wgpu.zmq import ZmqPubMaster
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[3]
|
||||||
|
CAMERASTREAM = ROOT / "openpilot/tools/camerastream/compressed_vipc.py"
|
||||||
|
WGPU_STATUS = "wgpuStatus"
|
||||||
|
|
||||||
|
|
||||||
|
def stop_process(proc: subprocess.Popen) -> None:
|
||||||
|
try:
|
||||||
|
os.killpg(proc.pid, signal.SIGTERM)
|
||||||
|
except ProcessLookupError:
|
||||||
|
proc.wait()
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
proc.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(description="Run modeld on this host for a remote comma device.")
|
||||||
|
parser.add_argument("device", help="comma device hostname or IP address")
|
||||||
|
parser.add_argument("--big-model", action="store_true", help="use the locally compiled big driving model")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
camera = subprocess.Popen([sys.executable, str(CAMERASTREAM), args.device, "--cams", "0,2"], start_new_session=True)
|
||||||
|
model_args = [sys.executable, "-m", "openpilot.selfdrive.modeld.modeld", "--remote", args.device]
|
||||||
|
if args.big_model:
|
||||||
|
model_args.append("--big-model")
|
||||||
|
model = subprocess.Popen(model_args, cwd=ROOT, start_new_session=True)
|
||||||
|
status = ZmqPubMaster([WGPU_STATUS])
|
||||||
|
model_name = b"BIG" if args.big_model else b"SMALL"
|
||||||
|
|
||||||
|
procs = {"camera bridge": camera, "modeld": model}
|
||||||
|
try:
|
||||||
|
while all(proc.poll() is None for proc in procs.values()):
|
||||||
|
status.send_raw(WGPU_STATUS, model_name)
|
||||||
|
time.sleep(0.25)
|
||||||
|
failed_name, failed = next((name, proc) for name, proc in procs.items() if proc.poll() is not None)
|
||||||
|
raise RuntimeError(f"wgpu {failed_name} exited with status {failed.returncode}")
|
||||||
|
finally:
|
||||||
|
for proc in procs.values():
|
||||||
|
if proc.poll() is None:
|
||||||
|
stop_process(proc)
|
||||||
|
print("wgpu host stopped")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
pass
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
|
import zmq
|
||||||
|
|
||||||
|
import openpilot.cereal.messaging as messaging
|
||||||
|
|
||||||
|
|
||||||
|
WGPU_CAR_PARAMS = "wgpuCarParams"
|
||||||
|
|
||||||
|
|
||||||
|
def service_port(endpoint: str) -> int:
|
||||||
|
# Keep this in sync with cereal/messaging/bridge_zmq.cc.
|
||||||
|
value = 0xcbf29ce484222325
|
||||||
|
for char in endpoint.encode():
|
||||||
|
value ^= char
|
||||||
|
value = (value * 0x100000001b3) & 0xffffffffffffffff
|
||||||
|
return 8023 + (value % (65535 - 8023))
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqSubSocket:
|
||||||
|
def __init__(self, endpoint: str, address: str, conflate: bool = False):
|
||||||
|
self.context = zmq.Context()
|
||||||
|
self.socket = self.context.socket(zmq.SUB)
|
||||||
|
self.socket.setsockopt(zmq.SUBSCRIBE, b"")
|
||||||
|
self.socket.setsockopt(zmq.RECONNECT_IVL_MAX, 500)
|
||||||
|
if conflate:
|
||||||
|
self.socket.setsockopt(zmq.CONFLATE, 1)
|
||||||
|
self.socket.connect(f"tcp://{address}:{service_port(endpoint)}")
|
||||||
|
|
||||||
|
def receive(self, non_blocking: bool = False) -> bytes | None:
|
||||||
|
try:
|
||||||
|
return self.socket.recv(flags=zmq.NOBLOCK if non_blocking else 0)
|
||||||
|
except zmq.Again:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def drain(self, wait_for_one: bool = False) -> list[bytes]:
|
||||||
|
messages = []
|
||||||
|
if wait_for_one:
|
||||||
|
message = self.receive()
|
||||||
|
if message is not None:
|
||||||
|
messages.append(message)
|
||||||
|
while (message := self.receive(non_blocking=True)) is not None:
|
||||||
|
messages.append(message)
|
||||||
|
return messages
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self.socket.close(linger=0)
|
||||||
|
self.context.term()
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqSubMaster:
|
||||||
|
def __init__(self, services: list[str], address: str):
|
||||||
|
self.services = services
|
||||||
|
self.sockets = {service: ZmqSubSocket(service, address, conflate=True) for service in services}
|
||||||
|
self.poller = zmq.Poller()
|
||||||
|
self.socket_to_service = {}
|
||||||
|
for service, sub in self.sockets.items():
|
||||||
|
self.poller.register(sub.socket, zmq.POLLIN)
|
||||||
|
self.socket_to_service[sub.socket] = service
|
||||||
|
|
||||||
|
self.data = {service: getattr(messaging.new_message(service).as_reader(), service) for service in services}
|
||||||
|
self.seen = dict.fromkeys(services, False)
|
||||||
|
self.updated = dict.fromkeys(services, False)
|
||||||
|
self.recv_frame = dict.fromkeys(services, 0)
|
||||||
|
self.frame = -1
|
||||||
|
|
||||||
|
def __getitem__(self, service: str):
|
||||||
|
return self.data[service]
|
||||||
|
|
||||||
|
def update(self, timeout: int = 100) -> None:
|
||||||
|
self.frame += 1
|
||||||
|
self.updated = dict.fromkeys(self.services, False)
|
||||||
|
for socket, _ in self.poller.poll(timeout):
|
||||||
|
service = self.socket_to_service[socket]
|
||||||
|
raw = self.sockets[service].receive(non_blocking=True)
|
||||||
|
if raw is None:
|
||||||
|
continue
|
||||||
|
event = messaging.log_from_bytes(raw)
|
||||||
|
self.data[service] = getattr(event, service)
|
||||||
|
self.seen[service] = True
|
||||||
|
self.updated[service] = True
|
||||||
|
self.recv_frame[service] = self.frame
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
for sub in self.sockets.values():
|
||||||
|
self.poller.unregister(sub.socket)
|
||||||
|
sub.close()
|
||||||
|
|
||||||
|
|
||||||
|
class ZmqPubMaster:
|
||||||
|
def __init__(self, services: list[str]):
|
||||||
|
context = zmq.Context.instance()
|
||||||
|
self.sockets = {}
|
||||||
|
for service in services:
|
||||||
|
socket = context.socket(zmq.PUB)
|
||||||
|
socket.bind(f"tcp://*:{service_port(service)}")
|
||||||
|
self.sockets[service] = socket
|
||||||
|
# Give already-running bridge subscribers time to finish their handshake.
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
def send(self, service: str, message) -> None:
|
||||||
|
self.sockets[service].send(message.to_bytes(), flags=zmq.NOBLOCK)
|
||||||
|
|
||||||
|
def send_raw(self, service: str, data: bytes) -> None:
|
||||||
|
self.sockets[service].send(data, flags=zmq.NOBLOCK)
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/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
|
||||||
Reference in New Issue
Block a user