diff --git a/.github/workflows/mici_raylib_ui_preview.yaml b/.github/workflows/mici_raylib_ui_preview.yaml index 707825b1a..555258652 100644 --- a/.github/workflows/mici_raylib_ui_preview.yaml +++ b/.github/workflows/mici_raylib_ui_preview.yaml @@ -24,7 +24,7 @@ env: jobs: preview: - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' name: preview runs-on: ubuntu-latest timeout-minutes: 20 @@ -64,7 +64,7 @@ jobs: - name: Getting master ui # filename: master_ui_raylib/mici_ui_replay.mp4 uses: actions/checkout@v4 with: - repository: commaai/ci-artifacts + repository: sunnypilot/ci-artifacts ssh-key: ${{ secrets.CI_ARTIFACTS_DEPLOY_KEY }} path: ${{ github.workspace }}/master_ui_raylib ref: ${{ env.MASTER_BRANCH_NAME }} @@ -100,7 +100,7 @@ jobs: # Run report export PYTHONPATH=${{ github.workspace }} - baseurl="https://github.com/commaai/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" + baseurl="https://github.com/sunnypilot/ci-artifacts/raw/refs/heads/${{ env.BRANCH_NAME }}" diff_exit_code=0 python3 ${{ github.workspace }}/selfdrive/ui/tests/diff/diff.py "${{ github.workspace }}/pr_ui/mici_ui_replay_master.mp4" "${{ github.workspace }}/pr_ui/mici_ui_replay_proposed.mp4" "diff.html" --basedir "$baseurl" --no-open || diff_exit_code=$? @@ -108,7 +108,7 @@ jobs: cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.html ${{ github.workspace }}/pr_ui/ cp ${{ github.workspace }}/selfdrive/ui/tests/diff/report/diff.mp4 ${{ github.workspace }}/pr_ui/ - REPORT_URL="https://commaai.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html" + REPORT_URL="https://sunnypilot.github.io/ci-artifacts/diff_pr_${{ github.event.number }}.html" if [ $diff_exit_code -eq 0 ]; then DIFF="✅ Videos are identical! [View Diff Report]($REPORT_URL)" else diff --git a/.github/workflows/sunnypilot-master-dev-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml index e7a966374..10794bf0f 100644 --- a/.github/workflows/sunnypilot-master-dev-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -49,6 +49,7 @@ jobs: with: fetch-depth: 0 # Fetch all history for all branches token: ${{ secrets.GITHUB_TOKEN }} + persist-credentials: false - name: Wait for Tests uses: ./.github/workflows/wait-for-action # Path to where you place the action @@ -173,11 +174,20 @@ jobs: echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig echo ' locksverify = false' >> .lfsconfig + - uses: actions/create-github-app-token@v2 + id: ci-token + with: + app-id: ${{ secrets.CI_GITHUB_ACTIONS_TOKEN_APP_ID }} + private-key: ${{ secrets.CI_GITHUB_ACTIONS_TOKEN_APP_PRIVATE_KEY }} + - name: Push changes if there are diffs - id: push-changes # Add an id so we can reference this step + id: push-changes run: | TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}" + # Use the App Token to set the remote URL with authentication + git remote set-url origin "https://x-access-token:${{ steps.ci-token.outputs.token }}@github.com/${{ github.repository }}.git" + # Fetch the latest from remote git fetch origin $TARGET_BRANCH @@ -188,7 +198,7 @@ jobs: exit 0 fi - # If we get here, there are diffs, so push + # Push with the authenticated origin if ! git push origin $TARGET_BRANCH --force; then echo "Failed to push changes to $TARGET_BRANCH" exit 1 diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 7ae393bb5..68dd5617a 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -107,6 +107,7 @@ jobs: build_mac: name: build macOS + if: false # tmp disable due to brew install not working runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 diff --git a/RELEASES.md b/RELEASES.md index fabe635c7..302835140 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,11 @@ -Version 0.10.3 (2025-12-10) +Version 0.10.3 (2025-12-17) ======================== +* New driving model #36249 + * New temporal policy architecture + * New on-policy training physics noise model +* New driver monitoring model #36409 + * Trained on a new dataset, including comma four data +* Improved inter-process communication memory efficiency Version 0.10.2 (2025-11-19) ======================== diff --git a/cereal/log.capnp b/cereal/log.capnp index ea880fffb..6c1804dfa 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2528,13 +2528,10 @@ struct Event { controlsState @7 :ControlsState; selfdriveState @130 :SelfdriveState; gyroscope @99 :SensorEventData; - gyroscope2 @100 :SensorEventData; accelerometer @98 :SensorEventData; - accelerometer2 @101 :SensorEventData; magnetometer @95 :SensorEventData; lightSensor @96 :SensorEventData; temperatureSensor @97 :SensorEventData; - temperatureSensor2 @123 :SensorEventData; pandaStates @81 :List(PandaState); peripheralState @80 :PeripheralState; radarState @13 :RadarState; @@ -2697,5 +2694,8 @@ struct Event { liveLocationKalman @72 :LiveLocationKalman; liveTracksDEPRECATED @16 :List(LiveTracksDEPRECATED); onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED); + gyroscope2DEPRECATED @100 :SensorEventData; + accelerometer2DEPRECATED @101 :SensorEventData; + temperatureSensor2DEPRECATED @123 :SensorEventData; } } diff --git a/cereal/messaging/__init__.py b/cereal/messaging/__init__.py index b03285f80..0ad846f0f 100644 --- a/cereal/messaging/__init__.py +++ b/cereal/messaging/__init__.py @@ -2,7 +2,7 @@ from msgq.ipc_pyx import Context, Poller, SubSocket, PubSocket, SocketEventHandle, toggle_fake_events, \ set_fake_prefix, get_fake_prefix, delete_fake_prefix, wait_for_one_event from msgq.ipc_pyx import MultiplePublishersError, IpcError -from msgq import fake_event_handle, pub_sock, sub_sock, drain_sock_raw +from msgq import fake_event_handle, drain_sock_raw import msgq import os @@ -18,6 +18,20 @@ from openpilot.common.util import MovingAverage NO_TRAVERSAL_LIMIT = 2**64-1 +def pub_sock(endpoint: str) -> PubSocket: + service = SERVICE_LIST.get(endpoint) + segment_size = service.queue_size if service else 0 + return msgq.pub_sock(endpoint, segment_size) + + +def sub_sock(endpoint: str, poller: Optional[Poller] = None, addr: str = "127.0.0.1", + conflate: bool = False, timeout: Optional[int] = None) -> SubSocket: + service = SERVICE_LIST.get(endpoint) + segment_size = service.queue_size if service else 0 + return msgq.sub_sock(endpoint, poller=poller, addr=addr, conflate=conflate, + timeout=timeout, segment_size=segment_size) + + def reset_context(): msgq.context = Context() diff --git a/cereal/messaging/socketmaster.cc b/cereal/messaging/socketmaster.cc index 7f7e2795c..dfeeb807e 100644 --- a/cereal/messaging/socketmaster.cc +++ b/cereal/messaging/socketmaster.cc @@ -50,7 +50,7 @@ SubMaster::SubMaster(const std::vector &service_list, const std::v assert(services.count(std::string(name)) > 0); service serv = services.at(std::string(name)); - SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true); + SubSocket *socket = SubSocket::create(message_context.context(), name, address ? address : "127.0.0.1", true, true, serv.queue_size); assert(socket != 0); bool is_polled = inList(poll, name) || poll.empty(); if (is_polled) poller_->registerSocket(socket); @@ -187,7 +187,8 @@ SubMaster::~SubMaster() { PubMaster::PubMaster(const std::vector &service_list) { for (auto name : service_list) { assert(services.count(name) > 0); - PubSocket *socket = PubSocket::create(message_context.context(), name); + service serv = services.at(std::string(name)); + PubSocket *socket = PubSocket::create(message_context.context(), name, true, serv.queue_size); assert(socket); sockets_[name] = socket; } diff --git a/cereal/services.py b/cereal/services.py index 67548bc79..a2fd5f7dc 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -1,37 +1,44 @@ #!/usr/bin/env python3 +from enum import IntEnum from typing import Optional +# TODO: this should be automatically determined using the capnp schema +class QueueSize(IntEnum): + BIG = 10 * 1024 * 1024 # 10MB - video frames, large AI outputs + MEDIUM = 2 * 1024 * 1024 # 2MB - high freq (CAN), livestream + SMALL = 250 * 1024 # 250KB - most services + + class Service: - def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None): + def __init__(self, should_log: bool, frequency: float, decimation: Optional[int] = None, + queue_size: QueueSize = QueueSize.SMALL): self.should_log = should_log self.frequency = frequency self.decimation = decimation + self.queue_size = queue_size _services: dict[str, tuple] = { # service: (should_log, frequency, qlog decimation (optional)) # note: the "EncodeIdx" packets will still be in the log "gyroscope": (True, 104., 104), - "gyroscope2": (True, 100., 100), "accelerometer": (True, 104., 104), - "accelerometer2": (True, 100., 100), "magnetometer": (True, 25.), "lightSensor": (True, 100., 100), "temperatureSensor": (True, 2., 200), - "temperatureSensor2": (True, 2., 200), "gpsNMEA": (True, 9.), "deviceState": (True, 2., 1), "touch": (True, 20., 1), - "can": (True, 100., 2053), # decimation gives ~3 msgs in a full segment - "controlsState": (True, 100., 10), + "can": (True, 100., 2053, QueueSize.BIG), # decimation gives ~3 msgs in a full segment + "controlsState": (True, 100., 10, QueueSize.MEDIUM), "selfdriveState": (True, 100., 10), "pandaStates": (True, 10., 1), "peripheralState": (True, 2., 1), "radarState": (True, 20., 5), "roadEncodeIdx": (False, 20., 1), "liveTracks": (True, 20.), - "sendcan": (True, 100., 139), + "sendcan": (True, 100., 139, QueueSize.MEDIUM), "logMessage": (True, 0.), "errorLogMessage": (True, 0., 1), "liveCalibration": (True, 4., 4), @@ -43,7 +50,7 @@ _services: dict[str, tuple] = { "carOutput": (True, 100., 10), "longitudinalPlan": (True, 20., 10), "driverAssistance": (True, 20., 20), - "procLog": (True, 0.5, 15), + "procLog": (True, 0.5, 15, QueueSize.BIG), "gpsLocationExternal": (True, 10., 10), "gpsLocation": (True, 1., 1), "ubloxGnss": (True, 10.), @@ -65,7 +72,7 @@ _services: dict[str, tuple] = { "wideRoadEncodeIdx": (False, 20., 1), "wideRoadCameraState": (True, 20., 20), "drivingModelData": (True, 20., 10), - "modelV2": (True, 20.), + "modelV2": (True, 20., None, QueueSize.BIG), "managerState": (True, 2., 1), "uploaderState": (True, 0., 1), "navInstruction": (True, 1., 10), @@ -77,10 +84,14 @@ _services: dict[str, tuple] = { "rawAudioData": (False, 20.), "bookmarkButton": (True, 0., 1), "audioFeedback": (True, 0., 1), + "roadEncodeData": (False, 20., None, QueueSize.BIG), + "driverEncodeData": (False, 20., None, QueueSize.BIG), + "wideRoadEncodeData": (False, 20., None, QueueSize.BIG), + "qRoadEncodeData": (False, 20., None, QueueSize.BIG), # sunnypilot - "modelManagerSP": (False, 1., 1), - "backupManagerSP": (False, 1., 1), + "modelManagerSP": (False, 1., 1, QueueSize.BIG), + "backupManagerSP": (False, 1., 1, QueueSize.BIG), "selfdriveStateSP": (True, 100., 10), "longitudinalPlanSP": (True, 20., 10), "onroadEventsSP": (True, 1., 1), @@ -88,23 +99,19 @@ _services: dict[str, tuple] = { "carControlSP": (True, 100., 10), "carStateSP": (True, 100., 10), "liveMapDataSP": (True, 1., 1), - "modelDataV2SP": (True, 20.), + "modelDataV2SP": (True, 20., None, QueueSize.BIG), "liveLocationKalman": (True, 20.), # debug "uiDebug": (True, 0., 1), "testJoystick": (True, 0.), "alertDebug": (True, 20., 5), - "roadEncodeData": (False, 20.), - "driverEncodeData": (False, 20.), - "wideRoadEncodeData": (False, 20.), - "qRoadEncodeData": (False, 20.), "livestreamWideRoadEncodeIdx": (False, 20.), "livestreamRoadEncodeIdx": (False, 20.), "livestreamDriverEncodeIdx": (False, 20.), - "livestreamWideRoadEncodeData": (False, 20.), - "livestreamRoadEncodeData": (False, 20.), - "livestreamDriverEncodeData": (False, 20.), + "livestreamWideRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamRoadEncodeData": (False, 20., None, QueueSize.MEDIUM), + "livestreamDriverEncodeData": (False, 20., None, QueueSize.MEDIUM), "customReservedRawData0": (True, 0.), "customReservedRawData1": (True, 0.), "customReservedRawData2": (True, 0.), @@ -122,13 +129,13 @@ def build_header(): h += "#include \n" h += "#include \n" - h += "struct service { std::string name; bool should_log; float frequency; int decimation; };\n" + h += "struct service { std::string name; bool should_log; float frequency; int decimation; size_t queue_size; };\n" h += "static std::map services = {\n" for k, v in SERVICE_LIST.items(): should_log = "true" if v.should_log else "false" decimation = -1 if v.decimation is None else v.decimation - h += ' { "%s", {"%s", %s, %f, %d}},\n' % \ - (k, k, should_log, v.frequency, decimation) + h += ' { "%s", {"%s", %s, %f, %d, %d}},\n' % \ + (k, k, should_log, v.frequency, decimation, v.queue_size) h += "};\n" h += "#endif\n" diff --git a/common/params_keys.h b/common/params_keys.h index 9df7731eb..1ec3ddaaf 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -151,6 +151,7 @@ inline static std::unordered_map keys = { {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, + {"CarList", {PERSISTENT, JSON}}, {"CarParamsSP", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, {"CarParamsSPCache", {CLEAR_ON_MANAGER_START, BYTES}}, {"CarParamsSPPersistent", {PERSISTENT, BYTES}}, diff --git a/common/prefix.py b/common/prefix.py index 207f8477d..b19ce1472 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -11,7 +11,7 @@ from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) - self.msgq_path = os.path.join(Paths.shm_path(), self.prefix) + self.msgq_path = os.path.join(Paths.shm_path(), "msgq_" + self.prefix) self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache diff --git a/docs/CARS.md b/docs/CARS.md index b36c11aef..5c659d284 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -14,12 +14,12 @@ A supported vehicle is one that just works when you install a comma device. All |Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Acura|TLX 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| |Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here
||| @@ -34,7 +34,7 @@ A supported vehicle is one that just works when you install a comma device. All |Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| -|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -48,8 +48,8 @@ A supported vehicle is one that just works when you install a comma device. All |Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Focus 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Ford|Focus Hybrid 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| @@ -82,7 +82,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| @@ -202,171 +202,170 @@ A supported vehicle is one that just works when you install a comma device. All |Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|ES 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|ES Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|NX Hybrid 2018-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|RC 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX 2016|Lexus Safety System+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX 2017-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX Hybrid 2016|Lexus Safety System+|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Lexus|RX Hybrid 2017-19|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Nissan[6](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[6](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[6](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Nissan[6](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Nissan[5](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 USB-C coupler
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Subaru|Ascent 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2017-18|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2019-21|All[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2015-18|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2015-17|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2018-19|EyeSight Driver Assistance[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2020-22|All[7](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2018-19|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2020-21|EyeSight Driver Assistance[7](#footnotes)|openpilot available[1,8](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Škoda|Fabia 2022-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Škoda|Kamiq 2021-23[12,14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Škoda|Karoq 2019-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Kodiaq 2017-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia 2015-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia RS 2016[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Octavia Scout 2017-19[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Škoda|Scala 2020-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Škoda|Superb 2015-22[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[10](#footnotes)|Model 3 (with HW3) 2019-23[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[10](#footnotes)|Model 3 (with HW4) 2024-25[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[10](#footnotes)|Model Y (with HW3) 2020-23[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Tesla[10](#footnotes)|Model Y (with HW4) 2024-25[9](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Subaru|Ascent 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2017-18|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2015-18|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2020-22|All[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2015-17|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2018-19|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2020-22|All[6](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2020-21|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Škoda|Fabia 2022-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Kamiq 2021-23[11,13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Karoq 2019-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Kodiaq 2017-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia 2015-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia RS 2016[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Octavia Scout 2017-19[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Škoda|Scala 2020-23[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Škoda|Superb 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW3) 2019-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model 3 (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW3) 2020-23[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Tesla[9](#footnotes)|Model Y (with HW4) 2024-25[8](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Avalon 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2017-18|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Avalon Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Avalon Hybrid 2019-21|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Camry 2018-20|All|Stock|0 mph[11](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Camry 2021-24|All|openpilot|0 mph[11](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry 2018-20|All|Stock|0 mph[10](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Camry 2021-24|All|openpilot|0 mph[10](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Corolla 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Highlander 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Highlander Hybrid 2017-19|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Prius 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|RAV4 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| |Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| -|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[16](#footnotes)||| -|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| -|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here
||| +|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat 2015-22[12](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
[15](#footnotes)||| +|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| +|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,14](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 OBD-C cable (2 ft)
- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here
||| ### Footnotes 1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`.
-2By default, this car will use the stock Adaptive Cruise Control (ACC) for longitudinal control. If the Driver Support Unit (DSU) is disconnected, openpilot ACC will replace stock ACC. NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).
-3Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia.
-4See more setup details for GM.
-52019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
-6See more setup details for Nissan.
-7In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
-8Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
-9Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
-10See more setup details for Tesla.
-11openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
-12Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
-13Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
-14Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
-15Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
-16Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
+2Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia.
+3See more setup details for GM.
+42019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph.
+5See more setup details for Nissan.
+6In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance.
+7Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB.
+8Some 2023 model years have HW4. To check which hardware type your vehicle has, look for Autopilot computer under Software -> Additional Vehicle Information on your vehicle's touchscreen. See this page for more information.
+9See more setup details for Tesla.
+10openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control.
+11Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform.
+12Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets.
+13Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality.
+14Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC.
+15Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store.
## Community Maintained Cars Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/). diff --git a/msgq_repo b/msgq_repo index a16cf1f60..6abe47bc9 160000 --- a/msgq_repo +++ b/msgq_repo @@ -1 +1 @@ -Subproject commit a16cf1f608538d14f66bd6142230d8728f2d0abc +Subproject commit 6abe47bc98b83338b6ea04a87a6b2b5c65d09630 diff --git a/opendbc_repo b/opendbc_repo index 836dd2973..028c2d8a6 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 836dd29732bd2f3d57785ff2686a92912a8ded58 +Subproject commit 028c2d8a6783cf7b4d9d5d058f67ff61d0f6bd17 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png index 4d83ed5cd..04ffc2435 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_background.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f27352a18194a1c819e9eaea89cfc11d2964402df0a28efa3ba60ae2d972fe67 -size 13108 +oid sha256:b7eb870d01e5bf6c421e204026a4ea08e177731f2d6b5b17c4ad43c90c1c3e78 +size 23549 diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png index 7aa7f0542..540b2029a 100644 --- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png +++ b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_person.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25d66e42a28a3367eb40724d28652889089aa762438b475645269e0319c46009 -size 1431 +oid sha256:f7b3bb76ee2359076339285ea6bced5b680e5b919a1b7dee163f36cd819c9ea1 +size 1746 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png new file mode 100644 index 000000000..92993e3e0 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b7dce550c008ff7a65ed19ccf308ecf92cd0118bb544978b7dd7393c5c27ae5 +size 809 diff --git a/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png new file mode 100644 index 000000000..53a837afb --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e102b8b2e71a25d9f818b37d6f75ed958430cb765a07ae50713995779fb6a886 +size 1388 diff --git a/selfdrive/assets/icons_mici/setup/orange_dm.png b/selfdrive/assets/icons_mici/setup/orange_dm.png new file mode 100644 index 000000000..74cce9d97 --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/orange_dm.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38a108f96f85a154b698693b07f2e4214124b8f2545b7c4490cea0aa998d75fd +size 11855 diff --git a/selfdrive/assets/icons_mici/setup/small_button_disabled.png b/selfdrive/assets/icons_mici/setup/small_button_disabled.png new file mode 100644 index 000000000..da8bb3eef --- /dev/null +++ b/selfdrive/assets/icons_mici/setup/small_button_disabled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ed258d8e0531c19705953ded065c6d5e14929728a2909d8d4e335898fa5d080 +size 4056 diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 71d057694..c0ae29916 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -90,7 +90,8 @@ class CarSpecificEvents: events = self.create_common_events(CS, CS_prev, extra_gears=extra_gears) if self.CP.openpilotLongitudinalControl: - if CS.cruiseState.standstill and not CS.brakePressed: + # Only can leave standstill when planner wants to move + if CS.cruiseState.standstill and not CS.brakePressed and CC.cruiseControl.resume: events.add(EventName.resumeRequired) if CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 48d174bfa..47d105f99 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -22,17 +22,15 @@ from openpilot.sunnypilot.selfdrive.controls.lib.latcontrol_torque_ext import La # Additionally, there is friction in the steering wheel that needs # to be overcome to move it at all, this is compensated for too. -KP = 0.8 -KI = 0.15 - +KP = 1.0 +KI = 0.3 +KD = 0.0 INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30] KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP] LP_FILTER_CUTOFF_HZ = 1.2 -JERK_LOOKAHEAD_SECONDS = 0.19 -JERK_GAIN = 0.3 LAT_ACCEL_REQUEST_BUFFER_SECONDS = 1.0 -VERSION = 1 +VERSION = 0 class LatControlTorque(LatControl): def __init__(self, CP, CP_SP, CI, dt): @@ -40,13 +38,13 @@ class LatControlTorque(LatControl): self.torque_params = CP.lateralTuning.torque.as_builder() self.torque_from_lateral_accel = CI.torque_from_lateral_accel() self.lateral_accel_from_torque = CI.lateral_accel_from_torque() - self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, rate=1/self.dt) + self.pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI, KD, rate=1/self.dt) self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg self.lat_accel_request_buffer_len = int(LAT_ACCEL_REQUEST_BUFFER_SECONDS / self.dt) self.lat_accel_request_buffer = deque([0.] * self.lat_accel_request_buffer_len , maxlen=self.lat_accel_request_buffer_len) - self.lookahead_frames = int(JERK_LOOKAHEAD_SECONDS / self.dt) - self.jerk_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) + self.previous_measurement = 0.0 + self.measurement_rate_filter = FirstOrderFilter(0.0, 1 / (2 * np.pi * LP_FILTER_CUTOFF_HZ), self.dt) self.extension = LatControlTorqueExt(self, CP, CP_SP, CI) @@ -78,15 +76,17 @@ class LatControlTorque(LatControl): delay_frames = int(np.clip(lat_delay / self.dt, 1, self.lat_accel_request_buffer_len)) expected_lateral_accel = self.lat_accel_request_buffer[-delay_frames] - lookahead_idx = int(np.clip(-delay_frames + self.lookahead_frames, -self.lat_accel_request_buffer_len+1, -2)) - raw_lateral_jerk = (self.lat_accel_request_buffer[lookahead_idx+1] - self.lat_accel_request_buffer[lookahead_idx-1]) / (2 * self.dt) - desired_lateral_jerk = self.jerk_filter.update(raw_lateral_jerk) + # TODO factor out lateral jerk from error to later replace it with delay independent alternative future_desired_lateral_accel = desired_curvature * CS.vEgo ** 2 self.lat_accel_request_buffer.append(future_desired_lateral_accel) gravity_adjusted_future_lateral_accel = future_desired_lateral_accel - roll_compensation - setpoint = expected_lateral_accel + desired_lateral_jerk = (future_desired_lateral_accel - expected_lateral_accel) / lat_delay measurement = measured_curvature * CS.vEgo ** 2 + measurement_rate = self.measurement_rate_filter.update((measurement - self.previous_measurement) / self.dt) + self.previous_measurement = measurement + + setpoint = lat_delay * desired_lateral_jerk + expected_lateral_accel error = setpoint - measurement # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly @@ -94,10 +94,15 @@ class LatControlTorque(LatControl): ff = gravity_adjusted_future_lateral_accel # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll ff -= self.torque_params.latAccelOffset - ff += get_friction(error + JERK_GAIN * desired_lateral_jerk, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + # TODO jerk is weighted by lat_delay for legacy reasons, but should be made independent of it + ff += get_friction(error, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 - output_lataccel = self.pid.update(pid_log.error, speed=CS.vEgo, feedforward=ff, freeze_integrator=freeze_integrator) + output_lataccel = self.pid.update(pid_log.error, + -measurement_rate, + feedforward=ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) # Lateral acceleration torque controller extension updates diff --git a/selfdrive/debug/analyze-msg-size.py b/selfdrive/debug/analyze-msg-size.py new file mode 100755 index 000000000..69015a6be --- /dev/null +++ b/selfdrive/debug/analyze-msg-size.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +import argparse +from tqdm import tqdm + +from cereal.services import SERVICE_LIST, QueueSize +from openpilot.tools.lib.logreader import LogReader + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Analyze message sizes from a log route") + parser.add_argument("route", nargs="?", default="98395b7c5b27882e/000000a8--f87e7cd255", + help="Log route to analyze (default: 98395b7c5b27882e/000000a8--f87e7cd255)") + args = parser.parse_args() + + lr = LogReader(args.route) + + szs = {} + for msg in tqdm(lr): + sz = len(msg.as_builder().to_bytes()) + msg_type = msg.which() + if msg_type not in szs: + szs[msg_type] = {'min': sz, 'max': sz, 'sum': sz, 'count': 1} + else: + szs[msg_type]['min'] = min(szs[msg_type]['min'], sz) + szs[msg_type]['max'] = max(szs[msg_type]['max'], sz) + szs[msg_type]['sum'] += sz + szs[msg_type]['count'] += 1 + + print() + print(f"{'Service':<36} {'Min (KB)':>12} {'Max (KB)':>12} {'Avg (KB)':>12} {'KB/min':>12} {'KB/sec':>12} {'Minutes in 10MB':>18} {'Seconds in Queue':>18}") + print("-" * 132) + def sort_key(x): + k, v = x + avg = v['sum'] / v['count'] + freq = SERVICE_LIST.get(k, None) + freq_val = freq.frequency if freq else 0.0 + kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 + return kb_per_min + total_kb_per_min = 0.0 + RINGBUFFER_SIZE_KB = 10 * 1024 # 10MB old default + for k, v in sorted(szs.items(), key=sort_key, reverse=True): + avg = v['sum'] / v['count'] + service = SERVICE_LIST.get(k, None) + freq_val = service.frequency if service else 0.0 + queue_size_kb = (service.queue_size / 1024) if service else 250 # default to SMALL + kb_per_min = (avg * freq_val * 60) / 1024 if freq_val > 0 else 0.0 + kb_per_sec = kb_per_min / 60 + minutes_in_buffer = RINGBUFFER_SIZE_KB / kb_per_min if kb_per_min > 0 else float('inf') + seconds_in_queue = (queue_size_kb / kb_per_sec) if kb_per_sec > 0 else float('inf') + total_kb_per_min += kb_per_min + min_str = f"{minutes_in_buffer:.2f}" if minutes_in_buffer != float('inf') else "inf" + sec_queue_str = f"{seconds_in_queue:.2f}" if seconds_in_queue != float('inf') else "inf" + print(f"{k:<36} {v['min']/1024:>12.2f} {v['max']/1024:>12.2f} {avg/1024:>12.2f} {kb_per_min:>12.2f} {kb_per_sec:>12.2f} {min_str:>18} {sec_queue_str:>18}") + + # Summary section + print() + print(f"Total usage: {total_kb_per_min / 1024:.2f} MB/min") + + # Calculate memory usage: old (10MB for all) vs new (from services.py) + OLD_SIZE = 10 * 1024 * 1024 # 10MB was the old default + old_total = len(SERVICE_LIST) * OLD_SIZE + + new_total = sum(s.queue_size for s in SERVICE_LIST.values()) + + # Count by queue size + size_counts = {QueueSize.BIG: 0, QueueSize.MEDIUM: 0, QueueSize.SMALL: 0} + for s in SERVICE_LIST.values(): + size_counts[s.queue_size] += 1 + + savings_pct = (1 - new_total / old_total) * 100 + + print() + print(f"{'Queue Size Comparison':<40}") + print("-" * 60) + print(f"{'Old (10MB default):':<30} {old_total / 1024 / 1024:>10.2f} MB") + print(f"{'New (from services.py):':<30} {new_total / 1024 / 1024:>10.2f} MB") + print(f"{'Savings:':<30} {savings_pct:>10.1f}%") + print() + print(f"{'Breakdown:':<30}") + print(f" BIG (10MB): {size_counts[QueueSize.BIG]:>3} services") + print(f" MEDIUM (2MB): {size_counts[QueueSize.MEDIUM]:>3} services") + print(f" SMALL (250KB): {size_counts[QueueSize.SMALL]:>3} services") diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index f64f4cbc1..5d0ce4ecf 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -11,6 +11,7 @@ #include "cereal/gen/cpp/car.capnp.h" #include "cereal/messaging/messaging.h" +#include "cereal/services.h" #include "common/ratekeeper.h" #include "common/swaglog.h" #include "common/timing.h" @@ -103,7 +104,7 @@ void can_send_thread(std::vector pandas, bool fake_send) { AlignedBuffer aligned_buf; std::unique_ptr context(Context::create()); - std::unique_ptr subscriber(SubSocket::create(context.get(), "sendcan")); + std::unique_ptr subscriber(SubSocket::create(context.get(), "sendcan", "127.0.0.1", false, true, services.at("sendcan").queue_size)); assert(subscriber != NULL); subscriber->setTimeout(100); diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 3ba11c0b7..c049e15bd 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -87,15 +87,6 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) -def steer_saturated_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - steer_text2 = "Steer Left" if sm['carControl'].actuators.torque > 0 else "Steer Right" - return Alert( - "Take Control", - steer_text2, - AlertStatus.userPrompt, AlertSize.mid, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.) - - def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: first_word = 'Recalibrating' if sm['liveCalibration'].calStatus == log.LiveCalibrationData.Status.recalibrating else 'Calibrating' return Alert( @@ -917,7 +908,11 @@ if HARDWARE.get_device_type() == 'mici': Priority.LOW, VisualAlert.none, AudibleAlert.prompt, .1), }, EventName.steerSaturated: { - ET.WARNING: steer_saturated_alert, + ET.WARNING: Alert( + "take control", + "turn exceeds limit", + AlertStatus.userPrompt, AlertSize.mid, + Priority.LOW, VisualAlert.steerRequired, AudibleAlert.promptRepeat, 2.), }, EventName.calibrationIncomplete: { ET.PERMANENT: calibration_incomplete_alert, diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 4a58e321f..cdd4301fc 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -e0ad86508edb61b3eaa1b84662c515d2c3368295 \ No newline at end of file +b508f43fb0481bce0859c9b6ab4f45ee690b8dab \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index e49a8b0f8..7bf8fe46d 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -46,7 +46,8 @@ segments = [ ("HYUNDAI", "regenAA0FC4ED71E|2025-04-08--22-57-50--0"), ("HYUNDAI2", "regenAFB9780D823|2025-04-08--23-00-34--0"), ("TOYOTA", "regen218A4DCFAA1|2025-04-08--22-57-51--0"), - ("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"), + # TODO: get new RAV4 route without enableDsu + # ("TOYOTA2", "regen107352E20EB|2025-04-08--22-57-46--0"), ("TOYOTA3", "regen1455E3B4BDF|2025-04-09--03-26-06--0"), ("HONDA", "regenB328FF8BA0A|2025-04-08--22-57-45--0"), ("HONDA2", "regen6170C8C9A35|2025-04-08--22-57-46--0"), diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index b9506d080..f57751c06 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -121,6 +121,7 @@ class TestOnroad: params.put_bool("RecordFront", True) set_params_enabled() os.environ['REPLAY'] = '1' + os.environ['MSGQ_PREALLOC'] = '1' os.environ['TESTING_CLOSET'] = '1' if os.path.exists(Paths.log_root()): shutil.rmtree(Paths.log_root()) @@ -283,11 +284,12 @@ class TestOnroad: print("------------------------------------------------") offset = int(SERVICE_LIST['deviceState'].frequency * LOG_OFFSET) mems = [m.deviceState.memoryUsagePercent for m in self.msgs['deviceState'][offset:]] - print("Memory usage: ", mems) + print("Overall memory usage: ", mems) + print("MSGQ (/dev/shm/) usage: ", subprocess.check_output(["du", "-hs", "/dev/shm"]).split()[0].decode()) # check for big leaks. note that memory usage is # expected to go up while the MSGQ buffers fill up - assert np.average(mems) <= 85, "Average memory usage above 85%" + assert np.average(mems) <= 80, "Average memory usage too high" assert np.max(np.diff(mems)) <= 4, "Max memory increase too high" assert np.average(np.diff(mems)) <= 1, "Average memory increase too high" diff --git a/selfdrive/ui/mici/layouts/onboarding.py b/selfdrive/ui/mici/layouts/onboarding.py index 38dd70cea..b175a3fd2 100644 --- a/selfdrive/ui/mici/layouts/onboarding.py +++ b/selfdrive/ui/mici/layouts/onboarding.py @@ -1,11 +1,14 @@ from enum import IntEnum -from collections.abc import Callable import weakref +import math +import numpy as np import pyray as rl +from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import FontWeight, gui_app from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.button import SmallButton +from openpilot.system.ui.widgets.button import SmallButton, SmallCircleIconButton from openpilot.system.ui.widgets.label import UnifiedLabel from openpilot.system.ui.widgets.slider import SmallSlider from openpilot.system.ui.mici_setup import TermsHeader, TermsPage as SetupTermsPage @@ -24,11 +27,12 @@ class OnboardingState(IntEnum): class DriverCameraSetupDialog(DriverCameraDialog): - def __init__(self, confirm_callback: Callable): + def __init__(self): super().__init__(no_escape=True) - self.driver_state_renderer = DriverStateRenderer(confirm_mode=True, confirm_callback=confirm_callback) - self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 200, 200)) + self.driver_state_renderer = DriverStateRenderer(inset=True) + self.driver_state_renderer.set_rect(rl.Rectangle(0, 0, 120, 120)) self.driver_state_renderer.load_icons() + self.driver_state_renderer.set_force_active(True) def _render(self, rect): rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) @@ -41,15 +45,15 @@ class DriverCameraSetupDialog(DriverCameraDialog): return -1 # Position dmoji on opposite side from driver - # TODO: we don't have design for RHD yet - is_rhd = False - driver_state_rect = ( - rect.x if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, - rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, + is_rhd = self.driver_state_renderer.is_rhd + self.driver_state_renderer.set_position( + rect.x + 8 if is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width - 8, + rect.y + 8, ) - self.driver_state_renderer.set_position(*driver_state_rect) self.driver_state_renderer.render() + self._draw_face_detection(rect) + rl.end_scissor_mode() return -1 @@ -88,18 +92,54 @@ class TrainingGuidePreDMTutorial(SetupTermsPage): )) +class DMBadFaceDetected(SetupTermsPage): + def __init__(self, continue_callback, back_callback): + super().__init__(continue_callback, back_callback, continue_text="power off") + self._title_header = TermsHeader("make sure comma four can see your face", gui_app.texture("icons_mici/setup/orange_dm.png", 60, 60)) + self._dm_label = UnifiedLabel("Re-mount if your face is occluded or driver monitoring has difficulty tracking your face.", 42, FontWeight.ROMAN) + + @property + def _content_height(self): + return self._dm_label.rect.y + self._dm_label.rect.height - self._scroll_panel.get_offset() + + def _render_content(self, scroll_offset): + self._title_header.render(rl.Rectangle( + self._rect.x + 16, + self._rect.y + 16 + scroll_offset, + self._title_header.rect.width, + self._title_header.rect.height, + )) + + self._dm_label.render(rl.Rectangle( + self._rect.x + 16, + self._title_header.rect.y + self._title_header.rect.height + 16, + self._rect.width - 32, + self._dm_label.get_content_height(int(self._rect.width - 32)), + )) + + class TrainingGuideDMTutorial(Widget): + PROGRESS_DURATION = 4 + LOOKING_THRESHOLD_DEG = 30.0 + def __init__(self, continue_callback): super().__init__() - self._title_header = TermsHeader("fill the circle to continue", gui_app.texture("icons_mici/setup/green_dm.png", 60, 60)) + self._back_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_question.png", 48, 48)) + self._back_button.set_click_callback(self._show_bad_face_page) + self._good_button = SmallCircleIconButton(gui_app.texture("icons_mici/setup/driver_monitoring/dm_check.png", 48, 35)) # Wrap the continue callback to restore settings def wrapped_continue_callback(): device.set_offroad_brightness(None) - device.reset_interactive_timeout() continue_callback() - self._dialog = DriverCameraSetupDialog(wrapped_continue_callback) + self._good_button.set_click_callback(wrapped_continue_callback) + self._good_button.set_enabled(False) + + self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps) + self._dialog = DriverCameraSetupDialog() + self._bad_face_page = DMBadFaceDetected(HARDWARE.shutdown, self._hide_bad_face_page) + self._should_show_bad_face_page = False # Disable driver monitoring model when device times out for inactivity def inactivity_callback(): @@ -107,31 +147,113 @@ class TrainingGuideDMTutorial(Widget): device.add_interactive_timeout_callback(inactivity_callback) + def _show_bad_face_page(self): + self._bad_face_page.show_event() + self.hide_event() + self._should_show_bad_face_page = True + + def _hide_bad_face_page(self): + self._bad_face_page.hide_event() + self.show_event() + self._should_show_bad_face_page = False + def show_event(self): super().show_event() self._dialog.show_event() + self._progress.x = 0.0 device.set_offroad_brightness(100) - device.reset_interactive_timeout(300) # 5 minutes def _update_state(self): super()._update_state() if device.awake: ui_state.params.put_bool("IsDriverViewEnabled", True) + sm = ui_state.sm + if sm.recv_frame.get("driverMonitoringState", 0) == 0: + return + + dm_state = sm["driverMonitoringState"] + driver_data = self._dialog.driver_state_renderer.get_driver_data() + + if len(driver_data.faceOrientation) == 3: + pitch, yaw, _ = driver_data.faceOrientation + looking_center = abs(math.degrees(pitch)) < self.LOOKING_THRESHOLD_DEG and abs(math.degrees(yaw)) < self.LOOKING_THRESHOLD_DEG + else: + looking_center = False + + # stay at 100% once reached + if (dm_state.faceDetected and looking_center) or self._progress.x > 0.99: + slow = self._progress.x < 0.25 + duration = self.PROGRESS_DURATION * 2 if slow else self.PROGRESS_DURATION + self._progress.x += 1.0 / (duration * gui_app.target_fps) + self._progress.x = min(1.0, self._progress.x) + else: + self._progress.update(0.0) + + self._good_button.set_enabled(self._progress.x >= 0.999) + def _render(self, _): + if self._should_show_bad_face_page: + return self._bad_face_page.render(self._rect) + self._dialog.render(self._rect) - rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - self._title_header.rect.height * 1.5 - 32), - int(self._rect.width), int(self._title_header.rect.height * 1.5 + 32), - rl.BLANK, rl.Color(0, 0, 0, 150)) - self._title_header.render(rl.Rectangle( - self._rect.x + 16, - self._rect.y + self._rect.height - self._title_header.rect.height - 16, - self._title_header.rect.width, - self._title_header.rect.height, + rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 80), + int(self._rect.width), 80, rl.BLANK, rl.BLACK) + + # draw white ring around dm icon to indicate progress + ring_thickness = 8 + + # DM icon is 120x120, positioned on opposite side from driver + dm_size = 120 + is_rhd = self._dialog.driver_state_renderer._is_rhd + dm_center_x = (self._rect.x + dm_size / 2 + 8) if is_rhd else (self._rect.x + self._rect.width - dm_size / 2 - 8) + dm_center_y = self._rect.y + dm_size / 2 + 8 + icon_edge_radius = dm_size / 2 + outer_radius = icon_edge_radius + 1 # 2px outward from icon edge + inner_radius = outer_radius - ring_thickness # Inset by ring_thickness + start_angle = 90.0 # Start from bottom + end_angle = start_angle + self._progress.x * 360.0 # Clockwise + + # Fade in alpha + current_angle = end_angle - start_angle + alpha = int(np.interp(current_angle, [0.0, 45.0], [0, 255])) + + # White to green + color_t = np.clip(np.interp(current_angle, [45.0, 360.0], [0.0, 1.0]), 0.0, 1.0) + r = int(np.interp(color_t, [0.0, 1.0], [255, 0])) + g = int(np.interp(color_t, [0.0, 1.0], [255, 255])) + b = int(np.interp(color_t, [0.0, 1.0], [255, 64])) + ring_color = rl.Color(r, g, b, alpha) + + rl.draw_ring( + rl.Vector2(dm_center_x, dm_center_y), + inner_radius, + outer_radius, + start_angle, + end_angle, + 36, + ring_color, + ) + + self._back_button.render(rl.Rectangle( + self._rect.x + 8, + self._rect.y + self._rect.height - self._back_button.rect.height, + self._back_button.rect.width, + self._back_button.rect.height, )) + self._good_button.render(rl.Rectangle( + self._rect.x + self._rect.width - self._good_button.rect.width - 8, + self._rect.y + self._rect.height - self._good_button.rect.height, + self._good_button.rect.width, + self._good_button.rect.height, + )) + + # rounded border + rl.draw_rectangle_rounded_lines_ex(self._rect, 0.2 * 1.02, 10, 50, rl.BLACK) + class TrainingGuideRecordFront(SetupTermsPage): def __init__(self, continue_callback): @@ -223,6 +345,14 @@ class TrainingGuide(Widget): TrainingGuideRecordFront(continue_callback=on_continue), ] + def show_event(self): + super().show_event() + device.set_override_interactive_timeout(300) + + def hide_event(self): + super().hide_event() + device.set_override_interactive_timeout(None) + def _advance_step(self): if self._step < len(self._steps) - 1: self._step += 1 @@ -319,6 +449,14 @@ class OnboardingWindow(Widget): self._training_guide = TrainingGuide(completed_callback=self._on_completed_training) self._decline_page = DeclinePage(back_callback=self._on_decline_back) + def show_event(self): + super().show_event() + device.set_override_interactive_timeout(300) + + def hide_event(self): + super().hide_event() + device.set_override_interactive_timeout(None) + @property def completed(self) -> bool: return self._accepted_terms and self._training_done diff --git a/selfdrive/ui/mici/layouts/settings/network/__init__.py b/selfdrive/ui/mici/layouts/settings/network/__init__.py index d085fdf55..1faf49311 100644 --- a/selfdrive/ui/mici/layouts/settings/network/__init__.py +++ b/selfdrive/ui/mici/layouts/settings/network/__init__.py @@ -171,6 +171,8 @@ class NetworkLayoutMici(NavWidget): }.get(self._wifi_manager.current_network_metered, 'default')) def _switch_to_panel(self, panel_type: NetworkPanelType): + if panel_type == NetworkPanelType.WIFI: + self._wifi_ui.show_event() self._current_panel = panel_type def _render(self, rect: rl.Rectangle): diff --git a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py index 18c4dd5d6..374539c4c 100644 --- a/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py +++ b/selfdrive/ui/mici/layouts/settings/network/wifi_ui.py @@ -83,8 +83,6 @@ class WifiIcon(Widget): class WifiItem(BigDialogOptionButton): LEFT_MARGIN = 20 - HEIGHT = 54 - SELECTED_HEIGHT = 74 def __init__(self, network: Network): super().__init__(network.ssid) @@ -97,10 +95,6 @@ class WifiItem(BigDialogOptionButton): self._wifi_icon = WifiIcon() self._wifi_icon.set_current_network(network) - def set_selected(self, selected: bool): - super().set_selected(selected) - self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT - def set_current_network(self, network: Network): self._network = network self._wifi_icon.set_current_network(network) @@ -120,11 +114,11 @@ class WifiItem(BigDialogOptionButton): )) if self._selected: - self._label.set_font_size(74) + self._label.set_font_size(self.SELECTED_HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._label.set_font_weight(FontWeight.DISPLAY) else: - self._label.set_font_size(54) + self._label.set_font_size(self.HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) @@ -322,6 +316,9 @@ class NetworkInfoPage(NavWidget): class WifiUIMici(BigMultiOptionDialog): + # Wait this long after user interacts with widget to update network list + INACTIVITY_TIMEOUT = 1 + def __init__(self, wifi_manager: WifiManager, back_callback: Callable): super().__init__([], None, None, right_btn_callback=None) @@ -330,7 +327,6 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page = NetworkInfoPage(wifi_manager, self._connect_to_network, self._forget_network, self._open_network_manage_page) self._network_info_page.set_connecting(lambda: self._connecting) - self._should_open_network_info_page = False # wait for scroll_to animation self._loading_animation = LoadingAnimation() @@ -338,6 +334,10 @@ class WifiUIMici(BigMultiOptionDialog): self._connecting: str | None = None self._networks: dict[str, Network] = {} + # widget state + self._last_interaction_time = -float('inf') + self._restore_selection = False + self._wifi_manager.add_callbacks( need_auth=self._on_need_auth, activated=self._on_activated, @@ -350,18 +350,12 @@ class WifiUIMici(BigMultiOptionDialog): # Call super to prepare scroller; selection scroll is handled dynamically super().show_event() self._wifi_manager.set_active(True) - self._scroller.show_event() + self._last_interaction_time = -float('inf') def hide_event(self): super().hide_event() self._wifi_manager.set_active(False) - def _update_state(self): - super()._update_state() - if self._should_open_network_info_page: - self._should_open_network_info_page = False - self._open_network_manage_page() - def _open_network_manage_page(self, result=None): self._network_info_page.update_networks(self._networks) gui_app.set_modal_overlay(self._network_info_page) @@ -380,6 +374,10 @@ class WifiUIMici(BigMultiOptionDialog): self._network_info_page.update_networks(self._networks) def _update_buttons(self): + # Don't update buttons while user is actively interacting + if rl.get_time() - self._last_interaction_time < self.INACTIVITY_TIMEOUT: + return + for network in self._networks.values(): # pop and re-insert to eliminate stuttering on update (prevents position lost for a frame) network_button_idx = next((i for i, btn in enumerate(self._scroller._items) if btn.option == network.ssid), None) @@ -390,23 +388,28 @@ class WifiUIMici(BigMultiOptionDialog): else: network_button = WifiItem(network) - def show_network_info_page(_network): - self._network_info_page.set_current_network(_network) - self._should_open_network_info_page = True - - network_button.set_click_callback(lambda _net=network, _button=network_button: _button._selected and show_network_info_page(_net)) - self.add_button(network_button) # remove networks no longer present self._scroller._items[:] = [btn for btn in self._scroller._items if btn.option in self._networks] + # try to restore previous selection to prevent jumping from adding/removing/reordering buttons + self._restore_selection = True + def _connect_with_password(self, ssid: str, password: str): if password: self._connecting = ssid self._wifi_manager.connect_to_network(ssid, password) self._update_buttons() + def _on_option_selected(self, option: str, smooth_scroll: bool = True): + super()._on_option_selected(option, smooth_scroll) + + # only open if button is already selected + if option in self._networks and option == self._selected_option: + self._network_info_page.set_current_network(self._networks[option]) + self._open_network_manage_page() + def _connect_to_network(self, ssid: str): network = self._networks.get(ssid) if network is None: @@ -440,7 +443,19 @@ class WifiUIMici(BigMultiOptionDialog): def _on_disconnected(self): self._connecting = None + def _update_state(self): + super()._update_state() + if self.is_pressed: + self._last_interaction_time = rl.get_time() + def _render(self, _): + # Update Scroller layout and restore current selection whenever buttons are updated, before first render + current_selection = self.get_selected_option() + if self._restore_selection and current_selection in self._networks: + self._scroller._layout() + BigMultiOptionDialog._on_option_selected(self, current_selection, smooth_scroll=False) + self._restore_selection = None + super()._render(_) if not self._networks: diff --git a/selfdrive/ui/mici/layouts/settings/toggles.py b/selfdrive/ui/mici/layouts/settings/toggles.py index 82a78ce37..1266311a1 100644 --- a/selfdrive/ui/mici/layouts/settings/toggles.py +++ b/selfdrive/ui/mici/layouts/settings/toggles.py @@ -78,13 +78,13 @@ class TogglesLayoutMici(NavWidget): # CP gating for experimental mode if ui_state.CP is not None: if ui_state.has_longitudinal_control: - self._experimental_btn.set_enabled(True) - self._personality_toggle.set_enabled(True) + self._experimental_btn.set_visible(True) + self._personality_toggle.set_visible(True) else: # no long for now - self._experimental_btn.set_enabled(False) + self._experimental_btn.set_visible(False) self._experimental_btn.set_checked(False) - self._personality_toggle.set_enabled(False) + self._personality_toggle.set_visible(False) ui_state.params.remove("ExperimentalMode") # Refresh toggles from params to mirror external changes diff --git a/selfdrive/ui/mici/onroad/driver_camera_dialog.py b/selfdrive/ui/mici/onroad/driver_camera_dialog.py index 9adb660d8..bab3d6e6f 100644 --- a/selfdrive/ui/mici/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/mici/onroad/driver_camera_dialog.py @@ -51,14 +51,14 @@ class DriverCameraDialog(NavWidget): super().show_event() ui_state.params.put_bool("IsDriverViewEnabled", True) self._publish_alert_sound(None) - device.reset_interactive_timeout(300) + device.set_override_interactive_timeout(300) ui_state.params.remove("DriverTooDistracted") self._pm = messaging.PubMaster(['selfdriveState']) def hide_event(self): super().hide_event() ui_state.params.put_bool("IsDriverViewEnabled", False) - device.reset_interactive_timeout() + device.set_override_interactive_timeout(None) def _handle_mouse_release(self, _): ui_state.params.remove("DriverTooDistracted") @@ -89,12 +89,13 @@ class DriverCameraDialog(NavWidget): self._publish_alert_sound(None) return -1 - self._draw_face_detection(rect) + driver_data = self._draw_face_detection(rect) + if driver_data is not None: + self._draw_eyes(rect, driver_data) # Position dmoji on opposite side from driver - dm_state = ui_state.sm["driverMonitoringState"] driver_state_rect = ( - rect.x if dm_state.isRHD else rect.x + rect.width - self.driver_state_renderer.rect.width, + rect.x if self.driver_state_renderer.is_rhd else rect.x + rect.width - self.driver_state_renderer.rect.width, rect.y + (rect.height - self.driver_state_renderer.rect.height) / 2, ) self.driver_state_renderer.set_position(*driver_state_rect) @@ -138,7 +139,7 @@ class DriverCameraDialog(NavWidget): # Show first event (only one should be active at a time) event_name_str = str(dm_state.events[0].name).split('.')[-1] - alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if dm_state.isRHD else rl.GuiTextAlignment.TEXT_ALIGN_LEFT + alignment = rl.GuiTextAlignment.TEXT_ALIGN_RIGHT if self.driver_state_renderer.is_rhd else rl.GuiTextAlignment.TEXT_ALIGN_LEFT shadow_rect = rl.Rectangle(rect.x + 2, rect.y + 2, rect.width, rect.height) gui_label(shadow_rect, event_name_str, font_size=40, font_weight=FontWeight.BOLD, @@ -159,12 +160,10 @@ class DriverCameraDialog(NavWidget): if self._glasses_texture is None: self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size) - def _draw_face_detection(self, rect: rl.Rectangle) -> None: - driver_state = ui_state.sm["driverStateV2"] - is_rhd = driver_state.wheelOnRightProb > 0.5 - driver_data = driver_state.rightDriverData if is_rhd else driver_state.leftDriverData - face_detect = driver_data.faceProb > 0.7 - if not face_detect: + def _draw_face_detection(self, rect: rl.Rectangle): + dm_state = ui_state.sm["driverMonitoringState"] + driver_data = self.driver_state_renderer.get_driver_data() + if not dm_state.faceDetected: return # Get face position and orientation @@ -188,7 +187,7 @@ class DriverCameraDialog(NavWidget): scale_y = rect.height / 1080.0 fbox_x = rect.x + rect.width / 2 + offset_x * scale_x fbox_y = rect.y + rect.height / 2 + offset_y * scale_y - box_size = 50 + box_size = 75 line_thickness = 3 line_color = rl.Color(255, 255, 255, int(alpha * 255)) @@ -199,7 +198,9 @@ class DriverCameraDialog(NavWidget): line_thickness, line_color, ) + return driver_data + def _draw_eyes(self, rect: rl.Rectangle, driver_data): # Draw eye indicators based on eye probabilities eye_offset_x = 10 eye_offset_y = 10 diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py index 080083c3e..356d7ac83 100644 --- a/selfdrive/ui/mici/onroad/driver_state.py +++ b/selfdrive/ui/mici/onroad/driver_state.py @@ -1,9 +1,7 @@ import pyray as rl -from collections.abc import Callable import numpy as np import math from cereal import log -from openpilot.system.hardware import PC from openpilot.common.filter_simple import FirstOrderFilter from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.widgets import Widget @@ -22,15 +20,11 @@ class DriverStateRenderer(Widget): LINES_ANGLE_INCREMENT = 5 LINES_STALE_ANGLES = 3.0 # seconds - def __init__(self, lines: bool = False, confirm_mode: bool = False, confirm_callback: Callable | None = None): + def __init__(self, lines: bool = False, inset: bool = False): super().__init__() self.set_rect(rl.Rectangle(0, 0, self.BASE_SIZE, self.BASE_SIZE)) - self._lines = lines or confirm_mode - - # In confirm mode, user must fill out the circle to confirm some action in the UI - self._confirm_mode = confirm_mode - self._confirm_callback = confirm_callback - self._confirm_angles: dict[int, float] = {} # angle: timestamp + self._lines = lines + self._inset = inset # In line mode, track smoothed angles assert 360 % self.LINES_ANGLE_INCREMENT == 0 @@ -54,12 +48,20 @@ class DriverStateRenderer(Widget): def load_icons(self): """Load or reload the driver face icon texture""" - self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", self._rect.width, self._rect.height) - self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", self._rect.width, self._rect.height) + cone_and_person_size = round(52 / self.BASE_SIZE * self._rect.width) + + # If inset is enabled, push cone and person smaller by 2x the current inset space + if self._inset: + # Current inset space = (rect.width - cone_and_person_size) / 2 + current_inset = (self._rect.width - cone_and_person_size) / 2 + # Reduce size by 2x the current inset (1x on each side) + cone_and_person_size = round(cone_and_person_size - current_inset * 2) + + self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size) + self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size) center_size = round(36 / self.BASE_SIZE * self._rect.width) self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size) - background_size = round(52 / self.BASE_SIZE * self._rect.width) - self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", background_size, background_size) + self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", self._rect.width, self._rect.height) def set_should_draw(self, should_draw: bool): self._should_draw = should_draw @@ -78,16 +80,22 @@ class DriverStateRenderer(Widget): """Returns True if dmoji should appear active (either actually active or forced)""" return bool(self._force_active or self._is_active) + @property + def is_rhd(self) -> bool: + return self._is_rhd + def _render(self, _): if DEBUG: rl.draw_rectangle_lines_ex(self._rect, 1, rl.RED) rl.draw_texture(self._dm_background, - int(self._rect.x + (self._rect.width - self._dm_background.width) / 2), - int(self._rect.y + (self._rect.height - self._dm_background.height) / 2), + int(self._rect.x), + int(self._rect.y), rl.Color(255, 255, 255, int(255 * self._fade_filter.x))) - rl.draw_texture(self._dm_person, int(self._rect.x), int(self._rect.y), + rl.draw_texture(self._dm_person, + int(self._rect.x + (self._rect.width - self._dm_person.width) / 2), + int(self._rect.y + (self._rect.height - self._dm_person.height) / 2), rl.Color(255, 255, 255, int(255 * 0.9 * self._fade_filter.x))) if self.effective_active: @@ -120,38 +128,18 @@ class DriverStateRenderer(Widget): else: # remove old angles - now = rl.get_time() - self._confirm_angles = {angle: t for angle, t in self._confirm_angles.items() if now - t < self.LINES_STALE_ANGLES} - - looking_center = self._looking_center_filter.x > 0.2 for angle, f in self._head_angles.items(): dst_from_current = ((angle - self._rotation_filter.x) % 360) - 180 target = 1.0 if abs(dst_from_current) <= self.LINES_ANGLE_INCREMENT * 5 else 0.0 if not self._face_detected: target = 0.0 - if self._confirm_mode: - # Extra careful to not add angles when looking near center - if target > 0 and not looking_center: - self._confirm_angles[angle] = now - - # User is looking at area already confirmed, reduce target to indicate where they are - if angle in self._confirm_angles and target == 0: - target = 0.65 - # Reduce all line lengths when looking center if self._looking_center: target = np.interp(self._looking_center_filter.x, [0.0, 1.0], [target, 0.45]) f.update(target) - self._draw_line(angle, f, self._looking_center and angle not in self._confirm_angles) - - # if all lines placed, reset for next time and call callback - if self._confirm_mode: - if len(self._confirm_angles) >= 360 // self.LINES_ANGLE_INCREMENT: - self._confirm_angles = {} - if self._confirm_callback is not None: - self._confirm_callback() + self._draw_line(angle, f, self._looking_center) def _draw_line(self, angle: int, f: FirstOrderFilter, grey: bool): line_length = self._rect.width / 6 @@ -171,10 +159,9 @@ class DriverStateRenderer(Widget): if f.x > 0.01: rl.draw_line_ex((start_x, start_y), (end_x, end_y), 12, color) - def _update_state(self): + def get_driver_data(self): sm = ui_state.sm - # Get monitoring state dm_state = sm["driverMonitoringState"] self._is_active = dm_state.isActiveMode self._is_rhd = dm_state.isRHD @@ -182,6 +169,11 @@ class DriverStateRenderer(Widget): driverstate = sm["driverStateV2"] driver_data = driverstate.rightDriverData if self._is_rhd else driverstate.leftDriverData + return driver_data + + def _update_state(self): + # Get monitoring state + driver_data = self.get_driver_data() driver_orient = driver_data.faceOrientation if len(driver_orient) != 3: @@ -218,10 +210,7 @@ class DriverStateRenderer(Widget): rotation = math.degrees(math.atan2(pitch, yaw)) angle_diff = rotation - self._rotation_filter.x angle_diff = ((angle_diff + 180) % 360) - 180 - if PC and self._confirm_mode: - self._rotation_filter.x += 2 - else: - self._rotation_filter.update(self._rotation_filter.x + angle_diff) + self._rotation_filter.update(self._rotation_filter.x + angle_diff) if not self.should_draw: self._fade_filter.update(0.0) diff --git a/selfdrive/ui/mici/widgets/dialog.py b/selfdrive/ui/mici/widgets/dialog.py index b11056f99..3d9aa3f9e 100644 --- a/selfdrive/ui/mici/widgets/dialog.py +++ b/selfdrive/ui/mici/widgets/dialog.py @@ -274,10 +274,13 @@ class BigInputDialog(BigDialogBase): class BigDialogOptionButton(Widget): + HEIGHT = 64 + SELECTED_HEIGHT = 74 + def __init__(self, option: str): super().__init__() self.option = option - self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), 64)) + self.set_rect(rl.Rectangle(0, 0, int(gui_app.width / 2 + 220), self.HEIGHT)) self._selected = False @@ -285,8 +288,13 @@ class BigDialogOptionButton(Widget): font_weight=FontWeight.DISPLAY_REGULAR, alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, scroll=True) + def show_event(self): + super().show_event() + self._label.reset_scroll() + def set_selected(self, selected: bool): self._selected = selected + self._rect.height = self.SELECTED_HEIGHT if selected else self.HEIGHT def _render(self, _): if DEBUG: @@ -294,11 +302,11 @@ class BigDialogOptionButton(Widget): # FIXME: offset x by -45 because scroller centers horizontally if self._selected: - self._label.set_font_size(74) + self._label.set_font_size(self.SELECTED_HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.9))) self._label.set_font_weight(FontWeight.DISPLAY) else: - self._label.set_font_size(70) + self._label.set_font_size(self.HEIGHT) self._label.set_color(rl.Color(255, 255, 255, int(255 * 0.58))) self._label.set_font_weight(FontWeight.DISPLAY_REGULAR) @@ -319,7 +327,7 @@ class BigMultiOptionDialog(BigDialogBase): self._selected_option: str = self._default_option self._last_selected_option: str = self._selected_option - self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0) + self._scroller = Scroller([], horizontal=False, pad_start=100, pad_end=100, spacing=0, snap_items=True) if self._right_btn is not None: self._scroller.set_enabled(lambda: not cast(Widget, self._right_btn).is_pressed) @@ -327,14 +335,10 @@ class BigMultiOptionDialog(BigDialogBase): self.add_button(BigDialogOptionButton(option)) def add_button(self, button: BigDialogOptionButton): - og_callback = button._click_callback + def click_callback(_btn=button): + self._on_option_selected(_btn.option) - def wrapped_callback(btn=button): - self._on_option_selected(btn.option) - if og_callback: - og_callback() - - button.set_click_callback(wrapped_callback) + button.set_click_callback(click_callback) self._scroller.add_widget(button) def show_event(self): @@ -345,13 +349,23 @@ class BigMultiOptionDialog(BigDialogBase): def get_selected_option(self) -> str: return self._selected_option - def _on_option_selected(self, option: str): + def _on_option_selected(self, option: str, smooth_scroll: bool = True): y_pos = 0.0 for btn in self._scroller._items: - if cast(BigDialogOptionButton, btn).option == option: - y_pos = btn.rect.y + btn = cast(BigDialogOptionButton, btn) + if btn.option == option: + rect_center_y = self._rect.y + self._rect.height / 2 + if btn._selected: + height = btn.rect.height + else: + # when selecting an option under current, account for changing heights + btn_center_y = btn.rect.y + btn.rect.height / 2 # not accurate, just to determine direction + height_offset = BigDialogOptionButton.SELECTED_HEIGHT - BigDialogOptionButton.HEIGHT + height = (BigDialogOptionButton.HEIGHT - height_offset) if rect_center_y < btn_center_y else BigDialogOptionButton.SELECTED_HEIGHT + y_pos = rect_center_y - (btn.rect.y + height / 2) + break - self._scroller.scroll_to(y_pos, smooth=True) + self._scroller.scroll_to(-y_pos, smooth=smooth_scroll) def _selected_option_changed(self): pass diff --git a/selfdrive/ui/onroad/augmented_road_view.py b/selfdrive/ui/onroad/augmented_road_view.py index 1f202141c..a47c04053 100644 --- a/selfdrive/ui/onroad/augmented_road_view.py +++ b/selfdrive/ui/onroad/augmented_road_view.py @@ -14,6 +14,9 @@ from openpilot.system.ui.lib.application import gui_app from openpilot.common.transformations.camera import DEVICE_CAMERAS, DeviceCameraConfig, view_frame_from_device_frame from openpilot.common.transformations.orientation import rot_from_euler +if gui_app.sunnypilot_ui(): + from openpilot.selfdrive.ui.sunnypilot.onroad.hud_renderer import HudRendererSP as HudRenderer + OpState = log.SelfdriveState.OpenpilotState CALIBRATED = log.LiveCalibrationData.Status.calibrated ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index 543ea35e8..f69ad8c49 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -14,16 +14,20 @@ class DriverCameraDialog(CameraView): super().__init__("camerad", VisionStreamType.VISION_STREAM_DRIVER) self.driver_state_renderer = DriverStateRenderer() # TODO: this can grow unbounded, should be given some thought - device.add_interactive_timeout_callback(self.stop_dmonitoringmodeld) + device.add_interactive_timeout_callback(lambda: gui_app.set_modal_overlay(None)) ui_state.params.put_bool("IsDriverViewEnabled", True) - def stop_dmonitoringmodeld(self): + def hide_event(self): + super().hide_event() ui_state.params.put_bool("IsDriverViewEnabled", False) - gui_app.set_modal_overlay(None) + self.close() def _handle_mouse_release(self, _): super()._handle_mouse_release(_) - self.stop_dmonitoringmodeld() + gui_app.set_modal_overlay(None) + + def __del__(self): + self.close() def _render(self, rect): super()._render(rect) diff --git a/selfdrive/ui/sunnypilot/layouts/settings/models.py b/selfdrive/ui/sunnypilot/layouts/settings/models.py index 211aad6a2..5820b34cc 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/models.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/models.py @@ -153,7 +153,7 @@ class ModelsLayout(Widget): self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB") if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading: - device.reset_interactive_timeout() + device._reset_interactive_timeout() for model in bundle.models: if label := labels.get(getattr(model.type, 'raw', model.type)): diff --git a/selfdrive/ui/sunnypilot/layouts/settings/osm.py b/selfdrive/ui/sunnypilot/layouts/settings/osm.py index 3726a820f..f8a3a8504 100644 --- a/selfdrive/ui/sunnypilot/layouts/settings/osm.py +++ b/selfdrive/ui/sunnypilot/layouts/settings/osm.py @@ -165,7 +165,7 @@ class OSMLayout(Widget): pending = ui_state.params.get_bool("OsmDbUpdatesCheck") if downloading or pending: if downloading: - device.reset_interactive_timeout() + device._reset_interactive_timeout() self._update_map_size() self._progress.set_visible(True) progress = ui_state.params.get("OSMDownloadProgress") diff --git a/selfdrive/ui/sunnypilot/onroad/__init__.py b/selfdrive/ui/sunnypilot/onroad/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py new file mode 100644 index 000000000..14a224ae7 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py @@ -0,0 +1,164 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pyray as rl +from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui.elements import ( + UiElement, RelDistElement, RelSpeedElement, SteeringAngleElement, + DesiredLateralAccelElement, ActualLateralAccelElement, DesiredSteeringAngleElement, + AEgoElement, LeadSpeedElement, FrictionCoefficientElement, LatAccelFactorElement, + SteeringTorqueEpsElement, BearingDegElement, AltitudeElement +) +from openpilot.system.ui.lib.application import gui_app, FontWeight +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget + + +class DeveloperUiRenderer(Widget): + DEV_UI_OFF = 0 + DEV_UI_RIGHT = 1 + DEV_UI_BOTTOM = 2 + DEV_UI_BOTH = 3 + + def __init__(self): + super().__init__() + self._font_bold: rl.Font = gui_app.font(FontWeight.BOLD) + self._font_semi_bold: rl.Font = gui_app.font(FontWeight.SEMI_BOLD) + self.dev_ui_mode = self.DEV_UI_OFF + + self.rel_dist_elem = RelDistElement() + self.rel_speed_elem = RelSpeedElement() + self.steering_angle_elem = SteeringAngleElement() + self.desired_lat_accel_elem = DesiredLateralAccelElement() + self.actual_lat_accel_elem = ActualLateralAccelElement() + self.desired_steer_elem = DesiredSteeringAngleElement() + self.a_ego_elem = AEgoElement() + self.lead_speed_elem = LeadSpeedElement() + self.friction_elem = FrictionCoefficientElement() + self.lat_accel_factor_elem = LatAccelFactorElement() + self.steering_torque_elem = SteeringTorqueEpsElement() + self.bearing_elem = BearingDegElement() + self.altitude_elem = AltitudeElement() + + def _update_state(self) -> None: + self.dev_ui_mode = ui_state.developer_ui + + def _render(self, rect: rl.Rectangle) -> None: + if self.dev_ui_mode == self.DEV_UI_OFF: + return + + sm = ui_state.sm + if sm.recv_frame["carState"] < ui_state.started_frame: + return + + if self.dev_ui_mode == self.DEV_UI_RIGHT: + self._draw_right_dev_ui(rect) + elif self.dev_ui_mode == self.DEV_UI_BOTTOM: + self._draw_bottom_dev_ui(rect) + elif self.dev_ui_mode == self.DEV_UI_BOTH: + self._draw_right_dev_ui(rect) + self._draw_bottom_dev_ui(rect) + + def _draw_right_dev_ui(self, rect: rl.Rectangle) -> None: + sm = ui_state.sm + controls_state = sm['controlsState'] + + UI_BORDER_SIZE = 20 + container_width = 184 + x = int(rect.x + rect.width - container_width - UI_BORDER_SIZE * 2) + y = int(rect.y + UI_BORDER_SIZE * 1.5) + + elements = [ + self.rel_dist_elem.update(sm, ui_state.is_metric), + self.rel_speed_elem.update(sm, ui_state.is_metric), + self.steering_angle_elem.update(sm, ui_state.is_metric), + ] + if controls_state.lateralControlState.which() == 'torqueState': + elements.append(self.desired_lat_accel_elem.update(sm, ui_state.is_metric)) + elements.append(self.actual_lat_accel_elem.update(sm, ui_state.is_metric)) + else: + elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric)) + + current_y = y + for element in elements: + current_y += self._draw_right_dev_ui_element(x, current_y, element) + + def _draw_right_dev_ui_element(self, x: int, y: int, element: UiElement) -> int: + x += 0 + y += 230 + container_width = 184 + label_size = 28 + value_size = 60 + unit_size = 28 + label_width = measure_text_cached(self._font_bold, element.label, label_size, 0).x + centered_label_x = x + (container_width - label_width) / 2 + rl.draw_text_ex(self._font_bold, element.label, rl.Vector2(centered_label_x, y), label_size, 0, rl.WHITE) + + y += 45 + value_width = measure_text_cached(self._font_bold, element.value, value_size, 0).x + centered_value_x = x + (container_width - value_width) / 2 + rl.draw_text_ex(self._font_bold, element.value, rl.Vector2(centered_value_x, y), value_size, 0, element.color) + + if element.unit: + units_height = measure_text_cached(self._font_bold, element.unit, unit_size, 0).x + + units_x = x + container_width - 10 + units_y = y + (value_size / 2) + (units_height / 2) + + rl.draw_text_pro(self._font_bold, element.unit, rl.Vector2(units_x, units_y), rl.Vector2(0, 0), -90.0, unit_size, 0, rl.WHITE) + + return 130 + + def _draw_bottom_dev_ui(self, rect: rl.Rectangle) -> None: + sm = ui_state.sm + bar_height = 61 + y = int(rect.y + rect.height - bar_height) + + rl.draw_rectangle(int(rect.x), y, int(rect.width), bar_height, + rl.Color(0, 0, 0, 100)) + + elements = [ + self.a_ego_elem.update(sm, ui_state.is_metric), + self.lead_speed_elem.update(sm, ui_state.is_metric), + ] + + # Add torque-specific elements if using torque control + if sm['controlsState'].lateralControlState.which() == 'torqueState': + if sm.valid['liveTorqueParameters']: + elements.extend([ + self.friction_elem.update(sm, ui_state.is_metric), + self.lat_accel_factor_elem.update(sm, ui_state.is_metric), + ]) + else: + # Non-torque: show steering torque and GPS data + elements.append(self.steering_torque_elem.update(sm, ui_state.is_metric)) + + if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']: + elements.append(self.bearing_elem.update(sm, ui_state.is_metric)) + + # Add altitude if GPS available + if sm.valid['gpsLocationExternal'] or sm.valid['gpsLocation']: + elements.append(self.altitude_elem.update(sm, ui_state.is_metric)) + + current_x = int(rect.x + 90) + center_y = y + bar_height // 2 + for element in elements: + current_x += self._draw_bottom_dev_ui_element(current_x, center_y, element) + + def _draw_bottom_dev_ui_element(self, x: int, y: int, element: UiElement) -> int: + font_size = 38 + + label_text = f"{element.label} " + label_width = measure_text_cached(self._font_bold, label_text, font_size, 0).x + rl.draw_text_ex(self._font_bold, label_text, rl.Vector2(x, y - font_size // 2), font_size, 0, rl.WHITE) + + value_width = measure_text_cached(self._font_bold, element.value, font_size, 0).x + rl.draw_text_ex(self._font_bold, element.value, rl.Vector2(x + label_width + 10, y - font_size // 2), font_size, 0, element.color) + + if element.unit: + rl.draw_text_ex(self._font_bold, element.unit, rl.Vector2(x + label_width + value_width + 20, y - font_size // 2), font_size, 0, rl.WHITE) + + return 400 diff --git a/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py new file mode 100644 index 000000000..e8daca886 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py @@ -0,0 +1,303 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pyray as rl +from dataclasses import dataclass + +from openpilot.common.constants import CV + + +@dataclass +class UiElement: + value: str + label: str + unit: str + color: rl.Color + + +class LeadInfoElement: + @staticmethod + def get_lead_status(sm): + lead_one = sm['radarState'].leadOne + return lead_one.status, lead_one.dRel, lead_one.vRel + + @staticmethod + def get_lead_color(lead_d_rel: float, lead_v_rel: float = 0.0, use_v_rel: bool = False) -> rl.Color: + if use_v_rel: + if lead_v_rel < -4.4704: + return rl.RED + elif lead_v_rel < 0: + return rl.Color(255, 188, 0, 255) # Orange + else: + if lead_d_rel < 5: + return rl.RED + elif lead_d_rel < 15: + return rl.Color(255, 188, 0, 255) # Orange + return rl.WHITE + + +class LateralControlElement: + @staticmethod + def get_lat_color(lat_active: bool, steer_override: bool, angle_steers: float = 0.0, + check_angle: bool = False) -> rl.Color: + color = rl.WHITE + if lat_active: + color = rl.Color(145, 155, 149, 255) if steer_override else rl.Color(0, 255, 0, 255) + + if check_angle and lat_active: + if abs(angle_steers) > 180: + color = rl.RED + elif abs(angle_steers) > 90: + color = rl.Color(255, 188, 0, 255) + else: + # Keep green/grey from above + pass + elif check_angle and not lat_active: + if abs(angle_steers) > 180: + color = rl.RED + elif abs(angle_steers) > 90: + color = rl.Color(255, 188, 0, 255) + + return color + + +class RelDistElement(LeadInfoElement): + def __init__(self): + self.unit = "m" + + def update(self, sm, is_metric: bool) -> UiElement: + lead_status, lead_d_rel, _ = self.get_lead_status(sm) + value = f"{lead_d_rel:.0f}" if lead_status else "-" + color = self.get_lead_color(lead_d_rel) if lead_status else rl.WHITE + return UiElement(value, "REL DIST", self.unit, color) + + +class RelSpeedElement(LeadInfoElement): + def __init__(self): + self.unit = "km/h" + + def update(self, sm, is_metric: bool) -> UiElement: + lead_status, _, lead_v_rel = self.get_lead_status(sm) + + self.unit = "km/h" if is_metric else "mph" + + conversion = CV.MS_TO_KPH if is_metric else CV.MS_TO_MPH + value = f"{lead_v_rel * conversion:.0f}" if lead_status else "-" + color = self.get_lead_color(0, lead_v_rel, use_v_rel=True) if lead_status else rl.WHITE + + return UiElement(value, "REL SPEED", self.unit, color) + + +class SteeringAngleElement(LateralControlElement): + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + car_state = sm['carState'] + angle_steers = car_state.steeringAngleDeg + lat_active = sm['carControl'].latActive + steer_override = car_state.steeringPressed + + value = f"{angle_steers:.1f}°" + color = self.get_lat_color(lat_active, steer_override, angle_steers, check_angle=True) + + return UiElement(value, "REAL STEER", self.unit, color) + + +class DesiredSteeringAngleElement(LateralControlElement): + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + car_state = sm['carState'] + controls_state = sm['controlsState'] + lat_active = sm['carControl'].latActive + angle_steers = car_state.steeringAngleDeg + steer_angle_desired = controls_state.lateralControlState.angleState.steeringAngleDeg + + value = f"{steer_angle_desired:.1f}°" if lat_active else "-" + + color = rl.WHITE + if lat_active: + if abs(angle_steers) > 180: + color = rl.RED + elif abs(angle_steers) > 90: + color = rl.Color(255, 188, 0, 255) + else: + color = rl.Color(0, 255, 0, 255) + + return UiElement(value, "DESIRED STEER", self.unit, color) + + +class ActualLateralAccelElement(LateralControlElement): + def __init__(self): + self.unit = "m/s^2" + + def update(self, sm, is_metric: bool) -> UiElement: + controls_state = sm['controlsState'] + curvature = controls_state.curvature + v_ego = sm['carState'].vEgo + roll = sm['liveParameters'].roll if sm.valid['liveParameters'] else 0.0 + lat_active = sm['carControl'].latActive + steer_override = sm['carState'].steeringPressed + + actual_lat_accel = (curvature * v_ego ** 2) - (roll * 9.81) + value = f"{actual_lat_accel:.2f}" + color = self.get_lat_color(lat_active, steer_override) + + return UiElement(value, "ACTUAL L.A.", self.unit, color) + + +class DesiredLateralAccelElement(LateralControlElement): + def __init__(self): + self.unit = "m/s^2" + + def update(self, sm, is_metric: bool) -> UiElement: + controls_state = sm['controlsState'] + desired_curvature = controls_state.desiredCurvature + v_ego = sm['carState'].vEgo + roll = sm['liveParameters'].roll if sm.valid['liveParameters'] else 0.0 + lat_active = sm['carControl'].latActive + steer_override = sm['carState'].steeringPressed + + desired_lat_accel = (desired_curvature * v_ego ** 2) - (roll * 9.81) + value = f"{desired_lat_accel:.2f}" if lat_active else "-" + color = self.get_lat_color(lat_active, steer_override) + + return UiElement(value, "DESIRED L.A.", self.unit, color) + + +class AEgoElement: + def __init__(self): + self.unit = "m/s^2" + + def update(self, sm, is_metric: bool) -> UiElement: + a_ego = sm['carState'].aEgo + value = f"{a_ego:.1f}" + return UiElement(value, "ACC.", self.unit, rl.WHITE) + + +class LeadSpeedElement(LeadInfoElement): + def __init__(self): + self.unit = "km/h" + + def update(self, sm, is_metric: bool) -> UiElement: + lead_status, _, lead_v_rel = self.get_lead_status(sm) + v_ego = sm['carState'].vEgo + + self.unit = "km/h" if is_metric else "mph" + + conversion = CV.MS_TO_KPH if is_metric else CV.MS_TO_MPH + value = f"{(lead_v_rel + v_ego) * conversion:.0f}" if lead_status else "-" + color = self.get_lead_color(0, lead_v_rel, use_v_rel=True) if lead_status else rl.WHITE + + return UiElement(value, "L.S.", self.unit, color) + + +class FrictionCoefficientElement: + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + ltp = sm['liveTorqueParameters'] + friction_coef = ltp.frictionCoefficientFiltered + live_valid = ltp.liveValid + + value = f"{friction_coef:.3f}" + color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE + return UiElement(value, "FRIC.", self.unit, color) + + +class LatAccelFactorElement: + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + ltp = sm['liveTorqueParameters'] + lat_accel_factor = ltp.latAccelFactorFiltered + live_valid = ltp.liveValid + + value = f"{lat_accel_factor:.3f}" + color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE + return UiElement(value, "L.A.F.", self.unit, color) + + +class SteeringTorqueEpsElement: + def __init__(self): + self.unit = "N·dm" + + def update(self, sm, is_metric: bool) -> UiElement: + steering_torque_eps = sm['carState'].steeringTorqueEps + value = f"{abs(steering_torque_eps):.1f}" + return UiElement(value, "E.T.", self.unit, rl.WHITE) + + +class GpsInfoElement: + @staticmethod + def get_gps_data(sm): + if sm.valid['gpsLocationExternal']: + return sm['gpsLocationExternal'], True + elif sm.valid['gpsLocation']: + return sm['gpsLocation'], True + return None, False + + +class BearingDegElement(GpsInfoElement): + def __init__(self): + self.unit = "" + + def update(self, sm, is_metric: bool) -> UiElement: + gps_data, valid = self.get_gps_data(sm) + if not valid: + return UiElement("OFF | -", "B.D.", self.unit, rl.WHITE) + + bearing_accuracy_deg = gps_data.bearingAccuracyDeg + bearing_deg = gps_data.bearingDeg + + if bearing_accuracy_deg != 180.0: + value = f"{bearing_deg:.0f}°" + if (337.5 <= bearing_deg <= 360) or (0 <= bearing_deg <= 22.5): + dir_value = "N" + elif 22.5 < bearing_deg < 67.5: + dir_value = "NE" + elif 67.5 <= bearing_deg <= 112.5: + dir_value = "E" + elif 112.5 < bearing_deg < 157.5: + dir_value = "SE" + elif 157.5 <= bearing_deg <= 202.5: + dir_value = "S" + elif 202.5 < bearing_deg < 247.5: + dir_value = "SW" + elif 247.5 <= bearing_deg <= 292.5: + dir_value = "W" + else: # 292.5 < bearing_deg < 337.5 + dir_value = "NW" + else: + value = "-" + dir_value = "OFF" + + return UiElement(f"{dir_value} | {value}", "B.D.", self.unit, rl.WHITE) + + +class AltitudeElement(GpsInfoElement): + def __init__(self): + self.unit = "m" + + def update(self, sm, is_metric: bool) -> UiElement: + gps_data, valid = self.get_gps_data(sm) + + gps_accuracy = 0.0 + altitude = 0.0 + + if valid: + altitude = gps_data.altitude + if sm.valid['gpsLocationExternal']: + gps_accuracy = gps_data.horizontalAccuracy + else: + gps_accuracy = 1.0 # Simulate valid for legacy check + + value = f"{altitude:.1f}" if gps_accuracy != 0.0 else "-" + return UiElement(value, "ALT.", self.unit, rl.WHITE) diff --git a/selfdrive/ui/sunnypilot/onroad/hud_renderer.py b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py new file mode 100644 index 000000000..33582df19 --- /dev/null +++ b/selfdrive/ui/sunnypilot/onroad/hud_renderer.py @@ -0,0 +1,20 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import pyray as rl + +from openpilot.selfdrive.ui.onroad.hud_renderer import HudRenderer +from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer + + +class HudRendererSP(HudRenderer): + def __init__(self): + super().__init__() + self.developer_ui = DeveloperUiRenderer() + + def _render(self, rect: rl.Rectangle) -> None: + super()._render(rect) + self.developer_ui.render(rect) diff --git a/selfdrive/ui/sunnypilot/ui_state.py b/selfdrive/ui/sunnypilot/ui_state.py index af625ee61..8a0bc24ad 100644 --- a/selfdrive/ui/sunnypilot/ui_state.py +++ b/selfdrive/ui/sunnypilot/ui_state.py @@ -27,3 +27,4 @@ class UIStateSP: if CP_SP_bytes is not None: self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP) self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled") + self.developer_ui = self.params.get("DevUIInfo") diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 0d52076fc..30394701b 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -207,6 +207,7 @@ class Device: def __init__(self): self._ignition = False self._interaction_time: float = -1 + self._override_interactive_timeout: int | None = None self._interactive_timeout_callbacks: list[Callable] = [] self._prev_timed_out = False self._awake: bool = True @@ -220,11 +221,21 @@ class Device: def awake(self) -> bool: return self._awake - def reset_interactive_timeout(self, timeout: int = -1) -> None: - if timeout == -1: - ignition_timeout = 10 if gui_app.big_ui() else 5 - timeout = ignition_timeout if ui_state.ignition else 30 - self._interaction_time = time.monotonic() + timeout + def set_override_interactive_timeout(self, timeout: int | None) -> None: + # Override the interactive timeout duration temporarily + self._override_interactive_timeout = timeout + self._reset_interactive_timeout() + + @property + def interactive_timeout(self) -> int: + if self._override_interactive_timeout is not None: + return self._override_interactive_timeout + + ignition_timeout = 10 if gui_app.big_ui() else 5 + return ignition_timeout if ui_state.ignition else 30 + + def _reset_interactive_timeout(self) -> None: + self._interaction_time = time.monotonic() + self.interactive_timeout def add_interactive_timeout_callback(self, callback: Callable): self._interactive_timeout_callbacks.append(callback) @@ -232,7 +243,7 @@ class Device: def update(self): # do initial reset if self._interaction_time <= 0: - self.reset_interactive_timeout() + self._reset_interactive_timeout() self._update_brightness() self._update_wakefulness() @@ -278,7 +289,7 @@ class Device: enable_screen_event = ui_state.ignition and (ui_state.has_alert or ui_state.has_status_change) if ignition_just_turned_off or ignition_just_turned_on or enable_screen_event or any(ev.left_down for ev in gui_app.mouse_events): - self.reset_interactive_timeout() + self._reset_interactive_timeout() interaction_timeout = time.monotonic() > self._interaction_time if interaction_timeout and not self._prev_timed_out: diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index b534b7e37..83114ac55 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -102,7 +102,10 @@ def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: _initialize_torque_lateral_control(CI, CP, enforce_torque, nnlc_enabled) _cleanup_unsupported_params(CP, CP_SP) - STATSLOGSP.raw('sunnypilot.car_params', CP.to_dict()) + try: + STATSLOGSP.raw('sunnypilot.car_params', CP.to_dict()) + except RuntimeError: + pass # to_dict fails on macOS due to library issues. # STATSLOGSP.raw('sunnypilot_params.car_params_sp', CP_SP.to_dict()) # https://github.com/sunnypilot/opendbc/pull/361 diff --git a/sunnypilot/selfdrive/car/sync_car_list_param.py b/sunnypilot/selfdrive/car/sync_car_list_param.py new file mode 100755 index 000000000..5e25f6da9 --- /dev/null +++ b/sunnypilot/selfdrive/car/sync_car_list_param.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import json +import os + +from openpilot.common.basedir import BASEDIR +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog + +CAR_LIST_JSON_OUT = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "car", "car_list.json") + + +def update_car_list_param(): + with open(CAR_LIST_JSON_OUT) as f: + current_car_list = json.load(f) + + params = Params() + if params.get("CarList") != current_car_list: + params.put("CarList", current_car_list) + cloudlog.warning("Updated CarList param with latest platform list") + else: + cloudlog.warning("CarList param is up to date, no need to update") + + +if __name__ == "__main__": + update_car_list_param() diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index a57b6ef55..3aeacf6a3 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -28,6 +28,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce create_connection, WebSocketConnectionClosedException) import cereal.messaging as messaging +from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param from openpilot.sunnypilot.sunnylink.api import SunnylinkApi from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string @@ -279,6 +280,8 @@ def main(exit_event: threading.Event = None): sunnylink_api = SunnylinkApi(sunnylink_dongle_id) UploadQueueCache.initialize(upload_queue) + update_car_list_param() + ws_uri = f"{SUNNYLINK_ATHENA_HOST}" conn_start = None conn_retries = 0 diff --git a/sunnypilot/sunnylink/params_metadata.json b/sunnypilot/sunnylink/params_metadata.json index 3228fd0d6..707e0620f 100644 --- a/sunnypilot/sunnylink/params_metadata.json +++ b/sunnypilot/sunnylink/params_metadata.json @@ -125,6 +125,10 @@ "title": "Car Battery Capacity", "description": "" }, + "CarList": { + "title": "Supported Car List", + "description": "All supported platform in sunnypilot" + }, "CarParams": { "title": "Car Params", "description": "" diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 21de1ff33..47da32102 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -238,7 +238,7 @@ void loggerd_thread() { if (it.should_log || (encoder && !livestream_encoder) || record_audio) { LOGD("logging %s", it.name.c_str()); - SubSocket * sock = SubSocket::create(ctx.get(), it.name); + SubSocket * sock = SubSocket::create(ctx.get(), it.name, "127.0.0.1", false, true, it.queue_size); assert(sock != NULL); poller->registerSocket(sock); service_state[sock] = { diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 1cac16adc..9703ac2f5 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -24,7 +24,6 @@ from openpilot.system.version import get_version from openpilot.tools.lib.helpers import RE from openpilot.tools.lib.logreader import LogReader from msgq.visionipc import VisionIpcServer, VisionStreamType -from openpilot.common.transformations.camera import DEVICE_CAMERAS SentinelType = log.Sentinel.SentinelType @@ -99,13 +98,17 @@ class TestLoggerd: return sent_msgs def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5): - d = DEVICE_CAMERAS[("tici", "ar0231")] + # Use small frame sizes for testing (width, height, size, stride, uv_offset) + # NV12 format: size = stride * height * 1.5, uv_offset = stride * height + w, h = 320, 240 + frame_spec = (w, h, w * h * 3 // 2, w, w * h) streams = [ - (VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048 * 2346, 2048, 2048 * 1216), "roadCameraState"), - (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048 * 2346, 2048, 2048 * 1216), "driverCameraState"), - (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048 * 2346, 2048, 2048 * 1216), "wideRoadCameraState"), + (VisionStreamType.VISION_STREAM_ROAD, frame_spec, "roadCameraState"), + (VisionStreamType.VISION_STREAM_DRIVER, frame_spec, "driverCameraState"), + (VisionStreamType.VISION_STREAM_WIDE_ROAD, frame_spec, "wideRoadCameraState"), ] + sm = messaging.SubMaster(["roadEncodeData"]) pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"]) vipc_server = VisionIpcServer("camerad") for stream_type, frame_spec, _ in streams: @@ -139,6 +142,8 @@ class TestLoggerd: for _, _, state in streams: assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) + sm.update(100) # wait for encode data publish + managed_processes["loggerd"].stop() managed_processes["encoderd"].stop() diff --git a/system/sensord/tests/test_sensord.py b/system/sensord/tests/test_sensord.py index 1dab65238..5e98e1224 100644 --- a/system/sensord/tests/test_sensord.py +++ b/system/sensord/tests/test_sensord.py @@ -56,8 +56,7 @@ def get_irq_count(irq: int): return sum(per_cpu) def read_sensor_events(duration_sec): - sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'accelerometer2', - 'gyroscope2', 'temperatureSensor', 'temperatureSensor2'] + sensor_types = ['accelerometer', 'gyroscope', 'magnetometer', 'temperatureSensor',] socks = {} poller = messaging.Poller() events = defaultdict(list) diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 20f2d3a42..1b61009a4 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -81,7 +81,7 @@ void main() { """ DEFAULT_TEXT_SIZE = 60 -DEFAULT_TEXT_COLOR = rl.WHITE +DEFAULT_TEXT_COLOR = rl.Color(255, 255, 255, int(255 * 0.9)) # Qt draws fonts accounting for ascent/descent differently, so compensate to match old styles # The real scales for the fonts below range from 1.212 to 1.266 @@ -221,6 +221,7 @@ class GuiApplication(GuiApplicationExt): self._trace_log_callback = None self._modal_overlay = ModalOverlay() self._modal_overlay_shown = False + self._modal_overlay_tick: Callable[[], None] | None = None self._mouse = MouseState(self._scale) self._mouse_events: list[MouseEvent] = [] @@ -352,6 +353,9 @@ class GuiApplication(GuiApplicationExt): self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) + def set_modal_overlay_tick(self, tick_function: Callable | None): + self._modal_overlay_tick = tick_function + def set_should_render(self, should_render: bool): self._should_render = should_render @@ -490,6 +494,9 @@ class GuiApplication(GuiApplicationExt): # Handle modal overlay rendering and input processing if self._handle_modal_overlay(): + # Allow a Widget to still run a function while overlay is shown + if self._modal_overlay_tick is not None: + self._modal_overlay_tick() yield False else: yield True diff --git a/system/ui/lib/scroll_panel2.py b/system/ui/lib/scroll_panel2.py index 00ef95cc8..0859071da 100644 --- a/system/ui/lib/scroll_panel2.py +++ b/system/ui/lib/scroll_panel2.py @@ -175,7 +175,8 @@ class GuiScrollPanel2: # Do not update velocity on the same frame the mouse was released previous_mouse_pos = self._get_mouse_pos(cast(MouseEvent, self._previous_mouse_event)) delta_x = mouse_pos - previous_mouse_pos - self._velocity = delta_x / (mouse_event.t - cast(MouseEvent, self._previous_mouse_event).t) + delta_t = max((mouse_event.t - cast(MouseEvent, self._previous_mouse_event).t), 1e-6) + self._velocity = delta_x / delta_t self._velocity = max(-MAX_SPEED, min(MAX_SPEED, self._velocity)) self._velocity_buffer.append(self._velocity) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index cd809f269..14eb6769d 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -630,7 +630,8 @@ class WifiManager: known_connections = self._get_connections() networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (-n.is_connected, n.ssid.lower())) + # sort with quantized strength to reduce jumping + networks.sort(key=lambda n: (-n.is_connected, -round(n.strength / 100 * 4), n.ssid.lower())) self._networks = networks self._update_ipv4_address() diff --git a/system/ui/mici_setup.py b/system/ui/mici_setup.py index 4435bd95e..adc3f125c 100755 --- a/system/ui/mici_setup.py +++ b/system/ui/mici_setup.py @@ -18,14 +18,13 @@ from openpilot.common.utils import run_cmd from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.wifi_manager import WifiManager -from openpilot.selfdrive.ui.ui_state import device from openpilot.system.ui.lib.scroll_panel2 import GuiScrollPanel2 from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.button import (IconButton, SmallButton, WideRoundedButton, SmallerRoundedButton, SmallCircleIconButton, WidishRoundedButton, SmallRedPillButton, FullRoundedButton) from openpilot.system.ui.widgets.label import UnifiedLabel -from openpilot.system.ui.widgets.slider import LargerSlider +from openpilot.system.ui.widgets.slider import LargerSlider, SmallSlider from openpilot.selfdrive.ui.mici.layouts.settings.network import WifiUIMici from openpilot.selfdrive.ui.mici.widgets.dialog import BigInputDialog @@ -199,15 +198,20 @@ class TermsPage(Widget): self._scroll_panel = GuiScrollPanel2(horizontal=False) self._continue_text = continue_text - self._continue_button: WideRoundedButton | FullRoundedButton - if back_callback is not None: + self._continue_slider: bool = continue_text in ("reboot", "power off") + self._continue_button: WideRoundedButton | FullRoundedButton | SmallSlider + if self._continue_slider: + self._continue_button = SmallSlider(continue_text, confirm_callback=continue_callback) + self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed) + elif back_callback is not None: self._continue_button = WideRoundedButton(continue_text) else: self._continue_button = FullRoundedButton(continue_text) self._continue_button.set_enabled(False) self._continue_button.set_opacity(0.0) self._continue_button.set_touch_valid_callback(self._scroll_panel.is_touch_valid) - self._continue_button.set_click_callback(continue_callback) + if not self._continue_slider: + self._continue_button.set_click_callback(continue_callback) self._enable_back = back_callback is not None self._back_button = SmallButton(back_text) @@ -228,7 +232,7 @@ class TermsPage(Widget): def show_event(self): super().show_event() - device.reset_interactive_timeout(300) + self.reset() @property @abstractmethod @@ -270,6 +274,11 @@ class TermsPage(Widget): rl.draw_rectangle_gradient_v(int(self._rect.x), int(self._rect.y + self._rect.height - 20), int(self._rect.width), 20, rl.BLANK, rl.BLACK) + # fade out back button as slider is moved + if self._continue_slider and scroll_offset <= self._scrolled_down_offset: + self._back_button.set_opacity(1.0 - self._continue_button.slider_percentage) + self._back_button.set_visible(self._continue_button.slider_percentage < 0.99) + self._back_button.render(rl.Rectangle( self._rect.x + 8, self._rect.y + self._rect.height - self._back_button.rect.height, @@ -280,6 +289,8 @@ class TermsPage(Widget): continue_x = self._rect.x + 8 if self._enable_back: continue_x = self._rect.x + self._rect.width - self._continue_button.rect.width - 8 + if self._continue_slider: + continue_x += 8 self._continue_button.render(rl.Rectangle( continue_x, self._rect.y + self._rect.height - self._continue_button.rect.height, @@ -443,9 +454,12 @@ class NetworkSetupPage(Widget): self._continue_button.set_click_callback(continue_callback) self._state = NetworkSetupState.MAIN + self._prev_has_internet = False def set_state(self, state: NetworkSetupState): self._state = state + if state == NetworkSetupState.WIFI_PANEL: + self._wifi_ui.show_event() def set_has_internet(self, has_internet: bool): if has_internet: @@ -457,6 +471,10 @@ class NetworkSetupPage(Widget): self._network_header.set_icon(self._no_wifi_txt) self._continue_button.set_enabled(False) + if has_internet and not self._prev_has_internet: + self.set_state(NetworkSetupState.MAIN) + self._prev_has_internet = has_internet + def show_event(self): super().show_event() self._state = NetworkSetupState.MAIN @@ -513,6 +531,8 @@ class Setup(Widget): self._network_monitor = NetworkConnectivityMonitor( lambda: self.state in (SetupState.NETWORK_SETUP, SetupState.NETWORK_SETUP_CUSTOM_SOFTWARE) ) + self._prev_has_internet = False + gui_app.set_modal_overlay_tick(self._modal_overlay_tick) self._start_page = StartPage() self._start_page.set_click_callback(self._getting_started_button_callback) @@ -530,6 +550,12 @@ class Setup(Widget): self._downloading_page = DownloadingPage() + def _modal_overlay_tick(self): + has_internet = self._network_monitor.network_connected.is_set() + if has_internet and not self._prev_has_internet: + gui_app.set_modal_overlay(None) + self._prev_has_internet = has_internet + def _update_state(self): self._wifi_manager.process_callbacks() @@ -603,7 +629,9 @@ class Setup(Widget): def render_network_setup(self, rect: rl.Rectangle): self._network_setup_page.render(rect) - self._network_setup_page.set_has_internet(self._network_monitor.network_connected.is_set()) + has_internet = self._network_monitor.network_connected.is_set() + self._prev_has_internet = has_internet + self._network_setup_page.set_has_internet(has_internet) def render_downloading(self, rect: rl.Rectangle): self._downloading_page.set_progress(self.download_progress) diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index 546c682f3..a3fed6d96 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -100,6 +100,7 @@ class Widget(abc.ABC): if not self.is_visible: return None + self._layout() ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle @@ -151,13 +152,16 @@ class Widget(abc.ABC): self.__is_pressed[mouse_event.slot] = False self._handle_mouse_event(mouse_event) - @abc.abstractmethod - def _render(self, rect: rl.Rectangle) -> bool | int | None: - """Render the widget within the given rectangle.""" + def _layout(self) -> None: + """Optionally lay out child widgets separately. This is called before rendering.""" def _update_state(self): """Optionally update the widget's non-layout state. This is called before rendering.""" + @abc.abstractmethod + def _render(self, rect: rl.Rectangle) -> bool | int | None: + """Render the widget within the given rectangle.""" + def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" @@ -270,13 +274,17 @@ class NavWidget(Widget, abc.ABC): in_dismiss_area = mouse_event.pos.y < self._rect.height * self.BACK_TOUCH_AREA_PERCENTAGE scroller_at_top = False + vertical_scroller = False # TODO: -20? snapping in WiFi dialog can make offset not be positive at the top if hasattr(self, '_scroller'): scroller_at_top = self._scroller.scroll_panel.get_offset() >= -20 and not self._scroller._horizontal + vertical_scroller = not self._scroller._horizontal elif hasattr(self, '_scroll_panel'): scroller_at_top = self._scroll_panel.get_offset() >= -20 and not self._scroll_panel._horizontal + vertical_scroller = not self._scroll_panel._horizontal - if in_dismiss_area or scroller_at_top: + # Vertical scrollers need to be at the top to swipe away to prevent erroneous swipes + if (not vertical_scroller and in_dismiss_area) or scroller_at_top: self._can_swipe_away = True self._back_button_start_pos = mouse_event.pos diff --git a/system/ui/widgets/button.py b/system/ui/widgets/button.py index 34b2a51a4..9c0ea75b4 100644 --- a/system/ui/widgets/button.py +++ b/system/ui/widgets/button.py @@ -201,6 +201,7 @@ class SmallCircleIconButton(Widget): self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._icon_bg_txt = gui_app.texture("icons_mici/setup/small_button.png", 100, 100) self._icon_bg_pressed_txt = gui_app.texture("icons_mici/setup/small_button_pressed.png", 100, 100) + self._icon_bg_disabled_txt = gui_app.texture("icons_mici/setup/small_button_disabled.png", 100, 100) self._icon_txt = icon_txt def set_opacity(self, opacity: float, smooth: bool = False): @@ -210,12 +211,18 @@ class SmallCircleIconButton(Widget): self._opacity_filter.x = opacity def _render(self, _): - bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) + if not self.enabled: + bg_txt = self._icon_bg_disabled_txt + icon_white = rl.Color(255, 255, 255, int(white.a * 0.35)) + else: + bg_txt = self._icon_bg_pressed_txt if self.is_pressed else self._icon_bg_txt + icon_white = white + rl.draw_texture(bg_txt, int(self.rect.x), int(self.rect.y), white) icon_x = self.rect.x + (self.rect.width - self._icon_txt.width) / 2 icon_y = self.rect.y + (self.rect.height - self._icon_txt.height) / 2 - rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), white) + rl.draw_texture(self._icon_txt, int(icon_x), int(icon_y), icon_white) class SmallButton(Widget): diff --git a/system/ui/widgets/scroller.py b/system/ui/widgets/scroller.py index 72f76d90c..f33ba941b 100644 --- a/system/ui/widgets/scroller.py +++ b/system/ui/widgets/scroller.py @@ -52,6 +52,11 @@ class Scroller(Widget): self._zoom_filter = FirstOrderFilter(1.0, 0.2, 1 / gui_app.target_fps) self._zoom_out_t: float = 0.0 + # layout state + self._visible_items: list[Widget] = [] + self._content_size: float = 0.0 + self._scroll_offset: float = 0.0 + self._item_pos_filter = BounceFilter(0.0, 0.05, 1 / gui_app.target_fps) # when not pressed, snap to closest item to be center @@ -74,7 +79,7 @@ class Scroller(Widget): return # FIXME: the padding correction doesn't seem correct - scroll_offset = self.scroll_panel.get_offset() - pos + self._pad_end + scroll_offset = self.scroll_panel.get_offset() - pos if smooth: self._scrolling_to = scroll_offset else: @@ -160,28 +165,28 @@ class Scroller(Widget): return self.scroll_panel.get_offset() - def _render(self, _): - visible_items = [item for item in self._items if item.is_visible] + def _layout(self): + self._visible_items = [item for item in self._items if item.is_visible] # Add line separator between items if self._line_separator is not None: - l = len(visible_items) - for i in range(1, len(visible_items)): - visible_items.insert(l - i, self._line_separator) + l = len(self._visible_items) + for i in range(1, len(self._visible_items)): + self._visible_items.insert(l - i, self._line_separator) - content_size = sum(item.rect.width if self._horizontal else item.rect.height for item in visible_items) - content_size += self._spacing * (len(visible_items) - 1) - content_size += self._pad_start + self._pad_end + self._content_size = sum(item.rect.width if self._horizontal else item.rect.height for item in self._visible_items) + self._content_size += self._spacing * (len(self._visible_items) - 1) + self._content_size += self._pad_start + self._pad_end - scroll_offset = self._get_scroll(visible_items, content_size) + self._scroll_offset = self._get_scroll(self._visible_items, self._content_size) rl.begin_scissor_mode(int(self._rect.x), int(self._rect.y), int(self._rect.width), int(self._rect.height)) - self._item_pos_filter.update(scroll_offset) + self._item_pos_filter.update(self._scroll_offset) cur_pos = 0 - for idx, item in enumerate(visible_items): + for idx, item in enumerate(self._visible_items): spacing = self._spacing if (idx > 0) else self._pad_start # Nicely lay out items horizontally/vertically if self._horizontal: @@ -195,29 +200,31 @@ class Scroller(Widget): # Consider scroll if self._horizontal: - x += scroll_offset + x += self._scroll_offset else: - y += scroll_offset + y += self._scroll_offset # Add some jello effect when scrolling if DO_JELLO: if self._horizontal: cx = self._rect.x + self._rect.width / 2 - jello_offset = scroll_offset - np.interp(x + item.rect.width / 2, - [self._rect.x, cx, self._rect.x + self._rect.width], - [self._item_pos_filter.x, scroll_offset, self._item_pos_filter.x]) + jello_offset = self._scroll_offset - np.interp(x + item.rect.width / 2, + [self._rect.x, cx, self._rect.x + self._rect.width], + [self._item_pos_filter.x, self._scroll_offset, self._item_pos_filter.x]) x -= np.clip(jello_offset, -20, 20) else: cy = self._rect.y + self._rect.height / 2 - jello_offset = scroll_offset - np.interp(y + item.rect.height / 2, - [self._rect.y, cy, self._rect.y + self._rect.height], - [self._item_pos_filter.x, scroll_offset, self._item_pos_filter.x]) + jello_offset = self._scroll_offset - np.interp(y + item.rect.height / 2, + [self._rect.y, cy, self._rect.y + self._rect.height], + [self._item_pos_filter.x, self._scroll_offset, self._item_pos_filter.x]) y -= np.clip(jello_offset, -20, 20) # Update item state item.set_position(round(x), round(y)) # round to prevent jumping when settling item.set_parent_rect(self._rect) + def _render(self, _): + for item in self._visible_items: # Skip rendering if not in viewport if not rl.check_collision_recs(item.rect, self._rect): continue @@ -227,17 +234,17 @@ class Scroller(Widget): if scale != 1.0: rl.rl_push_matrix() rl.rl_scalef(scale, scale, 1.0) - rl.rl_translatef((1 - scale) * (x + item.rect.width / 2) / scale, - (1 - scale) * (y + item.rect.height / 2) / scale, 0) + rl.rl_translatef((1 - scale) * (item.rect.x + item.rect.width / 2) / scale, + (1 - scale) * (item.rect.y + item.rect.height / 2) / scale, 0) item.render() rl.rl_pop_matrix() else: item.render() # Draw scroll indicator - if SCROLL_BAR and not self._horizontal and len(visible_items) > 0: - _real_content_size = content_size - self._rect.height + self._txt_scroll_indicator.height - scroll_bar_y = -scroll_offset / _real_content_size * self._rect.height + if SCROLL_BAR and not self._horizontal and len(self._visible_items) > 0: + _real_content_size = self._content_size - self._rect.height + self._txt_scroll_indicator.height + scroll_bar_y = -self._scroll_offset / _real_content_size * self._rect.height scroll_bar_y = min(max(scroll_bar_y, self._rect.y), self._rect.y + self._rect.height - self._txt_scroll_indicator.height) rl.draw_texture_ex(self._txt_scroll_indicator, rl.Vector2(self._rect.x, scroll_bar_y), 0, 1.0, rl.WHITE) @@ -246,7 +253,7 @@ class Scroller(Widget): def show_event(self): super().show_event() if self._reset_scroll_at_show: - self.scroll_to(self.scroll_panel.get_offset()) + self.scroll_panel.set_offset(0.0) for item in self._items: item.show_event() diff --git a/system/ui/widgets/slider.py b/system/ui/widgets/slider.py index b17d8f3b7..455cdeef7 100644 --- a/system/ui/widgets/slider.py +++ b/system/ui/widgets/slider.py @@ -24,7 +24,7 @@ class SmallSlider(Widget): self._drag_threshold = -self._rect.width // 2 # State - self._opacity = 1.0 + self._opacity_filter = FirstOrderFilter(1.0, 0.1, 1 / gui_app.target_fps) self._confirmed_time = 0.0 self._confirm_callback_called = False # we keep dialog open by default, only call once self._start_x_circle = 0.0 @@ -54,8 +54,11 @@ class SmallSlider(Widget): self._confirmed_time = 0.0 self._confirm_callback_called = False - def set_opacity(self, opacity: float): - self._opacity = opacity + def set_opacity(self, opacity: float, smooth: bool = False): + if smooth: + self._opacity_filter.update(opacity) + else: + self._opacity_filter.x = opacity @property def slider_percentage(self): @@ -117,7 +120,7 @@ class SmallSlider(Widget): def _render(self, _): # TODO: iOS text shimmering animation - white = rl.Color(255, 255, 255, int(255 * self._opacity)) + white = rl.Color(255, 255, 255, int(255 * self._opacity_filter.x)) bg_txt_x = self._rect.x + (self._rect.width - self._bg_txt.width) / 2 bg_txt_y = self._rect.y + (self._rect.height - self._bg_txt.height) / 2 @@ -127,11 +130,11 @@ class SmallSlider(Widget): btn_y = self._rect.y + (self._rect.height - self._circle_bg_txt.height) / 2 if self._confirmed_time == 0.0 or self._scroll_x_circle > 0: - self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity))) + self._label.set_text_color(rl.Color(255, 255, 255, int(255 * 0.65 * (1.0 - self.slider_percentage) * self._opacity_filter.x))) label_rect = rl.Rectangle( self._rect.x + 20, self._rect.y, - self._rect.width - self._circle_bg_txt.width - 20 * 3, + self._rect.width - self._circle_bg_txt.width - 20 * 2.5, self._rect.height, ) self._label.render(label_rect) diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index 6de63dfbb..462dd7a36 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -3,6 +3,8 @@ #include #include +#include "cereal/services.h" + #include #include #include @@ -20,7 +22,7 @@ void DeviceStream::streamThread() { std::unique_ptr context(Context::create()); std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); - std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); + std::unique_ptr sock(SubSocket::create(context.get(), "can", address, false, true, services.at("can").queue_size)); assert(sock != NULL); // run as fast as messages come in while (!QThread::currentThread()->isInterruptionRequested()) { diff --git a/tools/joystick/joystickd.py b/tools/joystick/joystickd.py index 673a5bc1d..789dad562 100755 --- a/tools/joystick/joystickd.py +++ b/tools/joystick/joystickd.py @@ -48,6 +48,7 @@ def joystickd_thread(): if CC.longActive: actuators.accel = 4.0 * float(np.clip(joystick_axes[0], -1, 1)) actuators.longControlState = LongCtrlState.pid if sm['carState'].vEgo > CP.vEgoStopping else LongCtrlState.stopping + CC.cruiseControl.resume = actuators.accel > 0.0 if CC.latActive: max_curvature = MAX_LAT_ACCEL / max(sm['carState'].vEgo ** 2, 5) diff --git a/tools/replay/unlog_ci_segment.py b/tools/replay/unlog_ci_segment.py deleted file mode 100755 index e5a7a3ffd..000000000 --- a/tools/replay/unlog_ci_segment.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import bisect -import select -import sys -import termios -import time -import tty -from collections import defaultdict - -import cereal.messaging as messaging -from openpilot.tools.lib.framereader import FrameReader -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.openpilotci import get_url - -IGNORE = ['initData', 'sentinel'] - - -def input_ready(): - return select.select([sys.stdin], [], [], 0) == ([sys.stdin], [], []) - - -def replay(route, segment, loop): - route = route.replace('|', '/') - - lr = LogReader(get_url(route, segment, "rlog.bz2")) - fr = FrameReader(get_url(route, segment, "fcamera.hevc"), readahead=True) - - # Build mapping from frameId to segmentId from roadEncodeIdx, type == fullHEVC - msgs = [m for m in lr if m.which() not in IGNORE] - msgs = sorted(msgs, key=lambda m: m.logMonoTime) - times = [m.logMonoTime for m in msgs] - frame_idx = {m.roadEncodeIdx.frameId: m.roadEncodeIdx.segmentId for m in msgs if m.which() == 'roadEncodeIdx' and m.roadEncodeIdx.type == 'fullHEVC'} - - socks = {} - lag = 0.0 - i = 0 - max_i = len(msgs) - 2 - - while True: - msg = msgs[i].as_builder() - next_msg = msgs[i + 1] - - start_time = time.monotonic() - w = msg.which() - - if w == 'roadCameraState': - try: - img = fr.get(frame_idx[msg.roadCameraState.frameId]) - img = img[:, ::-1] # Convert RGB to BGR, which is what the camera outputs - msg.roadCameraState.image = img.flatten().tobytes() - except (KeyError, ValueError): - pass - - if w not in socks: - socks[w] = messaging.pub_sock(w) - - try: - if socks[w]: - socks[w].send(msg.to_bytes()) - except messaging.messaging_pyx.MultiplePublishersError: - socks[w] = None - - lag += (next_msg.logMonoTime - msg.logMonoTime) / 1e9 - lag -= time.monotonic() - start_time - - dt = max(lag, 0.0) - lag -= dt - time.sleep(dt) - - if lag < -1.0 and i % 1000 == 0: - print(f"{-lag:.2f} s behind") - - if input_ready(): - key = sys.stdin.read(1) - - # Handle pause - if key == " ": - while True: - if input_ready() and sys.stdin.read(1) == " ": - break - time.sleep(0.01) - - # Handle seek - dt = defaultdict(int, s=10, S=-10)[key] - new_time = msgs[i].logMonoTime + dt * 1e9 - i = bisect.bisect_left(times, new_time) - - i = (i + 1) % max_i if loop else min(i + 1, max_i) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--loop", action='store_true') - parser.add_argument("route") - parser.add_argument("segment") - args = parser.parse_args() - - orig_settings = termios.tcgetattr(sys.stdin) - tty.setcbreak(sys.stdin) - - try: - replay(args.route, args.segment, args.loop) - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings) - except Exception: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, orig_settings) - raise