mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-09 00:17:12 +08:00
Compare commits
131 Commits
accel-pers
...
paramwatch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a6e0c2781 | ||
|
|
e5e56614c9 | ||
|
|
1eb82fcc85 | ||
|
|
987f53e69a | ||
|
|
9a04a5eaae | ||
|
|
50b8ae9e09 | ||
|
|
a30fc9bcd2 | ||
|
|
19a7d1d5d7 | ||
|
|
fb8f46cba9 | ||
|
|
70386c6b00 | ||
|
|
edeede5e82 | ||
|
|
6df313b974 | ||
|
|
9442bc9aec | ||
|
|
e989633aba | ||
|
|
593a48f808 | ||
|
|
763049f068 | ||
|
|
c6c644a3a6 | ||
|
|
c876a83a31 | ||
|
|
a04a5b4284 | ||
|
|
373894a81f | ||
|
|
34ce746869 | ||
|
|
e96b0da9d7 | ||
|
|
6d7910ed74 | ||
|
|
e54ddf30b8 | ||
|
|
3093bb0b66 | ||
|
|
9c7c84bd03 | ||
|
|
6c6be573c7 | ||
|
|
8904300565 | ||
|
|
09c4b933a8 | ||
|
|
1a1178140f | ||
|
|
452aa67581 | ||
|
|
5bf2ac1657 | ||
|
|
f42dbf0c34 | ||
|
|
40f838260b | ||
|
|
f8487cae23 | ||
|
|
2e576178cb | ||
|
|
5578b7e754 | ||
|
|
57e7c0b2c1 | ||
|
|
93f98a8a36 | ||
|
|
b52d0df6e3 | ||
|
|
f3598ce3ed | ||
|
|
2b51adff11 | ||
|
|
f5b3d87e25 | ||
|
|
28fa7d5ed9 | ||
|
|
7560497f15 | ||
|
|
5999079838 | ||
|
|
2458a6d115 | ||
|
|
4bfc28dec0 | ||
|
|
be2818a131 | ||
|
|
1646fd94b8 | ||
|
|
3fbd928b98 | ||
|
|
d2125aafd4 | ||
|
|
b9c3b1219a | ||
|
|
99983d39c3 | ||
|
|
31e46f929d | ||
|
|
cecce82015 | ||
|
|
a112e6e882 | ||
|
|
c69c076acb | ||
|
|
6069c87b07 | ||
|
|
90ed6d739c | ||
|
|
95350ad854 | ||
|
|
9768109ec1 | ||
|
|
4fa4237e3f | ||
|
|
4624d8f936 | ||
|
|
bcdeec3133 | ||
|
|
545f7c6f2a | ||
|
|
507f420927 | ||
|
|
752ef8696a | ||
|
|
9e4c2bcacf | ||
|
|
9d9e5aa02d | ||
|
|
a1d0f6aa55 | ||
|
|
e7554170b8 | ||
|
|
9c2fd8d2be | ||
|
|
e2fd6f34e9 | ||
|
|
c324123e8b | ||
|
|
9f6264eca9 | ||
|
|
f77457cebc | ||
|
|
6a193ed2d8 | ||
|
|
a416de477a | ||
|
|
785e87376a | ||
|
|
7692d35a63 | ||
|
|
e0a27514e2 | ||
|
|
a3c638697f | ||
|
|
f287d487e5 | ||
|
|
7cabab69a1 | ||
|
|
716ad288bb | ||
|
|
1c135f7ff2 | ||
|
|
1504e10380 | ||
|
|
65008d281f | ||
|
|
2e8586fab5 | ||
|
|
6c5be6ddab | ||
|
|
7a324fc377 | ||
|
|
f4dea7977b | ||
|
|
e9255d1e9c | ||
|
|
350dc6a50f | ||
|
|
3206784dd8 | ||
|
|
0dd59d0404 | ||
|
|
bc07cecfa0 | ||
|
|
ce30d815f7 | ||
|
|
907bc5cf06 | ||
|
|
42e08515e6 | ||
|
|
d0ec46dc5d | ||
|
|
48a8802298 | ||
|
|
79971b9eb2 | ||
|
|
7ba21f9f1b | ||
|
|
6b4118ab27 | ||
|
|
0844424ad1 | ||
|
|
d52ce19c15 | ||
|
|
05cc9a14e2 | ||
|
|
e8ee5a23f0 | ||
|
|
3b1fddfde9 | ||
|
|
bddec6971e | ||
|
|
e2e52bcccb | ||
|
|
ccf86b7b72 | ||
|
|
a678554122 | ||
|
|
bfd3eab260 | ||
|
|
f5aedbce6e | ||
|
|
4f860dd397 | ||
|
|
f308d9ab17 | ||
|
|
9226222ad4 | ||
|
|
3e317a8b4d | ||
|
|
0eae4e0b3b | ||
|
|
37ffa5ed21 | ||
|
|
05e3eaf2fc | ||
|
|
c8fc344d68 | ||
|
|
264948e5ff | ||
|
|
9d87beac8e | ||
|
|
2e0bc80f94 | ||
|
|
4b8781886a | ||
|
|
97edff5e5c | ||
|
|
a81570a6c2 |
@@ -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
|
||||
|
||||
@@ -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,10 +174,37 @@ jobs:
|
||||
echo ' pushurl = ${{ env.LFS_PUSH_URL }}' >> .lfsconfig
|
||||
echo ' locksverify = false' >> .lfsconfig
|
||||
|
||||
- name: Push changes if there are diffs
|
||||
id: push-changes # Add an id so we can reference this step
|
||||
- name: Restore workflows from source
|
||||
run: |
|
||||
TARGET_BRANCH="${{ inputs.target_branch || env.DEFAULT_TARGET_BRANCH }}"
|
||||
SOURCE_BRANCH="${{ inputs.source_branch || env.DEFAULT_SOURCE_BRANCH }}"
|
||||
|
||||
# Ensure we are on the target branch
|
||||
git checkout $TARGET_BRANCH
|
||||
|
||||
echo "Restoring .github/workflows from $SOURCE_BRANCH"
|
||||
git checkout origin/$SOURCE_BRANCH -- .github/workflows
|
||||
|
||||
if ! git diff --cached --quiet; then
|
||||
echo "Workflows differ. Committing restoration."
|
||||
git commit -m "chore: restore .github/workflows from $SOURCE_BRANCH"
|
||||
else
|
||||
echo "Workflows match $SOURCE_BRANCH."
|
||||
fi
|
||||
|
||||
- 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
|
||||
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 +216,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
|
||||
|
||||
10
.github/workflows/tests.yaml
vendored
10
.github/workflows/tests.yaml
vendored
@@ -108,6 +108,7 @@ jobs:
|
||||
build_mac:
|
||||
name: build macOS
|
||||
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' }}
|
||||
if: false # There'll be one day that this works. That day is not today.
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -115,14 +116,13 @@ jobs:
|
||||
- run: echo "CACHE_COMMIT_DATE=$(git log -1 --pretty='format:%cd' --date=format:'%Y-%m-%d-%H:%M')" >> $GITHUB_ENV
|
||||
- name: Homebrew cache
|
||||
uses: ./.github/workflows/auto-cache
|
||||
if: false # disabling the cache for now because it is breaking macos builds...
|
||||
with:
|
||||
save: false # No need save here if we manually save it later conditionally
|
||||
path: ~/Library/Caches/Homebrew
|
||||
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
|
||||
key: brew-macos-${{ hashFiles('tools/Brewfile') }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
brew-macos-${{ env.CACHE_COMMIT_DATE }}
|
||||
brew-macos
|
||||
brew-macos-${{ hashFiles('tools/Brewfile') }}
|
||||
brew-macos-
|
||||
- name: Install dependencies
|
||||
run: ./tools/mac_setup.sh
|
||||
env:
|
||||
@@ -133,7 +133,7 @@ jobs:
|
||||
if: github.ref == 'refs/heads/master'
|
||||
with:
|
||||
path: ~/Library/Caches/Homebrew
|
||||
key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }}
|
||||
key: brew-macos-${{ hashFiles('tools/Brewfile') }}-${{ github.sha }}
|
||||
- run: git lfs pull
|
||||
- name: Getting scons cache
|
||||
uses: ./.github/workflows/auto-cache
|
||||
|
||||
60
README.md
60
README.md
@@ -11,66 +11,10 @@ Join the official sunnypilot community forum to stay up to date with all the lat
|
||||
https://docs.sunnypilot.ai/ is your one stop shop for everything from features to installation to FAQ about the sunnypilot
|
||||
|
||||
## 🚘 Running on a dedicated device in a car
|
||||
* A supported device to run this software
|
||||
* a [comma three](https://comma.ai/shop/products/three) or a [C3X](https://comma.ai/shop/comma-3x)
|
||||
* This software
|
||||
* One of [the 325+ supported cars](https://github.com/sunnypilot/sunnypilot/blob/master/docs/CARS.md). We support Honda, Toyota, Hyundai, Nissan, Kia, Chrysler, Lexus, Acura, Audi, VW, Ford, and more. If your car is not supported but has adaptive cruise control and lane-keeping assist, it's likely able to run sunnypilot.
|
||||
* A [car harness](https://comma.ai/shop/products/car-harness) to connect to your car
|
||||
|
||||
Detailed instructions for [how to mount the device in a car](https://comma.ai/setup).
|
||||
First, check out this list of items you'll need to [get started](https://community.sunnypilot.ai/t/getting-started-using-sunnypilot-in-your-supported-car/251).
|
||||
|
||||
## Installation
|
||||
Please refer to [Recommended Branches](#recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging` branch.
|
||||
|
||||
### If you want to use our newest branches (our rewrite)
|
||||
> [!TIP]
|
||||
>You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links
|
||||
|
||||
* sunnypilot not installed or you installed a version before 0.8.17?
|
||||
1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed.
|
||||
2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option.
|
||||
3. Input the installation URL per [Recommended Branches](#recommended-branches). Example: ```https://staging.sunnypilot.ai```.
|
||||
4. Complete the rest of the installation following the onscreen instructions.
|
||||
|
||||
* sunnypilot already installed and you installed a version after 0.8.17?
|
||||
1. On the comma three/3X, go to `Settings` ▶️ `Software`.
|
||||
2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot.
|
||||
3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector.
|
||||
4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging`
|
||||
|
||||
### Recommended Branches
|
||||
| Branch | Installation URL |
|
||||
|:---------------:|:---------------------------------------------:|
|
||||
| `release` | `https://release.sunnypilot.ai` |
|
||||
| `staging` | `https://staging.sunnypilot.ai` |
|
||||
| `dev` | `https://dev.sunnypilot.ai` |
|
||||
| `custom-branch` | `https://install.sunnypilot.ai/{branch_name}` |
|
||||
|
||||
> [!TIP]
|
||||
> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging'.
|
||||
|
||||
> [!NOTE]
|
||||
> Do you require further assistance with software installation? Join the [sunnypilot community forum](https://community.sunnypilot.ai/new-topic?category=general/qa) and create a topic in the General/Q&A Category channel.
|
||||
|
||||
|
||||
<details>
|
||||
|
||||
<summary>Older legacy branches</summary>
|
||||
|
||||
### If you want to use our older legacy branches (*not recommended*)
|
||||
|
||||
> [**IMPORTANT**]
|
||||
> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches.
|
||||
> You can still restore the latest sunnylink backup made on the old branches.
|
||||
|
||||
| Branch | Installation URL |
|
||||
|:------------:|:--------------------------------:|
|
||||
| `release-c3` | https://release-c3.sunnypilot.ai |
|
||||
| `staging-c3` | https://staging-c3.sunnypilot.ai |
|
||||
| `dev-c3` | https://dev-c3.sunnypilot.ai |
|
||||
|
||||
</details>
|
||||
|
||||
Next, refer to the sunnypilot community forum for [installation instructions](https://community.sunnypilot.ai/t/read-before-installing-sunnypilot/254), as well as a complete list of [Recommended Branch Installations](https://community.sunnypilot.ai/t/recommended-branch-installations/235).
|
||||
|
||||
## 🎆 Pull Requests
|
||||
We welcome both pull requests and issues on GitHub. Bug fixes are encouraged.
|
||||
|
||||
@@ -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)
|
||||
========================
|
||||
|
||||
@@ -192,7 +192,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
aTarget @5 :Float32;
|
||||
events @6 :List(OnroadEventSP.Event);
|
||||
e2eAlerts @7 :E2eAlerts;
|
||||
accelPersonality @8 :AccelerationPersonality;
|
||||
|
||||
struct DynamicExperimentalControl {
|
||||
state @0 :DynamicExperimentalControlState;
|
||||
@@ -204,11 +203,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
|
||||
blended @1;
|
||||
}
|
||||
}
|
||||
enum AccelerationPersonality {
|
||||
sport @0;
|
||||
normal @1;
|
||||
eco @2;
|
||||
}
|
||||
|
||||
struct SmartCruiseControl {
|
||||
vision @0 :Vision;
|
||||
map @1 :Map;
|
||||
|
||||
@@ -2524,13 +2524,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;
|
||||
@@ -2693,5 +2690,8 @@ struct Event {
|
||||
liveLocationKalman @72 :LiveLocationKalman;
|
||||
liveTracksDEPRECATED @16 :List(LiveTracksDEPRECATED);
|
||||
onroadEventsDEPRECATED @68: List(Car.OnroadEventDEPRECATED);
|
||||
gyroscope2DEPRECATED @100 :SensorEventData;
|
||||
accelerometer2DEPRECATED @101 :SensorEventData;
|
||||
temperatureSensor2DEPRECATED @123 :SensorEventData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ SubMaster::SubMaster(const std::vector<const char *> &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<const char *> &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;
|
||||
}
|
||||
|
||||
@@ -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 <map>\n"
|
||||
h += "#include <string>\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<std::string, service> 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"
|
||||
|
||||
@@ -133,19 +133,19 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"Version", {PERSISTENT, STRING}},
|
||||
|
||||
// --- sunnypilot params --- //
|
||||
{"AccelPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast<int>(cereal::LongitudinalPlanSP::AccelerationPersonality::NORMAL))}},
|
||||
{"AccelPersonalityEnabled", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"ApiCache_DriveStats", {PERSISTENT, JSON}},
|
||||
{"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
{"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}},
|
||||
{"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}},
|
||||
{"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}},
|
||||
{"CompletedSunnylinkConsentVersion", {PERSISTENT, STRING, "0"}},
|
||||
{"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}},
|
||||
{"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}},
|
||||
@@ -155,6 +155,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||
{"HideVEgoUI", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}},
|
||||
{"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}},
|
||||
@@ -214,16 +215,19 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
{"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
|
||||
// sunnypilot model params
|
||||
{"CameraOffset", {PERSISTENT | BACKUP, FLOAT, "0.0"}},
|
||||
{"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}},
|
||||
{"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}},
|
||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||
|
||||
// mapd
|
||||
{"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}},
|
||||
|
||||
@@ -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
|
||||
|
||||
245
docs/CARS.md
245
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|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2016-18">Buy Here</a></sub></details>|||
|
||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|
||||
|Acura|TLX 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura TLX 2021">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 2014-19">Buy Here</a></sub></details>|||
|
||||
|Audi|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|
||||
|Audi|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q2 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi Q3 2019-24">Buy Here</a></sub></details>|||
|
||||
|Audi|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi RS3 2018">Buy Here</a></sub></details>|||
|
||||
|Audi|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Audi S3 2015-17">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|Bolt EV Non-ACC 2017|Adaptive Cruise Control (ACC)|Stock|24 mph|7 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV Non-ACC 2017">Buy Here</a></sub></details>|||
|
||||
@@ -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|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chrysler Pacifica Hybrid 2019-25">Buy Here</a></sub></details>|||
|
||||
|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|CUPRA|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|
||||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
||||
@@ -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|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Focus 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus Hybrid 2018[<sup>3</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus Hybrid 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
@@ -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|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|14 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>5</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
@@ -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[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2019-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus GS F 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2022-24">Buy Here</a></sub></details>|||
|
||||
|Lexus|LC 2024-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus LC 2024-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2018-19|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX 2020-21">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX Hybrid 2018-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX Hybrid 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX Hybrid 2018-19|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX Hybrid 2018-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|NX Hybrid 2020-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus NX Hybrid 2020-21">Buy Here</a></sub></details>|||
|
||||
|Lexus|RC 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RC 2018-20">Buy Here</a></sub></details>|||
|
||||
|Lexus|RC 2023|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RC 2023">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX 2016|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX 2016|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX 2017-19|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX Hybrid 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX Hybrid 2016|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX Hybrid 2017-19|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|RX Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus RX Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus UX Hybrid 2019-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator 2020-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator Plug-in Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-20, 2024">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>6</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-20, 2024">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|
||||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>12,14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda|Karoq 2019-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda|Kodiaq 2017-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia 2015-19[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia RS 2016[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia Scout 2017-19[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Scala 2020-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda|Superb 2015-22[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|SEAT|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Ateca 2016-23">Buy Here</a></sub></details>|||
|
||||
|SEAT|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Legacy 2020-22|All[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>11,13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Škoda|Karoq 2019-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Karoq 2019-23">Buy Here</a></sub></details>|||
|
||||
|Škoda|Kodiaq 2017-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kodiaq 2017-23">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia 2015-19[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia 2015-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia RS 2016[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia RS 2016">Buy Here</a></sub></details>|||
|
||||
|Škoda|Octavia Scout 2017-19[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|
||||
|Škoda|Scala 2020-23[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Scala 2020-23">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Škoda|Superb 2015-22[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Superb 2015-22">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla A connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Tesla B connector<br>- 1 USB-C coupler<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard 2019-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2019-21|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2019-21|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon 2022|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon 2022">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon Hybrid 2019-21|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon Hybrid 2019-21|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|Avalon Hybrid 2022|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Avalon Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR 2017-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2017-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2021-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>11</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>11</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross (Non-US only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross Hybrid (Non-US only) 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid (South America only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2017-19|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Mirai 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2017-20|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|openpilot available[<sup>2</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius v 2017|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2022">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2023-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Sienna 2018-20|All|openpilot available[<sup>2</sup>](#footnotes)|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas Cross Sport 2020-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen California 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Caravelle 2020">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Golf 2014-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf Alltrack 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTI 2015-21">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Cross 2021">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Atlas Cross Sport 2020-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen California 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Caravelle 2020">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Golf 2014-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf Alltrack 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTD 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTE 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf GTI 2015-21">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf R 2015-19">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Golf SportsVan 2015-20">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen|Jetta 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>12</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Passat GTE 2015-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo 2018-23">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Cross 2021">Buy Here</a></sub></details>[<sup>15</sup>](#footnotes)|||
|
||||
|Volkswagen|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen T-Roc 2018-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont 2018-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Teramont X 2021-22">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,14</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 USB-C coupler<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|
||||
|
||||
### Footnotes
|
||||
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`. <br />
|
||||
<sup>2</sup>By 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. <b><i>NOTE: disconnecting the DSU disables Automatic Emergency Braking (AEB).</i></b> <br />
|
||||
<sup>3</sup>Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia. <br />
|
||||
<sup>4</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <br />
|
||||
<sup>5</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
|
||||
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>12</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>13</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>14</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>15</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>16</sup>Model-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. <br />
|
||||
<sup>2</sup>Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia. <br />
|
||||
<sup>3</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <br />
|
||||
<sup>4</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
|
||||
<sup>5</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||
<sup>6</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||
<sup>7</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||
<sup>8</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
|
||||
<sup>9</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||
<sup>10</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
|
||||
<sup>11</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||
<sup>12</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||
<sup>13</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||
<sup>14</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
|
||||
<sup>15</sup>Model-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. <br />
|
||||
|
||||
## 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/).
|
||||
|
||||
Submodule msgq_repo updated: a16cf1f608...6abe47bc98
Submodule opendbc_repo updated: f746884333...e03fbf9be8
Binary file not shown.
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/driver_monitoring/dm_check.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/driver_monitoring/dm_question.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/orange_dm.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/orange_dm.png
LFS
Normal file
Binary file not shown.
BIN
selfdrive/assets/icons_mici/setup/small_button_disabled.png
LFS
Normal file
BIN
selfdrive/assets/icons_mici/setup/small_button_disabled.png
LFS
Normal file
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,8 +10,6 @@ from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.modeld.constants import index_function
|
||||
from openpilot.selfdrive.controls.radard import _LEAD_ACCEL_TAU
|
||||
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_controller import AccelPersonalityController
|
||||
|
||||
if __name__ == '__main__': # generating code
|
||||
from openpilot.third_party.acados.acados_template import AcadosModel, AcadosOcp, AcadosOcpSolver
|
||||
else:
|
||||
@@ -230,7 +228,6 @@ class LongitudinalMpc:
|
||||
self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
self.reset()
|
||||
self.source = SOURCES[2]
|
||||
self.accel_controller = AccelPersonalityController()
|
||||
|
||||
def reset(self):
|
||||
# self.solver = AcadosOcpSolverCython(MODEL_NAME, ACADOS_SOLVER_TYPE, N)
|
||||
@@ -335,13 +332,6 @@ class LongitudinalMpc:
|
||||
v_ego = self.x0[1]
|
||||
self.status = radarstate.leadOne.status or radarstate.leadTwo.status
|
||||
|
||||
|
||||
if self.accel_controller.is_enabled():
|
||||
min_accel = self.accel_controller.get_min_accel(v_ego)
|
||||
else:
|
||||
min_accel = CRUISE_MIN_ACCEL
|
||||
a_cruise_min = min_accel
|
||||
|
||||
lead_xv_0 = self.process_lead(radarstate.leadOne)
|
||||
lead_xv_1 = self.process_lead(radarstate.leadTwo)
|
||||
|
||||
@@ -360,7 +350,7 @@ class LongitudinalMpc:
|
||||
|
||||
# Fake an obstacle for cruise, this ensures smooth acceleration to set speed
|
||||
# when the leads are no factor.
|
||||
v_lower = v_ego + (T_IDXS * a_cruise_min * 1.05)
|
||||
v_lower = v_ego + (T_IDXS * CRUISE_MIN_ACCEL * 1.05)
|
||||
# TODO does this make sense when max_a is negative?
|
||||
v_upper = v_ego + (T_IDXS * CRUISE_MAX_ACCEL * 1.05)
|
||||
v_cruise_clipped = np.clip(v_cruise * np.ones(N+1),
|
||||
|
||||
@@ -124,11 +124,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP):
|
||||
prev_accel_constraint = not (reset_state or sm['carState'].standstill)
|
||||
|
||||
if mode == 'acc':
|
||||
if self.accel_controller.is_enabled():
|
||||
max_accel = self.accel_controller.get_max_accel(v_ego)
|
||||
accel_clip = [ACCEL_MIN, max_accel]
|
||||
else:
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
accel_clip = [ACCEL_MIN, get_max_accel(v_ego)]
|
||||
steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg
|
||||
accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP)
|
||||
else:
|
||||
|
||||
82
selfdrive/debug/analyze-msg-size.py
Executable file
82
selfdrive/debug/analyze-msg-size.py
Executable file
@@ -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")
|
||||
@@ -449,7 +449,7 @@ class DriverMonitoring:
|
||||
rpyCalib = [0., 0., 0.]
|
||||
else:
|
||||
highway_speed = sm['carState'].vEgo
|
||||
enabled = sm['selfdriveState'].enabled
|
||||
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
|
||||
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
|
||||
standstill = sm['carState'].standstill
|
||||
driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from cereal import log
|
||||
from cereal import log, car
|
||||
from openpilot.common.realtime import DT_DMON
|
||||
from openpilot.selfdrive.monitoring.helpers import DriverMonitoring, DRIVER_MONITOR_SETTINGS
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
@@ -204,3 +205,66 @@ class TestMonitoring:
|
||||
assert EventName.driverUnresponsive in \
|
||||
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
|
||||
|
||||
|
||||
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
|
||||
(False, False, False), # Both Disabled
|
||||
(True, False, True), # OP Enabled, Lat Inactive
|
||||
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
|
||||
(True, True, True) # Both Active
|
||||
])
|
||||
def test_enabled_states(enabled_state, lat_active_state, expected):
|
||||
"""
|
||||
Test DriverMonitoring.run_step with all 4 combinations of:
|
||||
- selfdriveState.enabled (True/False)
|
||||
- carControl.latActive (True/False)
|
||||
"""
|
||||
cs = car.CarState.new_message()
|
||||
cs.vEgo = 30.0
|
||||
cs.gearShifter = car.CarState.GearShifter.drive
|
||||
cs.standstill = False
|
||||
cs.steeringPressed = False
|
||||
cs.gasPressed = False
|
||||
|
||||
ss = log.SelfdriveState.new_message()
|
||||
ss.enabled = enabled_state
|
||||
|
||||
cc = car.CarControl.new_message()
|
||||
cc.latActive = lat_active_state
|
||||
|
||||
mv2 = log.ModelDataV2.new_message()
|
||||
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
|
||||
|
||||
lc = log.LiveCalibrationData.new_message()
|
||||
lc.rpyCalib = [0.0, 0.0, 0.0]
|
||||
|
||||
ds = make_msg(False)
|
||||
|
||||
sm = {
|
||||
'carState': cs,
|
||||
'selfdriveState': ss,
|
||||
'carControl': cc,
|
||||
'modelV2': mv2,
|
||||
'liveCalibration': lc,
|
||||
'driverStateV2': ds
|
||||
}
|
||||
|
||||
driver_monitoring = DriverMonitoring()
|
||||
|
||||
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
|
||||
captured_args = []
|
||||
original_update_events = driver_monitoring._update_events
|
||||
|
||||
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||
captured_args.append(op_engaged)
|
||||
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
|
||||
|
||||
driver_monitoring._update_events = spy_update_events
|
||||
|
||||
driver_monitoring.run_step(sm, demo=False)
|
||||
|
||||
# Assertion
|
||||
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
|
||||
actual_enabled = captured_args[0]
|
||||
|
||||
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
|
||||
|
||||
|
||||
@@ -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<Panda *> pandas, bool fake_send) {
|
||||
|
||||
AlignedBuffer aligned_buf;
|
||||
std::unique_ptr<Context> context(Context::create());
|
||||
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "sendcan"));
|
||||
std::unique_ptr<SubSocket> subscriber(SubSocket::create(context.get(), "sendcan", "127.0.0.1", false, true, services.at("sendcan").queue_size));
|
||||
assert(subscriber != NULL);
|
||||
subscriber->setTimeout(100);
|
||||
|
||||
|
||||
@@ -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(
|
||||
@@ -901,7 +892,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,
|
||||
|
||||
@@ -1 +1 @@
|
||||
e0ad86508edb61b3eaa1b84662c515d2c3368295
|
||||
b508f43fb0481bce0859c9b6ab4f45ee690b8dab
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -11,7 +11,9 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkOnboarding
|
||||
|
||||
DEBUG = False
|
||||
|
||||
@@ -33,6 +35,7 @@ class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
SUNNYLINK_CONSENT = 3
|
||||
|
||||
|
||||
class TrainingGuide(Widget):
|
||||
@@ -110,14 +113,14 @@ class TermsPage(Widget):
|
||||
self._on_decline = on_decline
|
||||
|
||||
self._title = Label(tr("Welcome to sunnypilot"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._desc = Label(tr("You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at https://comma.ai/terms before continuing."),
|
||||
self._desc = Label(tr("You must accept the Terms of Service to use sunnypilot. Read the latest terms at https://sunnypilot.ai/terms before continuing."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._decline_btn = Button(tr("Decline"), click_callback=on_decline)
|
||||
self._accept_btn = Button(tr("Agree"), button_style=ButtonStyle.PRIMARY, click_callback=on_accept)
|
||||
|
||||
def _render(self, _):
|
||||
welcome_x = self._rect.x + 165
|
||||
welcome_x = self._rect.x + 95
|
||||
welcome_y = self._rect.y + 165
|
||||
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||
self._title.render(welcome_rect)
|
||||
@@ -143,7 +146,7 @@ class TermsPage(Widget):
|
||||
class DeclinePage(Widget):
|
||||
def __init__(self, back_callback=None):
|
||||
super().__init__()
|
||||
self._text = Label(tr("You must accept the Terms and Conditions in order to use sunnypilot."),
|
||||
self._text = Label(tr("You must accept the Terms of Service in order to use sunnypilot."),
|
||||
font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
self._back_btn = Button(tr("Back"), click_callback=back_callback)
|
||||
self._uninstall_btn = Button(tr("Decline, uninstall sunnypilot"), button_style=ButtonStyle.DANGER,
|
||||
@@ -180,9 +183,21 @@ class OnboardingWindow(Widget):
|
||||
self._training_guide: TrainingGuide | None = None
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
# sunnylink consent pages
|
||||
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
self._sunnylink = SunnylinkOnboarding()
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self._accepted_terms and self._training_done
|
||||
return self._accepted_terms and self._sunnylink.completed and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
@@ -192,8 +207,12 @@ class OnboardingWindow(Widget):
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
if self._training_done:
|
||||
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
if not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def _on_completed_training(self):
|
||||
@@ -206,8 +225,18 @@ class OnboardingWindow(Widget):
|
||||
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
if self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
|
||||
self._sunnylink.render(self._rect)
|
||||
if self._sunnylink.completed:
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
gui_app.set_modal_overlay(None)
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
|
||||
@@ -9,6 +9,8 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.sidebar import SidebarSP
|
||||
|
||||
SIDEBAR_WIDTH = 300
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_WIDTH = 240
|
||||
@@ -62,9 +64,10 @@ class MetricData:
|
||||
self.color = color
|
||||
|
||||
|
||||
class Sidebar(Widget):
|
||||
class Sidebar(Widget, SidebarSP):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
Widget.__init__(self)
|
||||
SidebarSP.__init__(self)
|
||||
self._net_type = NETWORK_TYPES.get(NetworkType.none)
|
||||
self._net_strength = 0
|
||||
|
||||
@@ -112,6 +115,7 @@ class Sidebar(Widget):
|
||||
self._update_temperature_status(device_state)
|
||||
self._update_connection_status(device_state)
|
||||
self._update_panda_status()
|
||||
SidebarSP._update_sunnylink_status(self)
|
||||
|
||||
def _update_network_status(self, device_state):
|
||||
self._net_type = NETWORK_TYPES.get(device_state.networkType.raw, tr_noop("Unknown"))
|
||||
@@ -200,6 +204,13 @@ class Sidebar(Widget):
|
||||
rl.draw_text_ex(self._font_regular, tr(self._net_type), text_pos, FONT_SIZE, 0, Colors.WHITE)
|
||||
|
||||
def _draw_metrics(self, rect: rl.Rectangle):
|
||||
if gui_app.sunnypilot_ui():
|
||||
metrics, start_y, spacing = SidebarSP._draw_metrics_w_sunnylink(self, rect, self._temp_status, self._panda_status, self._connect_status)
|
||||
for idx, metric in enumerate(metrics):
|
||||
self._draw_metric(rect, metric, start_y + idx * spacing)
|
||||
|
||||
return
|
||||
|
||||
metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)]
|
||||
|
||||
for metric, y_offset in metrics:
|
||||
|
||||
@@ -109,7 +109,7 @@ class MiciHomeLayout(Widget):
|
||||
self._cell_high_txt = gui_app.texture("icons_mici/settings/network/cell_strength_high.png", 55, 35)
|
||||
self._cell_full_txt = gui_app.texture("icons_mici/settings/network/cell_strength_full.png", 55, 35)
|
||||
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=96, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.DISPLAY)
|
||||
self._openpilot_label = MiciLabel("sunnypilot", font_size=90, color=rl.Color(255, 255, 255, int(255 * 0.9)), font_weight=FontWeight.AUDIOWIDE)
|
||||
self._version_label = MiciLabel("", font_size=36, font_weight=FontWeight.ROMAN)
|
||||
self._large_version_label = MiciLabel("", font_size=64, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
self._date_label = MiciLabel("", font_size=36, color=rl.GRAY, font_weight=FontWeight.ROMAN)
|
||||
|
||||
@@ -11,6 +11,9 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -14,21 +17,25 @@ from openpilot.selfdrive.ui.mici.onroad.driver_state import DriverStateRenderer
|
||||
from openpilot.selfdrive.ui.mici.onroad.driver_camera_dialog import DriverCameraDialog
|
||||
from openpilot.system.ui.widgets.label import gui_label
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkOnboarding
|
||||
|
||||
|
||||
class OnboardingState(IntEnum):
|
||||
TERMS = 0
|
||||
ONBOARDING = 1
|
||||
DECLINE = 2
|
||||
SUNNYLINK_CONSENT = 3
|
||||
|
||||
|
||||
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 +48,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 +95,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 +150,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 +348,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
|
||||
@@ -282,10 +415,10 @@ class TermsPage(SetupTermsPage):
|
||||
super().__init__(on_accept, on_decline, "decline")
|
||||
|
||||
info_txt = gui_app.texture("icons_mici/setup/green_info.png", 60, 60)
|
||||
self._title_header = TermsHeader("terms & conditions", info_txt)
|
||||
self._title_header = TermsHeader("terms of service", info_txt)
|
||||
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms and Conditions to use sunnypilot. " +
|
||||
"Read the latest terms at https://comma.ai/terms before continuing.", 36,
|
||||
self._terms_label = UnifiedLabel("You must accept the Terms of Service to use sunnypilot. " +
|
||||
"Read the latest terms at https://sunnypilot.ai/terms before continuing.", 36,
|
||||
FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
@@ -319,9 +452,29 @@ class OnboardingWindow(Widget):
|
||||
self._training_guide = TrainingGuide(completed_callback=self._on_completed_training)
|
||||
self._decline_page = DeclinePage(back_callback=self._on_decline_back)
|
||||
|
||||
# sunnylink consent pages
|
||||
self._accepted_terms = self._accepted_terms and ui_state.params.get("HasAcceptedTermsSP") == terms_version_sp
|
||||
self._sunnylink = SunnylinkOnboarding()
|
||||
if not self._accepted_terms:
|
||||
self._state = OnboardingState.TERMS
|
||||
elif not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
|
||||
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
|
||||
return self._accepted_terms and self._sunnylink.completed and self._training_done
|
||||
|
||||
def _on_terms_declined(self):
|
||||
self._state = OnboardingState.DECLINE
|
||||
@@ -335,7 +488,13 @@ class OnboardingWindow(Widget):
|
||||
|
||||
def _on_terms_accepted(self):
|
||||
ui_state.params.put("HasAcceptedTerms", terms_version)
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
ui_state.params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
if not self._sunnylink.completed:
|
||||
self._state = OnboardingState.SUNNYLINK_CONSENT
|
||||
elif not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
|
||||
def _on_completed_training(self):
|
||||
ui_state.params.put("CompletedTrainingVersion", training_version)
|
||||
@@ -344,8 +503,18 @@ class OnboardingWindow(Widget):
|
||||
def _render(self, _):
|
||||
if self._state == OnboardingState.TERMS:
|
||||
self._terms.render(self._rect)
|
||||
elif self._state == OnboardingState.SUNNYLINK_CONSENT:
|
||||
self._sunnylink.render(self._rect)
|
||||
if self._sunnylink.completed:
|
||||
if not self._training_done:
|
||||
self._state = OnboardingState.ONBOARDING
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.ONBOARDING:
|
||||
self._training_guide.render(self._rect)
|
||||
if not self._training_done:
|
||||
self._training_guide.render(self._rect)
|
||||
else:
|
||||
self.close()
|
||||
elif self._state == OnboardingState.DECLINE:
|
||||
self._decline_page.render(self._rect)
|
||||
return -1
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -6,6 +6,8 @@ from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.confidence_ball import ConfidenceBallSP
|
||||
|
||||
|
||||
def draw_circle_gradient(center_x: float, center_y: float, radius: int,
|
||||
top: rl.Color, bottom: rl.Color) -> None:
|
||||
@@ -21,9 +23,10 @@ def draw_circle_gradient(center_x: float, center_y: float, radius: int,
|
||||
20, rl.BLACK)
|
||||
|
||||
|
||||
class ConfidenceBall(Widget):
|
||||
class ConfidenceBall(Widget, ConfidenceBallSP):
|
||||
def __init__(self, demo: bool = False):
|
||||
super().__init__()
|
||||
Widget.__init__(self)
|
||||
ConfidenceBallSP.__init__(self)
|
||||
self._demo = demo
|
||||
self._confidence_filter = FirstOrderFilter(-0.5, 0.5, 1 / gui_app.target_fps)
|
||||
|
||||
@@ -37,6 +40,8 @@ class ConfidenceBall(Widget):
|
||||
# animate status dot in from bottom
|
||||
if ui_state.status == UIStatus.DISENGAGED:
|
||||
self._confidence_filter.update(-0.5)
|
||||
elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY):
|
||||
self._confidence_filter.update(1 - max(self.get_animate_status_probs() or [1]))
|
||||
else:
|
||||
self._confidence_filter.update((1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs or [1])) *
|
||||
(1 - max(ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs or [1])))
|
||||
@@ -65,6 +70,9 @@ class ConfidenceBall(Widget):
|
||||
top_dot_color = rl.Color(255, 0, 21, 255)
|
||||
bottom_dot_color = rl.Color(255, 0, 89, 255)
|
||||
|
||||
elif ui_state.status in (UIStatus.LAT_ONLY, UIStatus.LONG_ONLY):
|
||||
top_dot_color = bottom_dot_color = self.get_lat_long_dot_color()
|
||||
|
||||
elif ui_state.status == UIStatus.OVERRIDE:
|
||||
top_dot_color = rl.Color(255, 255, 255, 255)
|
||||
bottom_dot_color = rl.Color(82, 82, 82, 255)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,6 +12,8 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.model_renderer import LANE_LINE_COLORS_SP
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
@@ -32,6 +34,7 @@ LANE_LINE_COLORS = {
|
||||
UIStatus.DISENGAGED: rl.Color(200, 200, 200, 255),
|
||||
UIStatus.OVERRIDE: rl.Color(255, 255, 255, 255),
|
||||
UIStatus.ENGAGED: rl.Color(0, 255, 64, 255),
|
||||
**LANE_LINE_COLORS_SP,
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +80,9 @@ class ModelRenderer(Widget):
|
||||
self._transform_dirty = True
|
||||
self._clip_region = None
|
||||
|
||||
self._counter = -1
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
|
||||
self._exp_gradient = Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
@@ -96,6 +102,10 @@ class ModelRenderer(Widget):
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
sm = ui_state.sm
|
||||
|
||||
if self._counter % 180 == 0: # This runs at 60fps, so we query every 3 seconds
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
self._counter += 1
|
||||
|
||||
self._torque_filter.update(-ui_state.sm['carOutput'].actuatorsOutput.torque)
|
||||
|
||||
# Check if data is up-to-date
|
||||
@@ -147,13 +157,13 @@ class ModelRenderer(Widget):
|
||||
|
||||
def _update_raw_points(self, model):
|
||||
"""Update raw 3D points from model data"""
|
||||
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
|
||||
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
|
||||
|
||||
for i, lane_line in enumerate(model.laneLines):
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
|
||||
|
||||
for i, road_edge in enumerate(model.roadEdges):
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
|
||||
|
||||
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
|
||||
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
@@ -171,7 +181,7 @@ class ModelRenderer(Widget):
|
||||
|
||||
# Get z-coordinate from path at the lead vehicle position
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
|
||||
@@ -185,13 +185,13 @@ class TorqueBar(Widget):
|
||||
|
||||
# animate alpha and angle span
|
||||
if not self._demo:
|
||||
self._torque_line_alpha_filter.update(ui_state.status != UIStatus.DISENGAGED)
|
||||
self._torque_line_alpha_filter.update(ui_state.status not in (UIStatus.DISENGAGED, UIStatus.LONG_ONLY))
|
||||
else:
|
||||
self._torque_line_alpha_filter.update(1.0)
|
||||
|
||||
torque_line_bg_alpha = np.interp(abs(self._torque_filter.x), [0.5, 1.0], [0.25, 0.5])
|
||||
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * torque_line_bg_alpha * self._torque_line_alpha_filter.x))
|
||||
if ui_state.status != UIStatus.ENGAGED and not self._demo:
|
||||
if ui_state.status not in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) and not self._demo:
|
||||
torque_line_bg_color = rl.Color(255, 255, 255, int(255 * 0.15 * self._torque_line_alpha_filter.x))
|
||||
|
||||
# draw curved line polygon torque bar
|
||||
@@ -234,7 +234,7 @@ class TorqueBar(Widget):
|
||||
max(0, abs(self._torque_filter.x) - 0.75) * 4,
|
||||
)
|
||||
|
||||
if ui_state.status != UIStatus.ENGAGED and not self._demo:
|
||||
if ui_state.status not in (UIStatus.ENGAGED, UIStatus.LAT_ONLY) and not self._demo:
|
||||
start_color = end_color = rl.Color(255, 255, 255, int(255 * 0.35 * self._torque_line_alpha_filter.x))
|
||||
|
||||
gradient = Gradient(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,12 @@ 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
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.driver_state import DriverStateRendererSP as DriverStateRenderer
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.augmented_road_view import BORDER_COLORS_SP
|
||||
|
||||
OpState = log.SelfdriveState.OpenpilotState
|
||||
CALIBRATED = log.LiveCalibrationData.Status.calibrated
|
||||
ROAD_CAM = VisionStreamType.VISION_STREAM_ROAD
|
||||
@@ -24,6 +30,7 @@ BORDER_COLORS = {
|
||||
UIStatus.DISENGAGED: rl.Color(0x12, 0x28, 0x39, 0xFF), # Blue for disengaged state
|
||||
UIStatus.OVERRIDE: rl.Color(0x89, 0x92, 0x8D, 0xFF), # Gray for override state
|
||||
UIStatus.ENGAGED: rl.Color(0x16, 0x7F, 0x40, 0xFF), # Green for engaged state
|
||||
**BORDER_COLORS_SP,
|
||||
}
|
||||
|
||||
WIDE_CAM_MAX_SPEED = 10.0 # m/s (22 mph)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -11,6 +11,8 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.model_renderer import ChevronMetrics, ModelRendererSP
|
||||
|
||||
CLIP_MARGIN = 500
|
||||
MIN_DRAW_DISTANCE = 10.0
|
||||
MAX_DRAW_DISTANCE = 100.0
|
||||
@@ -41,9 +43,11 @@ class LeadVehicle:
|
||||
fill_alpha: int = 0
|
||||
|
||||
|
||||
class ModelRenderer(Widget):
|
||||
class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
Widget.__init__(self)
|
||||
ChevronMetrics.__init__(self)
|
||||
ModelRendererSP.__init__(self)
|
||||
self._longitudinal_control = False
|
||||
self._experimental_mode = False
|
||||
self._blend_filter = FirstOrderFilter(1.0, 0.25, 1 / gui_app.target_fps)
|
||||
@@ -52,7 +56,8 @@ class ModelRenderer(Widget):
|
||||
self._road_edge_stds = np.zeros(2, dtype=np.float32)
|
||||
self._lead_vehicles = [LeadVehicle(), LeadVehicle()]
|
||||
self._path_offset_z = HEIGHT_INIT[0]
|
||||
|
||||
self._counter = -1
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
# Initialize ModelPoints objects
|
||||
self._path = ModelPoints()
|
||||
self._lane_lines = [ModelPoints() for _ in range(4)]
|
||||
@@ -99,6 +104,10 @@ class ModelRenderer(Widget):
|
||||
live_calib = sm['liveCalibration']
|
||||
self._path_offset_z = live_calib.height[0] if live_calib.height else HEIGHT_INIT[0]
|
||||
|
||||
if self._counter % 60 == 0:
|
||||
self._camera_offset = ui_state.params.get("CameraOffset", return_default=True) if ui_state.active_bundle else 0.0
|
||||
self._counter += 1
|
||||
|
||||
if sm.updated['carParams']:
|
||||
self._longitudinal_control = sm['carParams'].openpilotLongitudinalControl
|
||||
|
||||
@@ -128,16 +137,17 @@ class ModelRenderer(Widget):
|
||||
|
||||
if render_lead_indicator and radar_state:
|
||||
self._draw_lead_indicator()
|
||||
self.chevron_metrics.draw_lead_status(sm, radar_state, self._rect, self._lead_vehicles)
|
||||
|
||||
def _update_raw_points(self, model):
|
||||
"""Update raw 3D points from model data"""
|
||||
self._path.raw_points = np.array([model.position.x, model.position.y, model.position.z], dtype=np.float32).T
|
||||
self._path.raw_points = np.array([model.position.x, np.array(model.position.y) + self._camera_offset, model.position.z], dtype=np.float32).T
|
||||
|
||||
for i, lane_line in enumerate(model.laneLines):
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, lane_line.y, lane_line.z], dtype=np.float32).T
|
||||
self._lane_lines[i].raw_points = np.array([lane_line.x, np.array(lane_line.y) + self._camera_offset, lane_line.z], dtype=np.float32).T
|
||||
|
||||
for i, road_edge in enumerate(model.roadEdges):
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, road_edge.y, road_edge.z], dtype=np.float32).T
|
||||
self._road_edges[i].raw_points = np.array([road_edge.x, np.array(road_edge.y) + self._camera_offset, road_edge.z], dtype=np.float32).T
|
||||
|
||||
self._lane_line_probs = np.array(model.laneLineProbs, dtype=np.float32)
|
||||
self._road_edge_stds = np.array(model.roadEdgeStds, dtype=np.float32)
|
||||
@@ -155,7 +165,7 @@ class ModelRenderer(Widget):
|
||||
|
||||
# Get z-coordinate from path at the lead vehicle position
|
||||
z = self._path.raw_points[idx, 2] if idx < len(self._path.raw_points) else 0.0
|
||||
point = self._map_to_screen(d_rel, -y_rel, z + self._path_offset_z)
|
||||
point = self._map_to_screen(d_rel, -y_rel + self._camera_offset, z + self._path_offset_z)
|
||||
if point:
|
||||
self._lead_vehicles[i] = self._update_lead_vehicle(d_rel, v_rel, point, self._rect)
|
||||
|
||||
@@ -281,6 +291,10 @@ class ModelRenderer(Widget):
|
||||
allow_throttle = sm['longitudinalPlan'].allowThrottle or not self._longitudinal_control
|
||||
self._blend_filter.update(int(allow_throttle))
|
||||
|
||||
if ui_state.rainbow_path:
|
||||
self.rainbow_path.draw_rainbow_path(self._rect, self._path)
|
||||
return
|
||||
|
||||
if self._experimental_mode:
|
||||
# Draw with acceleration coloring
|
||||
if len(self._exp_gradient.colors) > 1:
|
||||
|
||||
116
selfdrive/ui/sunnypilot/layouts/onboarding.py
Normal file
116
selfdrive/ui/sunnypilot/layouts/onboarding.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""
|
||||
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.system.ui.lib.application import FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.button import Button, ButtonStyle
|
||||
from openpilot.system.ui.widgets.label import Label
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
|
||||
class SunnylinkConsentPage(Widget):
|
||||
def __init__(self, done_callback=None):
|
||||
super().__init__()
|
||||
self._done_callback = done_callback
|
||||
self._step = 0
|
||||
|
||||
self._title = Label(tr("sunnylink"), font_size=90, font_weight=FontWeight.BOLD, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
|
||||
self._content = [
|
||||
{
|
||||
"text": tr("sunnylink enables secured remote access to your comma device from anywhere, " +
|
||||
"including settings management, remote monitoring, real-time dashboard, etc."),
|
||||
"primary_btn": tr("Enable"),
|
||||
"secondary_btn": tr("Disable"),
|
||||
"highlight_primary": True
|
||||
},
|
||||
{
|
||||
"text": tr("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
|
||||
"If sunnylink is disabled, features such as settings management, remote monitoring, " +
|
||||
"real-time dashboards will be unavailable."),
|
||||
"secondary_btn": tr("Back"),
|
||||
"danger_btn": tr("Disable"),
|
||||
"highlight_primary": True
|
||||
}
|
||||
]
|
||||
|
||||
self._primary_btn = Button("", button_style=ButtonStyle.PRIMARY, click_callback=lambda: self._handle_choice("enable"))
|
||||
self._secondary_btn = Button("", button_style=ButtonStyle.NORMAL, click_callback=lambda: self._handle_choice("secondary"))
|
||||
self._danger_btn = Button("", button_style=ButtonStyle.DANGER, click_callback=lambda: self._handle_choice("disable"))
|
||||
|
||||
def _handle_choice(self, choice):
|
||||
if choice == "enable":
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
if self._done_callback:
|
||||
self._done_callback()
|
||||
elif choice == "secondary":
|
||||
if self._step == 0:
|
||||
self._step = 1
|
||||
elif self._step == 1:
|
||||
self._step = 0
|
||||
elif choice == "disable":
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
if self._done_callback:
|
||||
self._done_callback()
|
||||
|
||||
def _render(self, _):
|
||||
step_data = self._content[self._step]
|
||||
|
||||
welcome_x = self._rect.x + 95
|
||||
welcome_y = self._rect.y + 165
|
||||
welcome_rect = rl.Rectangle(welcome_x, welcome_y, self._rect.width - welcome_x, 90)
|
||||
self._title.render(welcome_rect)
|
||||
|
||||
desc_x = welcome_x
|
||||
desc_y = welcome_y + 120
|
||||
desc_rect = rl.Rectangle(desc_x, desc_y, self._rect.width - desc_x, self._rect.height - desc_y - 250)
|
||||
|
||||
desc_label = Label(step_data["text"], font_size=90, font_weight=FontWeight.MEDIUM, text_alignment=rl.GuiTextAlignment.TEXT_ALIGN_LEFT)
|
||||
desc_label.render(desc_rect)
|
||||
|
||||
btn_y = self._rect.y + self._rect.height - 160 - 45
|
||||
|
||||
if "danger_btn" in step_data:
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
|
||||
self._secondary_btn.set_text(step_data["secondary_btn"])
|
||||
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
|
||||
self._danger_btn.set_text(step_data["danger_btn"])
|
||||
self._danger_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
else:
|
||||
btn_width = (self._rect.width - 45 * 3) / 2
|
||||
|
||||
self._secondary_btn.set_text(step_data["secondary_btn"])
|
||||
self._secondary_btn.render(rl.Rectangle(self._rect.x + 45, btn_y, btn_width, 160))
|
||||
|
||||
self._primary_btn.set_text(step_data["primary_btn"])
|
||||
self._primary_btn.render(rl.Rectangle(self._rect.x + 45 * 2 + btn_width, btn_y, btn_width, 160))
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
class SunnylinkOnboarding:
|
||||
def __init__(self):
|
||||
self.consent_page = SunnylinkConsentPage(done_callback=self._on_done)
|
||||
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.consent_done
|
||||
|
||||
def _on_done(self):
|
||||
self.consent_done = True
|
||||
|
||||
def render(self, rect):
|
||||
if not self.consent_done:
|
||||
self.consent_page.render(rect)
|
||||
@@ -5,8 +5,216 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp, multiple_button_item_sp, button_item_sp, \
|
||||
dual_button_item_sp, Spacer
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.list_view import text_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import LineSeparator
|
||||
|
||||
offroad_time_options = {
|
||||
0: 0,
|
||||
1: 5,
|
||||
2: 10,
|
||||
3: 15,
|
||||
4: 30,
|
||||
5: 60,
|
||||
6: 120,
|
||||
7: 180,
|
||||
8: 300,
|
||||
9: 600,
|
||||
10: 1440,
|
||||
11: 1800,
|
||||
}
|
||||
|
||||
|
||||
class DeviceLayoutSP(DeviceLayout):
|
||||
def __init__(self):
|
||||
DeviceLayout.__init__(self)
|
||||
self._scroller._line_separator = None
|
||||
|
||||
def _initialize_items(self):
|
||||
DeviceLayout._initialize_items(self)
|
||||
|
||||
# Using dual button with no right button for better alignment
|
||||
self._always_offroad_btn = dual_button_item_sp(
|
||||
left_text=lambda: tr("Enable Always Offroad"),
|
||||
left_callback=self._handle_always_offroad,
|
||||
right_text="",
|
||||
right_callback=None,
|
||||
)
|
||||
self._always_offroad_btn.action_item.right_button.set_visible(False)
|
||||
|
||||
self._max_time_offroad = option_item_sp(
|
||||
title=lambda: tr("Max Time Offroad"),
|
||||
description=lambda: tr("Device will automatically shutdown after set time once the engine is turned off.\n(30h is the default)"),
|
||||
param="MaxTimeOffroad",
|
||||
min_value=0,
|
||||
max_value=11,
|
||||
value_change_step=1,
|
||||
on_value_changed=None,
|
||||
enabled=True,
|
||||
icon="",
|
||||
value_map=offroad_time_options,
|
||||
label_width=360,
|
||||
use_float_scaling=False,
|
||||
inline=True,
|
||||
label_callback=self._update_max_time_offroad_label
|
||||
)
|
||||
|
||||
self._device_wake_mode = multiple_button_item_sp(
|
||||
title=lambda: tr("Wake Up Behavior"),
|
||||
description=self.wake_mode_description,
|
||||
param="DeviceBootMode",
|
||||
buttons=[lambda: tr("Default"), lambda: tr("Offroad")],
|
||||
button_width=364,
|
||||
callback=None,
|
||||
inline=True,
|
||||
)
|
||||
|
||||
self._quiet_mode_and_dcam = dual_button_item_sp(
|
||||
left_text=lambda: tr("Quiet Mode"),
|
||||
right_text=lambda: tr("Driver Camera Preview"),
|
||||
left_callback=lambda: ui_state.params.put_bool("QuietMode", not ui_state.params.get_bool("QuietMode")),
|
||||
right_callback=self._show_driver_camera
|
||||
)
|
||||
self._quiet_mode_and_dcam.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
|
||||
|
||||
self._reg_and_training = dual_button_item_sp(
|
||||
left_text=lambda: tr("Regulatory"),
|
||||
left_callback=self._on_regulatory,
|
||||
right_text=lambda: tr("Training Guide"),
|
||||
right_callback=self._on_review_training_guide
|
||||
)
|
||||
self._reg_and_training.action_item.right_button.set_button_style(ButtonStyle.NORMAL)
|
||||
|
||||
self._onroad_uploads_and_reset_settings = dual_button_item_sp(
|
||||
left_text=lambda: tr("Onroad Uploads"),
|
||||
left_callback=lambda: ui_state.params.put_bool("OnroadUploads", not ui_state.params.get_bool("OnroadUploads")),
|
||||
right_text=lambda: tr("Reset Settings"),
|
||||
right_callback=self._reset_settings
|
||||
)
|
||||
|
||||
self._power_buttons = dual_button_item_sp(
|
||||
left_text=lambda: tr("Reboot"),
|
||||
right_text=lambda: tr("Power Off"),
|
||||
left_callback=self._reboot_prompt,
|
||||
right_callback=self._power_off_prompt
|
||||
)
|
||||
|
||||
items = [
|
||||
text_item(lambda: tr("Dongle ID"), self._params.get("DongleId") or (lambda: tr("N/A"))),
|
||||
LineSeparator(),
|
||||
text_item(lambda: tr("Serial"), self._params.get("HardwareSerial") or (lambda: tr("N/A"))),
|
||||
LineSeparator(),
|
||||
self._pair_device_btn,
|
||||
LineSeparator(),
|
||||
self._reset_calib_btn,
|
||||
LineSeparator(),
|
||||
button_item_sp(lambda: tr("Change Language"), lambda: tr("CHANGE"), callback=self._show_language_dialog),
|
||||
LineSeparator(),
|
||||
self._device_wake_mode,
|
||||
LineSeparator(),
|
||||
self._max_time_offroad,
|
||||
LineSeparator(height=10),
|
||||
self._quiet_mode_and_dcam,
|
||||
self._reg_and_training,
|
||||
self._onroad_uploads_and_reset_settings,
|
||||
Spacer(10),
|
||||
LineSeparator(height=10),
|
||||
self._power_buttons,
|
||||
]
|
||||
|
||||
return items
|
||||
|
||||
def _offroad_transition(self):
|
||||
self._power_buttons.action_item.right_button.set_visible(ui_state.is_offroad())
|
||||
|
||||
@staticmethod
|
||||
def wake_mode_description() -> str:
|
||||
def_str = tr("Default: Device will boot/wake-up normally & will be ready to engage.")
|
||||
offrd_str = tr("Offroad: Device will be in Always Offroad mode after boot/wake-up.")
|
||||
header = tr("Controls state of the device after boot/sleep.")
|
||||
|
||||
return f"{header}\n\n{def_str}\n{offrd_str}"
|
||||
|
||||
@staticmethod
|
||||
def _reset_settings():
|
||||
def _do_reset(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
for _key in ui_state.params.all_keys():
|
||||
ui_state.params.remove(_key)
|
||||
HARDWARE.reboot()
|
||||
|
||||
def _second_confirm(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
text=tr("The reset cannot be undone. You have been warned."),
|
||||
confirm_text=tr("Confirm")
|
||||
), callback=_do_reset)
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(
|
||||
text=tr("Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back."),
|
||||
confirm_text=tr("Reset")
|
||||
), callback=_second_confirm)
|
||||
|
||||
@staticmethod
|
||||
def _handle_always_offroad():
|
||||
if ui_state.engaged:
|
||||
gui_app.set_modal_overlay(alert_dialog(tr("Disengage to Enter Always Offroad Mode")))
|
||||
return
|
||||
|
||||
_offroad_mode_state = ui_state.params.get_bool("OffroadMode")
|
||||
_offroad_mode_str = tr("Are you sure you want to exit Always Offroad mode?") if _offroad_mode_state else \
|
||||
tr("Are you sure you want to enter Always Offroad mode?")
|
||||
|
||||
def _set_always_offroad(result: int):
|
||||
if result == DialogResult.CONFIRM and not ui_state.engaged:
|
||||
ui_state.params.put_bool("OffroadMode", not _offroad_mode_state)
|
||||
|
||||
gui_app.set_modal_overlay(ConfirmDialog(_offroad_mode_str, tr("Confirm")), callback=lambda result: _set_always_offroad(result))
|
||||
|
||||
@staticmethod
|
||||
def _update_max_time_offroad_label(value: int) -> str:
|
||||
label = tr("Always On") if value == 0 else f"{value}" + tr("m") if value < 60 else f"{value // 60}" + tr("h")
|
||||
label += tr(" (Default)") if value == 1800 else ""
|
||||
return label
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
# Handle Always Offroad button
|
||||
always_offroad = ui_state.params.get_bool("OffroadMode")
|
||||
|
||||
# Text & Color
|
||||
offroad_mode_btn_text = tr("Exit Always Offroad") if always_offroad else tr("Enable Always Offroad")
|
||||
offroad_mode_btn_style = ButtonStyle.NORMAL if always_offroad else ButtonStyle.DANGER
|
||||
self._always_offroad_btn.action_item.left_button.set_text(offroad_mode_btn_text)
|
||||
self._always_offroad_btn.action_item.left_button.set_button_style(offroad_mode_btn_style)
|
||||
|
||||
# Position
|
||||
if self._scroller._items.__contains__(self._always_offroad_btn):
|
||||
self._scroller._items.remove(self._always_offroad_btn)
|
||||
if ui_state.is_offroad() and not always_offroad:
|
||||
self._scroller._items.insert(len(self._scroller._items) - 1, self._always_offroad_btn)
|
||||
else:
|
||||
self._scroller._items.insert(0, self._always_offroad_btn)
|
||||
|
||||
# Quiet Mode button
|
||||
self._quiet_mode_and_dcam.action_item.left_button.set_button_style(ButtonStyle.PRIMARY if ui_state.params.get_bool("QuietMode") else ButtonStyle.NORMAL)
|
||||
|
||||
# Onroad Uploads
|
||||
self._onroad_uploads_and_reset_settings.action_item.left_button.set_button_style(
|
||||
ButtonStyle.PRIMARY if ui_state.params.get_bool("OnroadUploads") else ButtonStyle.NORMAL
|
||||
)
|
||||
|
||||
# Offroad only buttons
|
||||
self._quiet_mode_and_dcam.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
self._reg_and_training.action_item.left_button.set_enabled(ui_state.is_offroad())
|
||||
self._reg_and_training.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
self._onroad_uploads_and_reset_settings.action_item.right_button.set_enabled(ui_state.is_offroad())
|
||||
|
||||
@@ -74,7 +74,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
self.lane_turn_value_control = option_item_sp(tr("Adjust Lane Turn Speed"), "LaneTurnValue", 500, 2000,
|
||||
tr("Set the maximum speed for lane turn desires. Default is 19 mph."),
|
||||
int(round(100 / CV.MPH_TO_KPH)), None, True, "", style.BUTTON_WIDTH, None, True,
|
||||
int(round(100 / CV.MPH_TO_KPH)), None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
||||
lambda v: f"{int(round(v / 100 * (CV.MPH_TO_KPH if ui_state.is_metric else 1)))}" +
|
||||
f" {'km/h' if ui_state.is_metric else 'mph'}")
|
||||
|
||||
@@ -86,7 +86,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
self.delay_control = option_item_sp(tr("Adjust Software Delay"), "LagdToggleDelay", 5, 50,
|
||||
tr("Adjust the software delay when Live Learning Steer Delay is toggled off. The default software delay value is 0.2"),
|
||||
1, None, True, "", style.BUTTON_WIDTH, None, True, lambda v: f"{v / 100:.2f}s")
|
||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True, lambda v: f"{v / 100:.2f}s")
|
||||
|
||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -9,28 +9,28 @@ from enum import IntEnum
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.device import DeviceLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.network import NetworkUISP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.osm import OSMLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.software import SoftwareLayoutSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.sunnylink import SunnylinkLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.trips import TripsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle import VehicleLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.steering import SteeringLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.cruise import CruiseLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.visuals import VisualsLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import DisplayLayout
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.developer import DeveloperLayoutSP
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
from openpilot.system.ui.lib.wifi_manager import WifiManager
|
||||
from openpilot.system.ui.sunnypilot.lib.styles import style
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
|
||||
# from openpilot.selfdrive.ui.sunnypilot.layouts.settings.navigation import NavigationLayout
|
||||
|
||||
|
||||
@@ -4,23 +4,23 @@ 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 cereal import custom
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.widgets.button import ButtonStyle, Button
|
||||
from openpilot.system.ui.widgets.confirm_dialog import alert_dialog, ConfirmDialog
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.list_view import button_item, dual_button_item
|
||||
from openpilot.system.ui.widgets.list_view import dual_button_item
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller, LineSeparator
|
||||
from openpilot.system.ui.widgets import Widget, DialogResult
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
import pyray as rl
|
||||
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import button_item_sp as button_item
|
||||
from openpilot.system.version import sunnylink_consent_version
|
||||
|
||||
|
||||
class SunnylinkHeader(Widget):
|
||||
@@ -160,14 +160,14 @@ class SunnylinkLayout(Widget):
|
||||
self._sunnylink_description = SunnylinkDescriptionItem()
|
||||
self._sunnylink_description.set_visible(False)
|
||||
|
||||
self._sponsor_btn = button_item(
|
||||
self._sponsor_btn = button_item_sp(
|
||||
title=tr("Sponsor Status"),
|
||||
button_text=tr("SPONSOR"),
|
||||
description=tr(
|
||||
"Become a sponsor of sunnypilot to get early access to sunnylink features when they become available."),
|
||||
callback=lambda: self._handle_pair_btn(False)
|
||||
)
|
||||
self._pair_btn = button_item(
|
||||
self._pair_btn = button_item_sp(
|
||||
title=tr("Pair GitHub Account"),
|
||||
button_text=tr("Not Paired"),
|
||||
description=tr(
|
||||
@@ -209,8 +209,8 @@ class SunnylinkLayout(Widget):
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _get_sunnylink_dongle_id() -> str | None:
|
||||
return str(ui_state.params.get("SunnylinkDongleId") or (lambda: tr("N/A")))
|
||||
def _get_sunnylink_dongle_id() -> str:
|
||||
return ui_state.params.get("SunnylinkDongleId") or tr("N/A")
|
||||
|
||||
def _handle_pair_btn(self, sponsor_pairing: bool = False):
|
||||
sunnylink_dongle_id = self._get_sunnylink_dongle_id()
|
||||
@@ -302,6 +302,22 @@ class SunnylinkLayout(Widget):
|
||||
self._restore_btn.set_text(tr("Restore Settings"))
|
||||
|
||||
def _sunnylink_toggle_callback(self, state: bool):
|
||||
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
|
||||
sl_enabled: bool = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
|
||||
if state and not sl_consent and not sl_enabled:
|
||||
def on_consent_done():
|
||||
enabled = ui_state.params.get_bool("SunnylinkEnabled")
|
||||
self._update_description(enabled)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
sl_terms_dlg = SunnylinkConsentPage(done_callback=on_consent_done)
|
||||
gui_app.set_modal_overlay(sl_terms_dlg)
|
||||
else:
|
||||
ui_state.params.put_bool("SunnylinkEnabled", state)
|
||||
self._update_description(state)
|
||||
|
||||
def _update_description(self, state: bool):
|
||||
if state:
|
||||
description = tr(
|
||||
"Welcome back!! We're excited to see you've enabled sunnylink again!")
|
||||
|
||||
@@ -23,7 +23,7 @@ class VehicleLayout(Widget):
|
||||
self._current_brand = None
|
||||
self._platform_selector = PlatformSelector(self._update_brand_settings)
|
||||
|
||||
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("Select")),
|
||||
self._vehicle_item = ListItemSP(title=self._platform_selector.text, action_item=ButtonAction(text=tr("SELECT")),
|
||||
callback=self._platform_selector._on_clicked)
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
self._legend_widget = LegendWidget(self._platform_selector)
|
||||
@@ -42,7 +42,7 @@ class VehicleLayout(Widget):
|
||||
def _update_brand_settings(self):
|
||||
self._vehicle_item._title = self._platform_selector.text
|
||||
self._vehicle_item.title_color = self._platform_selector.color
|
||||
vehicle_text = tr("Remove") if ui_state.params.get("CarPlatformBundle") else tr("Select")
|
||||
vehicle_text = tr("REMOVE") if ui_state.params.get("CarPlatformBundle") else tr("SELECT")
|
||||
self._vehicle_item.action_item.set_text(vehicle_text)
|
||||
|
||||
brand = self.get_brand()
|
||||
|
||||
@@ -5,11 +5,55 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.vehicle.brands.base import BrandSettings
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||
|
||||
|
||||
DESCRIPTIONS = {
|
||||
'enforce_stock_longitudinal': tr_noop(
|
||||
'sunnypilot will not take over control of gas and brakes. Factory Toyota longitudinal control will be used.'
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class ToyotaSettings(BrandSettings):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.enforce_stock_longitudinal = toggle_item_sp(
|
||||
lambda: tr("Enforce Factory Longitudinal Control"),
|
||||
description=lambda: tr(DESCRIPTIONS["enforce_stock_longitudinal"]),
|
||||
initial_state=ui_state.params.get_bool("ToyotaEnforceStockLongitudinal"),
|
||||
callback=self._on_enable_enforce_stock_longitudinal,
|
||||
enabled=lambda: not ui_state.engaged,
|
||||
)
|
||||
|
||||
self.items = [self.enforce_stock_longitudinal, ]
|
||||
|
||||
def _on_enable_enforce_stock_longitudinal(self, state: bool):
|
||||
if state:
|
||||
def confirm_callback(result: int):
|
||||
if result == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", True)
|
||||
if ui_state.params.get_bool("AlphaLongitudinalEnabled"):
|
||||
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
else:
|
||||
self.enforce_stock_longitudinal.action_item.set_state(False)
|
||||
|
||||
content = (f"<h1>{self.enforce_stock_longitudinal.title}</h1><br>" +
|
||||
f"<p>{self.enforce_stock_longitudinal.description}</p>")
|
||||
|
||||
dlg = ConfirmDialog(content, tr("Enable"), rich=True)
|
||||
gui_app.set_modal_overlay(dlg, callback=confirm_callback)
|
||||
|
||||
else:
|
||||
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
|
||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||
|
||||
def update_settings(self):
|
||||
pass
|
||||
|
||||
87
selfdrive/ui/sunnypilot/layouts/sidebar.py
Normal file
87
selfdrive/ui/sunnypilot/layouts/sidebar.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""
|
||||
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
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.multilang import tr_noop
|
||||
|
||||
|
||||
PING_TIMEOUT_NS = 80_000_000_000 # 80 seconds in nanoseconds
|
||||
METRIC_HEIGHT = 126
|
||||
METRIC_MARGIN = 30
|
||||
METRIC_START_Y = 300
|
||||
HOME_BTN = rl.Rectangle(60, 860, 180, 180)
|
||||
|
||||
|
||||
# Color scheme
|
||||
class Colors:
|
||||
WHITE = rl.WHITE
|
||||
WHITE_DIM = rl.Color(255, 255, 255, 85)
|
||||
GRAY = rl.Color(84, 84, 84, 255)
|
||||
|
||||
# Status colors
|
||||
GOOD = rl.WHITE
|
||||
WARNING = rl.Color(218, 202, 37, 255)
|
||||
DANGER = rl.Color(201, 34, 49, 255)
|
||||
PROGRESS = rl.Color(0, 134, 233, 255)
|
||||
DISABLED = rl.Color(128, 128, 128, 255)
|
||||
|
||||
# UI elements
|
||||
METRIC_BORDER = rl.Color(255, 255, 255, 85)
|
||||
BUTTON_NORMAL = rl.WHITE
|
||||
BUTTON_PRESSED = rl.Color(255, 255, 255, 166)
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class MetricData:
|
||||
label: str
|
||||
value: str
|
||||
color: rl.Color
|
||||
|
||||
def update(self, label: str, value: str, color: rl.Color):
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.color = color
|
||||
|
||||
|
||||
class SidebarSP:
|
||||
def __init__(self):
|
||||
self._sunnylink_status = MetricData(tr_noop("SUNNYLINK"), tr_noop("OFFLINE"), Colors.WARNING)
|
||||
|
||||
def _update_sunnylink_status(self):
|
||||
if not ui_state.params.get_bool("SunnylinkEnabled"):
|
||||
self._sunnylink_status.update(tr_noop("SUNNYLINK"), tr_noop("DISABLED"), Colors.DISABLED)
|
||||
return
|
||||
|
||||
last_ping = ui_state.params.get("LastSunnylinkPingTime") or 0
|
||||
dongle_id = ui_state.params.get("SunnylinkDongleId")
|
||||
|
||||
is_online = last_ping and (time.monotonic_ns() - last_ping) < PING_TIMEOUT_NS
|
||||
is_temp_fault = ui_state.params.get_bool("SunnylinkTempFault")
|
||||
is_registering = not is_temp_fault and dongle_id in (None, "", UNREGISTERED_SUNNYLINK_DONGLE_ID)
|
||||
|
||||
# Determine status/color pair based on priority
|
||||
if last_ping:
|
||||
status, color = (tr_noop("ONLINE"), Colors.GOOD) if is_online else (tr_noop("ERROR"), Colors.DANGER)
|
||||
elif is_temp_fault:
|
||||
status, color = (tr_noop("FAULT"), Colors.WARNING)
|
||||
elif is_registering:
|
||||
status, color = (tr_noop("REGIST..."), Colors.PROGRESS)
|
||||
else:
|
||||
status, color = (tr_noop("OFFLINE"), Colors.DANGER)
|
||||
|
||||
self._sunnylink_status.update(tr_noop("SUNNYLINK"), status, color)
|
||||
|
||||
def _draw_metrics_w_sunnylink(self, rect: rl.Rectangle, _temp, _panda, _connect):
|
||||
metrics = [_temp, _panda, _connect, self._sunnylink_status]
|
||||
start_y = int(rect.y) + METRIC_START_Y
|
||||
available_height = max(0, int(HOME_BTN.y) - METRIC_MARGIN - METRIC_HEIGHT - start_y)
|
||||
spacing = available_height / max(1, len(metrics) - 1)
|
||||
|
||||
return metrics, start_y, spacing
|
||||
0
selfdrive/ui/sunnypilot/mici/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/mici/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/mici/layouts/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/mici/layouts/__init__.py
Normal file
97
selfdrive/ui/sunnypilot/mici/layouts/onboarding.py
Normal file
97
selfdrive/ui/sunnypilot/mici/layouts/onboarding.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
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.system.ui.lib.application import FontWeight, gui_app
|
||||
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
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
class SunnylinkConsentPage(SetupTermsPage):
|
||||
def __init__(self, on_accept=None, on_decline=None, left_text: str = "disable", right_text: str = "enable"):
|
||||
super().__init__(on_accept, on_decline, left_text, continue_text=right_text)
|
||||
|
||||
self._title_header = TermsHeader("sunnylink",
|
||||
gui_app.texture("../../sunnypilot/selfdrive/assets/logo.png", 66, 60))
|
||||
|
||||
self._terms_label = UnifiedLabel("sunnylink enables secured remote access to your comma device from anywhere, " +
|
||||
"including settings management, remote monitoring, real-time dashboard, etc.",
|
||||
36, FontWeight.ROMAN)
|
||||
|
||||
@property
|
||||
def _content_height(self):
|
||||
return self._terms_label.rect.y + self._terms_label.rect.height - self._scroll_panel.get_offset()
|
||||
|
||||
def _render(self, _):
|
||||
super()._render(_)
|
||||
return -1
|
||||
|
||||
def _render_content(self, scroll_offset):
|
||||
self._title_header.set_position(self._rect.x + 16, self._rect.y + 12 + scroll_offset)
|
||||
self._title_header.render()
|
||||
|
||||
self._terms_label.render(rl.Rectangle(
|
||||
self._rect.x + 16,
|
||||
self._title_header.rect.y + self._title_header.rect.height + self.ITEM_SPACING,
|
||||
self._rect.width - 100,
|
||||
self._terms_label.get_content_height(int(self._rect.width - 100)),
|
||||
))
|
||||
|
||||
|
||||
class SunnylinkConsentDisableConfirmPage(SunnylinkConsentPage):
|
||||
def __init__(self, on_accept=None, on_decline=None):
|
||||
super().__init__(on_accept=on_decline, on_decline=on_accept, left_text="enable", right_text="disable")
|
||||
|
||||
# we flip the continue & disable buttons to use slider for disable
|
||||
self._continue_slider = True
|
||||
self._continue_button = SmallSlider("disable", confirm_callback=on_decline)
|
||||
self._scroll_panel.set_enabled(lambda: not self._continue_button.is_pressed)
|
||||
|
||||
self._title_header = TermsHeader("disable sunnylink?",
|
||||
gui_app.texture("icons_mici/setup/red_warning.png", 66, 60))
|
||||
|
||||
self._terms_label = UnifiedLabel("sunnylink is designed to be enabled as part of sunnypilot's core functionality. " +
|
||||
"If sunnylink is disabled, features such as settings management, " +
|
||||
"remote monitoring, real-time dashboards will be unavailable.",
|
||||
36, FontWeight.ROMAN)
|
||||
|
||||
|
||||
class SunnylinkOnboarding:
|
||||
def __init__(self):
|
||||
self.consent_done: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") in {sunnylink_consent_version, sunnylink_consent_declined}
|
||||
self.disable_confirm = False
|
||||
|
||||
self.consent_page = SunnylinkConsentPage(on_decline=self._on_decline, on_accept=self._on_accept)
|
||||
self.confirm_page = SunnylinkConsentDisableConfirmPage(on_decline=self._on_confirm_decline, on_accept=self._on_accept)
|
||||
|
||||
@property
|
||||
def completed(self) -> bool:
|
||||
return self.consent_done
|
||||
|
||||
def _on_accept(self):
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
self.consent_done = True
|
||||
|
||||
def _on_decline(self):
|
||||
self.disable_confirm = True
|
||||
|
||||
def _on_confirm_decline(self):
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
self.consent_done = True
|
||||
|
||||
def render(self, rect):
|
||||
if self.consent_done:
|
||||
return
|
||||
|
||||
if self.disable_confirm:
|
||||
self.confirm_page.render(rect)
|
||||
else:
|
||||
self.consent_page.render(rect)
|
||||
39
selfdrive/ui/sunnypilot/mici/layouts/settings.py
Normal file
39
selfdrive/ui/sunnypilot/mici/layouts/settings.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
|
||||
ICON_SIZE = 70
|
||||
|
||||
OP.PanelType = IntEnum( # type: ignore
|
||||
"PanelType",
|
||||
[es.name for es in OP.PanelType] + [
|
||||
"SUNNYLINK",
|
||||
],
|
||||
start=0,
|
||||
)
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
|
||||
sunnylink_btn = BigButton("sunnylink", "", "icons_mici/settings/developer/ssh.png")
|
||||
sunnylink_btn.set_click_callback(lambda: self._set_current_panel(OP.PanelType.SUNNYLINK))
|
||||
self._panels.update({
|
||||
OP.PanelType.SUNNYLINK: OP.PanelInfo("sunnylink", SunnylinkLayoutMici(back_callback=lambda: self._set_current_panel(None))),
|
||||
})
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, sunnylink_btn)
|
||||
self._scroller._items.clear()
|
||||
for item in items:
|
||||
self._scroller.add_widget(item)
|
||||
211
selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
Normal file
211
selfdrive/ui/sunnypilot/mici/layouts/sunnylink.py
Normal file
@@ -0,0 +1,211 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
|
||||
import pyray as rl
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigToggle
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigDialog, BigConfirmationDialogV2
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkConsentPage
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.widgets.scroller import Scroller
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
|
||||
class SunnylinkLayoutMici(NavWidget):
|
||||
def __init__(self, back_callback: Callable):
|
||||
super().__init__()
|
||||
self.set_back_callback(back_callback)
|
||||
self._restore_in_progress = False
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
|
||||
initial_state=self._sunnylink_enabled,
|
||||
toggle_callback=self._sunnylink_toggle_callback)
|
||||
self._sunnylink_sponsor_button = SunnylinkPairBigButton(sponsor_pairing=False)
|
||||
self._sunnylink_pair_button = SunnylinkPairBigButton(sponsor_pairing=True)
|
||||
self._backup_btn = BigButton(tr("backup settings"), "", "")
|
||||
self._backup_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=False))
|
||||
self._restore_btn = BigButton(tr("restore settings"), "", "")
|
||||
self._restore_btn.set_click_callback(lambda: self._handle_backup_restore_btn(restore=True))
|
||||
self._sunnylink_uploader_toggle = BigToggle(text=tr("sunnylink uploader"), initial_state=False,
|
||||
toggle_callback=self._sunnylink_uploader_callback)
|
||||
|
||||
self._scroller = Scroller([
|
||||
self._sunnylink_toggle,
|
||||
self._sunnylink_sponsor_button,
|
||||
self._sunnylink_pair_button,
|
||||
self._backup_btn,
|
||||
self._restore_btn,
|
||||
self._sunnylink_uploader_toggle
|
||||
], snap_items=False)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
self._sunnylink_toggle.set_checked(self._sunnylink_enabled)
|
||||
self._sunnylink_pair_button.set_visible(self._sunnylink_enabled)
|
||||
self._sunnylink_sponsor_button.set_visible(self._sunnylink_enabled)
|
||||
self._backup_btn.set_visible(self._sunnylink_enabled)
|
||||
self._restore_btn.set_visible(self._sunnylink_enabled)
|
||||
self._sunnylink_uploader_toggle.set_visible(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
if ui_state.sunnylink_state.is_sponsor():
|
||||
self._sunnylink_sponsor_button.set_text(tr("thanks"))
|
||||
self._sunnylink_sponsor_button.set_value(ui_state.sunnylink_state.get_sponsor_tier().name.lower())
|
||||
self._sunnylink_sponsor_button.set_enabled(False)
|
||||
else:
|
||||
self._sunnylink_sponsor_button.set_text(tr("sponsor"))
|
||||
self._sunnylink_sponsor_button.set_value("")
|
||||
|
||||
if ui_state.sunnylink_state.is_paired():
|
||||
self._sunnylink_pair_button.set_text(tr("paired"))
|
||||
else:
|
||||
self._sunnylink_pair_button.set_text(tr("pair"))
|
||||
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
self._scroller.show_event()
|
||||
ui_state.update_params()
|
||||
|
||||
def _render(self, rect: rl.Rectangle):
|
||||
self._scroller.render(rect)
|
||||
|
||||
@staticmethod
|
||||
def _sunnylink_toggle_callback(state: bool):
|
||||
sl_consent: bool = ui_state.params.get("CompletedSunnylinkConsentVersion") == sunnylink_consent_version
|
||||
sl_enabled: bool = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
def sl_terms_accepted():
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", True)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
def sl_terms_declined():
|
||||
ui_state.params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_declined)
|
||||
ui_state.params.put_bool("SunnylinkEnabled", False)
|
||||
gui_app.set_modal_overlay(None)
|
||||
|
||||
if state and not sl_consent and not sl_enabled:
|
||||
sl_terms_dlg = SunnylinkConsentPage(on_accept=sl_terms_accepted, on_decline=sl_terms_declined)
|
||||
gui_app.set_modal_overlay(sl_terms_dlg)
|
||||
else:
|
||||
ui_state.params.put_bool("SunnylinkEnabled", state)
|
||||
|
||||
ui_state.update_params()
|
||||
|
||||
@staticmethod
|
||||
def _sunnylink_uploader_callback(state: bool):
|
||||
ui_state.params.put_bool("EnableSunnylinkUploader", state)
|
||||
|
||||
def _handle_backup_restore_btn(self, restore: bool = False):
|
||||
lbl = tr("slide to restore") if restore else tr("slide to backup")
|
||||
icon = "icons_mici/settings/device/update.png"
|
||||
dlg = BigConfirmationDialogV2(lbl, icon, confirm_callback=self._restore_handler if restore else self._backup_handler)
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
|
||||
def _backup_handler(self):
|
||||
self._backup_in_progress = True
|
||||
self._backup_btn.set_enabled(False)
|
||||
ui_state.params.put_bool("BackupManager_CreateBackup", True)
|
||||
|
||||
def _restore_handler(self):
|
||||
self._restore_in_progress = True
|
||||
self._restore_btn.set_enabled(False)
|
||||
ui_state.params.put("BackupManager_RestoreVersion", "latest")
|
||||
|
||||
def handle_backup_restore_progress(self):
|
||||
sunnylink_backup_manager = ui_state.sm["backupManagerSP"]
|
||||
|
||||
backup_status = sunnylink_backup_manager.backupStatus
|
||||
restore_status = sunnylink_backup_manager.restoreStatus
|
||||
backup_progress = sunnylink_backup_manager.backupProgress
|
||||
restore_progress = sunnylink_backup_manager.restoreProgress
|
||||
|
||||
if self._backup_in_progress:
|
||||
self._restore_btn.set_enabled(False)
|
||||
self._backup_btn.set_enabled(False)
|
||||
|
||||
if backup_status == custom.BackupManagerSP.Status.inProgress:
|
||||
self._backup_in_progress = True
|
||||
self._backup_btn.set_text(tr("backing up"))
|
||||
text = tr(f"{backup_progress}%")
|
||||
self._backup_btn.set_value(text)
|
||||
|
||||
elif backup_status == custom.BackupManagerSP.Status.failed:
|
||||
self._backup_in_progress = False
|
||||
self._backup_btn.set_enabled(not ui_state.is_onroad())
|
||||
self._backup_btn.set_text(tr("backup"))
|
||||
self._backup_btn.set_value(tr("failed"))
|
||||
|
||||
elif (backup_status == custom.BackupManagerSP.Status.completed or
|
||||
(backup_status == custom.BackupManagerSP.Status.idle and backup_progress == 100.0)):
|
||||
self._backup_in_progress = False
|
||||
gui_app.set_modal_overlay(BigDialog(title=tr("settings backed up"), description=""))
|
||||
self._backup_btn.set_enabled(not ui_state.is_onroad())
|
||||
|
||||
elif self._restore_in_progress:
|
||||
self._restore_btn.set_enabled(False)
|
||||
self._backup_btn.set_enabled(False)
|
||||
|
||||
if restore_status == custom.BackupManagerSP.Status.inProgress:
|
||||
self._restore_in_progress = True
|
||||
self._restore_btn.set_text(tr("restoring"))
|
||||
text = tr(f"{restore_progress}%")
|
||||
self._restore_btn.set_value(text)
|
||||
|
||||
elif restore_status == custom.BackupManagerSP.Status.failed:
|
||||
self._restore_in_progress = False
|
||||
self._restore_btn.set_enabled(not ui_state.is_onroad())
|
||||
self._restore_btn.set_text(tr("restore"))
|
||||
self._restore_btn.set_value(tr("failed"))
|
||||
gui_app.set_modal_overlay(BigDialog(title=tr("unable to restore"), description="try again later."))
|
||||
|
||||
elif (restore_status == custom.BackupManagerSP.Status.completed or
|
||||
(restore_status == custom.BackupManagerSP.Status.idle and restore_progress == 100.0)):
|
||||
self._restore_in_progress = False
|
||||
gui_app.set_modal_overlay(BigConfirmationDialogV2(
|
||||
title="slide to restart", icon="icons_mici/settings/device/reboot.png",
|
||||
confirm_callback=lambda: gui_app.request_close()))
|
||||
|
||||
else:
|
||||
can_enable = self._sunnylink_enabled and not ui_state.is_onroad()
|
||||
self._backup_btn.set_enabled(can_enable)
|
||||
self._backup_btn.set_text(tr("backup settings"))
|
||||
self._backup_btn.set_value("")
|
||||
self._restore_btn.set_enabled(can_enable)
|
||||
self._restore_btn.set_text(tr("restore settings"))
|
||||
self._restore_btn.set_value("")
|
||||
|
||||
|
||||
class SunnylinkPairBigButton(BigButton):
|
||||
def __init__(self, sponsor_pairing: bool = False):
|
||||
self.sponsor_pairing = sponsor_pairing
|
||||
super().__init__("", "", "")
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
dlg: BigDialog | SunnylinkPairingDialog | None = None
|
||||
if UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
dlg = BigDialog(tr("sunnylink Dongle ID not found. Please reboot & try again."), "")
|
||||
elif self.sponsor_pairing:
|
||||
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
|
||||
elif not self.sponsor_pairing:
|
||||
dlg = SunnylinkPairingDialog(sponsor_pairing=False)
|
||||
if dlg:
|
||||
gui_app.set_modal_overlay(dlg)
|
||||
0
selfdrive/ui/sunnypilot/mici/onroad/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/mici/onroad/__init__.py
Normal file
26
selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py
Normal file
26
selfdrive/ui/sunnypilot/mici/onroad/confidence_ball.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.onroad.augmented_road_view import BORDER_COLORS
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
||||
|
||||
|
||||
class ConfidenceBallSP:
|
||||
@staticmethod
|
||||
def get_animate_status_probs():
|
||||
if ui_state.status == UIStatus.LAT_ONLY:
|
||||
return ui_state.sm['modelV2'].meta.disengagePredictions.steerOverrideProbs
|
||||
|
||||
# UIStatus.LONG_ONLY
|
||||
return ui_state.sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs
|
||||
|
||||
@staticmethod
|
||||
def get_lat_long_dot_color():
|
||||
if ui_state.status == UIStatus.LAT_ONLY:
|
||||
return BORDER_COLORS[UIStatus.LAT_ONLY]
|
||||
|
||||
# UIStatus.LONG_ONLY
|
||||
return BORDER_COLORS[UIStatus.LONG_ONLY]
|
||||
13
selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py
Normal file
13
selfdrive/ui/sunnypilot/mici/onroad/model_renderer.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
|
||||
LANE_LINE_COLORS_SP = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0, 255, 64, 255),
|
||||
UIStatus.LONG_ONLY: rl.Color(0, 255, 64, 255),
|
||||
}
|
||||
0
selfdrive/ui/sunnypilot/mici/widgets/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/mici/widgets/__init__.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""
|
||||
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 base64
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.selfdrive.ui.mici.widgets.pairing_dialog import PairingDialog
|
||||
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID, API_HOST
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import NavWidget
|
||||
from openpilot.system.ui.widgets.label import MiciLabel
|
||||
|
||||
|
||||
class SunnylinkPairingDialog(PairingDialog):
|
||||
"""Dialog for device pairing with QR code."""
|
||||
|
||||
def __init__(self, sponsor_pairing: bool = False):
|
||||
PairingDialog.__init__(self)
|
||||
self._sponsor_pairing = sponsor_pairing
|
||||
label_text = tr("pair with sunnylink") if sponsor_pairing else tr("become a sunnypilot sponsor")
|
||||
self._pair_label = MiciLabel(label_text, 48, font_weight=FontWeight.BOLD,
|
||||
color=rl.Color(255, 255, 255, int(255 * 0.9)), line_height=40, wrap_text=True)
|
||||
|
||||
def _get_pairing_url(self) -> str:
|
||||
qr_string = "https://github.com/sponsors/sunnyhaibin"
|
||||
|
||||
if self._sponsor_pairing:
|
||||
try:
|
||||
sl_dongle_id = self._params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
token = SunnylinkApi(sl_dongle_id).get_token()
|
||||
inner_string = f"1|{sl_dongle_id}|{token}"
|
||||
payload_bytes = base64.b64encode(inner_string.encode('utf-8')).decode('utf-8')
|
||||
qr_string = f"{API_HOST}/sso?state={payload_bytes}"
|
||||
except Exception:
|
||||
cloudlog.exception("Failed to get pairing token")
|
||||
|
||||
return qr_string
|
||||
|
||||
def _update_state(self):
|
||||
NavWidget._update_state(self)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
gui_app.init_window("pairing device")
|
||||
pairing = SunnylinkPairingDialog(sponsor_pairing=True)
|
||||
try:
|
||||
for _ in gui_app.render():
|
||||
result = pairing.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height))
|
||||
if result != -1:
|
||||
break
|
||||
finally:
|
||||
del pairing
|
||||
0
selfdrive/ui/sunnypilot/onroad/__init__.py
Normal file
0
selfdrive/ui/sunnypilot/onroad/__init__.py
Normal file
13
selfdrive/ui/sunnypilot/onroad/augmented_road_view.py
Normal file
13
selfdrive/ui/sunnypilot/onroad/augmented_road_view.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||
|
||||
BORDER_COLORS_SP = {
|
||||
UIStatus.LAT_ONLY: rl.Color(0x00, 0xC8, 0xC8, 0xFF), # Cyan for lateral-only state
|
||||
UIStatus.LONG_ONLY: rl.Color(0x96, 0x1C, 0xA8, 0xFF), # Purple for longitudinal-only state
|
||||
}
|
||||
147
selfdrive/ui/sunnypilot/onroad/chevron_metrics.py
Normal file
147
selfdrive/ui/sunnypilot/onroad/chevron_metrics.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
import pyray as rl
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
|
||||
class ChevronOptions:
|
||||
OFF = 0
|
||||
DISTANCE_ONLY = 1
|
||||
SPEED_ONLY = 2
|
||||
TTC_ONLY = 3
|
||||
ALL = 4
|
||||
|
||||
|
||||
class ChevronMetrics:
|
||||
def __init__(self):
|
||||
self._lead_status_alpha: float = 0.0
|
||||
self._font = gui_app.font(FontWeight.SEMI_BOLD)
|
||||
|
||||
def update_alpha(self, has_lead: bool):
|
||||
"""Update the alpha value for fade in/out animation"""
|
||||
if not has_lead:
|
||||
self._lead_status_alpha = max(0.0, self._lead_status_alpha - 0.05)
|
||||
else:
|
||||
self._lead_status_alpha = min(1.0, self._lead_status_alpha + 0.1)
|
||||
|
||||
def should_render(self) -> bool:
|
||||
"""Check if dev UI should be rendered"""
|
||||
return ui_state.chevron_metrics != ChevronOptions.OFF and self._lead_status_alpha > 0.0
|
||||
|
||||
def _draw_lead(self, lead_data, lead_vehicle, v_ego: float, rect: rl.Rectangle):
|
||||
"""Draw lead vehicle status information (distance, speed, TTC)"""
|
||||
if not self.should_render():
|
||||
return
|
||||
|
||||
d_rel = lead_data.dRel
|
||||
v_rel = lead_data.vRel
|
||||
|
||||
if not lead_vehicle.chevron or len(lead_vehicle.chevron) < 2:
|
||||
return
|
||||
|
||||
chevron_x = lead_vehicle.chevron[1][0]
|
||||
chevron_y = lead_vehicle.chevron[1][1]
|
||||
sz = np.clip((25 * 30) / (d_rel / 3 + 30), 15.0, 30.0) * 2.35
|
||||
|
||||
text_lines = self._build_text_lines(d_rel, v_rel, v_ego)
|
||||
if not text_lines:
|
||||
return
|
||||
|
||||
self._render_text_lines(text_lines, chevron_x, chevron_y, sz, rect)
|
||||
|
||||
@staticmethod
|
||||
def _build_text_lines(d_rel: float, v_rel: float, v_ego: float) -> list[str]:
|
||||
"""Build text lines based on chevron info setting"""
|
||||
text_lines = []
|
||||
|
||||
# Distance
|
||||
if ui_state.chevron_metrics == ChevronOptions.DISTANCE_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
||||
val = max(0.0, d_rel)
|
||||
unit = "m" if ui_state.is_metric else "ft"
|
||||
if not ui_state.is_metric:
|
||||
val *= 3.28084
|
||||
text_lines.append(f"{val:.0f} {unit}")
|
||||
|
||||
# Speed
|
||||
if ui_state.chevron_metrics == ChevronOptions.SPEED_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
||||
multiplier = CV.MS_TO_KPH if ui_state.is_metric else CV.MS_TO_MPH
|
||||
val = max(0.0, (v_rel + v_ego) * multiplier)
|
||||
unit = "km/h" if ui_state.is_metric else "mph"
|
||||
text_lines.append(f"{val:.0f} {unit}")
|
||||
|
||||
# Time to collision
|
||||
if ui_state.chevron_metrics == ChevronOptions.TTC_ONLY or ui_state.chevron_metrics == ChevronOptions.ALL:
|
||||
val = (d_rel / v_ego) if (d_rel > 0 and v_ego > 0) else 0.0
|
||||
ttc_text = f"{val:.1f} s" if (0 < val < 200) else "---"
|
||||
text_lines.append(ttc_text)
|
||||
|
||||
return text_lines
|
||||
|
||||
def _render_text_lines(self, text_lines: list[str], chevron_x: float, chevron_y: float,
|
||||
sz: float, rect: rl.Rectangle):
|
||||
"""Render text lines with proper centering and positioning"""
|
||||
font_size = 40
|
||||
line_height = 50
|
||||
margin = 20
|
||||
|
||||
text_y = chevron_y + sz + 15
|
||||
total_height = len(text_lines) * line_height
|
||||
|
||||
# Adjust Y position if text would go off screen
|
||||
if text_y + total_height > rect.height - margin:
|
||||
y_max = min(chevron_y, rect.height - margin)
|
||||
text_y = y_max - 15 - total_height
|
||||
text_y = max(margin, text_y)
|
||||
|
||||
alpha = int(255 * self._lead_status_alpha)
|
||||
text_color = rl.Color(255, 255, 255, alpha)
|
||||
shadow_color = rl.Color(0, 0, 0, int(200 * self._lead_status_alpha))
|
||||
|
||||
for i, line in enumerate(text_lines):
|
||||
y = int(text_y + (i * line_height))
|
||||
if y + line_height > rect.height - margin:
|
||||
break
|
||||
|
||||
# Measure actual text width for proper centering
|
||||
text_size = measure_text_cached(self._font, line, font_size, 0)
|
||||
text_width = text_size.x
|
||||
|
||||
# Center the text horizontally on the chevron
|
||||
x = int(chevron_x - text_width / 2)
|
||||
x = int(np.clip(x, margin, rect.width - text_width - margin))
|
||||
|
||||
# Draw shadow
|
||||
rl.draw_text_ex(self._font, line, rl.Vector2(x + 2, y + 2), font_size, 0, shadow_color)
|
||||
# Draw text
|
||||
rl.draw_text_ex(self._font, line, rl.Vector2(x, y), font_size, 0, text_color)
|
||||
|
||||
def draw_lead_status(self, sm, radar_state, rect, lead_vehicles):
|
||||
lead_one = radar_state.leadOne
|
||||
lead_two = radar_state.leadTwo
|
||||
|
||||
has_lead_one = lead_one.status if lead_one else False
|
||||
has_lead_two = lead_two.status if lead_two else False
|
||||
|
||||
self.update_alpha(has_lead_one or has_lead_two)
|
||||
|
||||
if not self.should_render():
|
||||
return
|
||||
|
||||
v_ego = sm['carState'].vEgo
|
||||
|
||||
if has_lead_one and lead_vehicles[0].chevron:
|
||||
self._draw_lead(lead_one, lead_vehicles[0], v_ego, rect)
|
||||
|
||||
if has_lead_two and lead_vehicles[1].chevron:
|
||||
d_rel_diff = abs(lead_one.dRel - lead_two.dRel) if has_lead_one else float('inf')
|
||||
if d_rel_diff > 3.0:
|
||||
self._draw_lead(lead_two, lead_vehicles[1], v_ego, rect)
|
||||
185
selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py
Normal file
185
selfdrive/ui/sunnypilot/onroad/developer_ui/__init__.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
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
|
||||
BOTTOM_BAR_HEIGHT = 61
|
||||
|
||||
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()
|
||||
|
||||
@staticmethod
|
||||
def get_bottom_dev_ui_offset():
|
||||
if ui_state.developer_ui in (DeveloperUiRenderer.DEV_UI_BOTTOM, DeveloperUiRenderer.DEV_UI_BOTH):
|
||||
return DeveloperUiRenderer.BOTTOM_BAR_HEIGHT
|
||||
return 0
|
||||
|
||||
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))
|
||||
else:
|
||||
elements.append(self.desired_steer_elem.update(sm, ui_state.is_metric))
|
||||
|
||||
elements.append(self.actual_lat_accel_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
|
||||
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))
|
||||
|
||||
if not elements:
|
||||
return
|
||||
|
||||
font_size = 38
|
||||
element_widths = []
|
||||
for element in elements:
|
||||
element.measure(self._font_bold, font_size)
|
||||
element_widths.append(element.total_width)
|
||||
|
||||
total_element_width = sum(element_widths)
|
||||
num_gaps = len(elements) + 1
|
||||
available_width = rect.width
|
||||
gap_width = (available_width - total_element_width) / num_gaps
|
||||
|
||||
center_y = y + bar_height // 2
|
||||
current_x = rect.x + gap_width
|
||||
|
||||
for i, element in enumerate(elements):
|
||||
element_center_x = int(current_x + element_widths[i] / 2)
|
||||
self._draw_bottom_dev_ui_element(element_center_x, center_y, element)
|
||||
current_x += element_widths[i] + gap_width
|
||||
|
||||
def _draw_bottom_dev_ui_element(self, center_x: int, y: int, element: UiElement) -> None:
|
||||
font_size = 38
|
||||
start_x = center_x - element.total_width / 2
|
||||
|
||||
rl.draw_text_ex(self._font_bold, element.label_text, rl.Vector2(start_x, y - font_size // 2), font_size, 0, rl.WHITE)
|
||||
rl.draw_text_ex(self._font_bold, element.val_text, rl.Vector2(start_x + element.label_width, y - font_size // 2), font_size, 0, element.color)
|
||||
|
||||
if element.unit:
|
||||
rl.draw_text_ex(self._font_bold, element.unit_text, rl.Vector2(start_x + element.label_width + element.val_width, y - font_size // 2),
|
||||
font_size, 0, rl.WHITE)
|
||||
324
selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py
Normal file
324
selfdrive/ui/sunnypilot/onroad/developer_ui/elements.py
Normal file
@@ -0,0 +1,324 @@
|
||||
"""
|
||||
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
|
||||
|
||||
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
|
||||
@dataclass
|
||||
class UiElement:
|
||||
value: str
|
||||
label: str
|
||||
unit: str
|
||||
color: rl.Color
|
||||
val_text: str = ""
|
||||
label_text: str = ""
|
||||
unit_text: str = ""
|
||||
val_width: float = 0.0
|
||||
label_width: float = 0.0
|
||||
unit_width: float = 0.0
|
||||
total_width: float = 0.0
|
||||
|
||||
def measure(self, font, font_size: int):
|
||||
self.label_text = f"{self.label} "
|
||||
self.val_text = self.value
|
||||
self.unit_text = f" {self.unit}" if self.unit else ""
|
||||
|
||||
self.label_width = measure_text_cached(font, self.label_text, font_size, 0).x
|
||||
self.val_width = measure_text_cached(font, self.val_text, font_size, 0).x
|
||||
self.unit_width = measure_text_cached(font, self.unit_text, font_size, 0).x if self.unit else 0
|
||||
|
||||
self.total_width = self.label_width + self.val_width + self.unit_width
|
||||
|
||||
|
||||
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)
|
||||
49
selfdrive/ui/sunnypilot/onroad/driver_state.py
Normal file
49
selfdrive/ui/sunnypilot/onroad/driver_state.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.selfdrive.ui import UI_BORDER_SIZE
|
||||
from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer, BTN_SIZE, ARC_LENGTH
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer
|
||||
|
||||
|
||||
class DriverStateRendererSP(DriverStateRenderer):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.dev_ui_offset = DeveloperUiRenderer.get_bottom_dev_ui_offset()
|
||||
|
||||
def _pre_calculate_drawing_elements(self):
|
||||
"""Pre-calculate all drawing elements based on the current rectangle"""
|
||||
# Calculate icon position (bottom-left or bottom-right)
|
||||
width, height = self._rect.width, self._rect.height
|
||||
offset = UI_BORDER_SIZE + BTN_SIZE // 2
|
||||
self.position_x = self._rect.x + (width - offset if self.is_rhd else offset)
|
||||
self.position_y = self._rect.y + height - offset - self.dev_ui_offset
|
||||
|
||||
# Pre-calculate the face lines positions
|
||||
positioned_keypoints = self.face_keypoints_transformed + np.array([self.position_x, self.position_y])
|
||||
for i in range(len(positioned_keypoints)):
|
||||
self.face_lines[i].x = positioned_keypoints[i][0]
|
||||
self.face_lines[i].y = positioned_keypoints[i][1]
|
||||
|
||||
# Calculate arc dimensions based on head rotation
|
||||
delta_x = -self.driver_pose_sins[1] * ARC_LENGTH / 2.0 # Horizontal movement
|
||||
delta_y = -self.driver_pose_sins[0] * ARC_LENGTH / 2.0 # Vertical movement
|
||||
|
||||
# Horizontal arc
|
||||
h_width = abs(delta_x)
|
||||
self.h_arc_data = self._calculate_arc_data(
|
||||
delta_x, h_width, self.position_x, self.position_y - ARC_LENGTH / 2,
|
||||
self.driver_pose_sins[1], self.driver_pose_diff[1], is_horizontal=True
|
||||
)
|
||||
|
||||
# Vertical arc
|
||||
v_height = abs(delta_y)
|
||||
self.v_arc_data = self._calculate_arc_data(
|
||||
delta_y, v_height, self.position_x - ARC_LENGTH / 2, self.position_y,
|
||||
self.driver_pose_sins[0], self.driver_pose_diff[0], is_horizontal=False
|
||||
)
|
||||
20
selfdrive/ui/sunnypilot/onroad/hud_renderer.py
Normal file
20
selfdrive/ui/sunnypilot/onroad/hud_renderer.py
Normal file
@@ -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)
|
||||
14
selfdrive/ui/sunnypilot/onroad/model_renderer.py
Normal file
14
selfdrive/ui/sunnypilot/onroad/model_renderer.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||
|
||||
|
||||
class ModelRendererSP:
|
||||
def __init__(self):
|
||||
self.rainbow_path = RainbowPath()
|
||||
self.chevron_metrics = ChevronMetrics()
|
||||
78
selfdrive/ui/sunnypilot/onroad/rainbow_path.py
Normal file
78
selfdrive/ui/sunnypilot/onroad/rainbow_path.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""
|
||||
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 time
|
||||
import colorsys
|
||||
import pyray as rl
|
||||
from openpilot.system.ui.lib.shader_polygon import draw_polygon, Gradient
|
||||
|
||||
|
||||
class RainbowPath:
|
||||
DEFAULT_NUM_SEGMENTS = 8
|
||||
DEFAULT_SPEED = 50.0 # degrees per second
|
||||
DEFAULT_SATURATION = 0.9
|
||||
DEFAULT_LIGHTNESS = 0.6
|
||||
BASE_ALPHA = 0.8
|
||||
ALPHA_FADE = 0.3 # Alpha reduction from bottom to top
|
||||
|
||||
def __init__(self, num_segments: int = None, speed: float = None, saturation: float = None, lightness: float = None):
|
||||
self.num_segments = num_segments if num_segments is not None else self.DEFAULT_NUM_SEGMENTS
|
||||
self.speed = speed if speed is not None else self.DEFAULT_SPEED
|
||||
self.saturation = saturation if saturation is not None else self.DEFAULT_SATURATION
|
||||
self.lightness = lightness if lightness is not None else self.DEFAULT_LIGHTNESS
|
||||
|
||||
def set_speed(self, speed: float):
|
||||
self.speed = speed
|
||||
|
||||
def set_num_segments(self, num_segments: int):
|
||||
self.num_segments = num_segments
|
||||
|
||||
def set_saturation(self, saturation: float):
|
||||
self.saturation = max(0.0, min(1.0, saturation))
|
||||
|
||||
def set_lightness(self, lightness: float):
|
||||
self.lightness = max(0.0, min(1.0, lightness))
|
||||
|
||||
def get_gradient(self) -> Gradient:
|
||||
time_offset = time.monotonic()
|
||||
hue_offset = (time_offset * self.speed) % 360.0
|
||||
|
||||
segment_colors = []
|
||||
gradient_stops = []
|
||||
|
||||
for i in range(self.num_segments):
|
||||
position = i / (self.num_segments - 1)
|
||||
hue = (hue_offset + position * 360.0) % 360.0
|
||||
alpha = self.BASE_ALPHA * (1.0 - position * self.ALPHA_FADE)
|
||||
color = self._hsla_to_color(
|
||||
hue / 360.0,
|
||||
self.saturation,
|
||||
self.lightness,
|
||||
alpha
|
||||
)
|
||||
gradient_stops.append(position)
|
||||
segment_colors.append(color)
|
||||
|
||||
return Gradient(
|
||||
start=(0.0, 1.0), # Bottom of path
|
||||
end=(0.0, 0.0), # Top of path
|
||||
colors=segment_colors,
|
||||
stops=gradient_stops,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _hsla_to_color(h: float, s: float, l: float, a: float) -> rl.Color:
|
||||
rgb = colorsys.hls_to_rgb(h, l, s)
|
||||
return rl.Color(
|
||||
int(rgb[0] * 255),
|
||||
int(rgb[1] * 255),
|
||||
int(rgb[2] * 255),
|
||||
int(a * 255)
|
||||
)
|
||||
|
||||
def draw_rainbow_path(self, rect, path):
|
||||
gradient = self.get_gradient()
|
||||
draw_polygon(rect, path.projected_points, gradient=gradient)
|
||||
@@ -4,10 +4,13 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from cereal import messaging, custom
|
||||
from cereal import messaging, log, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
|
||||
OpenpilotState = log.SelfdriveState.OpenpilotState
|
||||
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
|
||||
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
@@ -19,11 +22,69 @@ class UIStateSP:
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
|
||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.global_brightness_override: int = self.params.get("Brightness", return_default=True)
|
||||
|
||||
def update(self) -> None:
|
||||
self.sunnylink_state.start()
|
||||
if self.sunnylink_enabled:
|
||||
self.sunnylink_state.start()
|
||||
else:
|
||||
self.sunnylink_state.stop()
|
||||
|
||||
@staticmethod
|
||||
def update_status(ss, ss_sp, onroad_evt) -> str:
|
||||
state = ss.state
|
||||
mads = ss_sp.mads
|
||||
mads_state = mads.state
|
||||
|
||||
if state == OpenpilotState.preEnabled:
|
||||
return "override"
|
||||
|
||||
if state == OpenpilotState.overriding:
|
||||
if not mads.available:
|
||||
return "override"
|
||||
|
||||
if any(e.overrideLongitudinal for e in onroad_evt):
|
||||
return "override"
|
||||
|
||||
if mads_state in (MADSState.paused, MADSState.overriding):
|
||||
return "override"
|
||||
|
||||
# MADS specific statuses
|
||||
if not mads.available:
|
||||
return "engaged" if ss.enabled else "disengaged"
|
||||
|
||||
if not mads.enabled and not ss.enabled:
|
||||
return "disengaged"
|
||||
|
||||
if mads.enabled and ss.enabled:
|
||||
return "engaged"
|
||||
|
||||
if mads.enabled:
|
||||
return "lat_only"
|
||||
|
||||
if ss.enabled:
|
||||
return "long_only"
|
||||
|
||||
return "disengaged"
|
||||
|
||||
def update_params(self) -> None:
|
||||
CP_SP_bytes = self.params.get("CarParamsSPPersistent")
|
||||
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")
|
||||
self.rainbow_path = self.params.get_bool("RainbowMode")
|
||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
||||
self.custom_interactive_timeout = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.global_brightness_override = self.params.get("Brightness", return_default=True)
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
def __init__(self):
|
||||
self._params = Params()
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on and self._params.get("DeviceBootMode", return_default=True) == 1:
|
||||
self._params.put_bool("OffroadMode", True)
|
||||
|
||||
@@ -13,7 +13,7 @@ if "RECORD_OUTPUT" not in os.environ:
|
||||
os.environ["RECORD_OUTPUT"] = os.path.join(DIFF_OUT_DIR, os.environ["RECORD_OUTPUT"])
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, MouseEvent
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.ui.mici.layouts.main import MiciMainLayout
|
||||
@@ -45,6 +45,8 @@ def setup_state():
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("DongleId", "test123456789")
|
||||
params.put("UpdaterCurrentDescription", "0.10.1 / test-branch / abc1234 / Nov 30")
|
||||
params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ from openpilot.common.prefix import OpenpilotPrefix
|
||||
from openpilot.selfdrive.test.helpers import with_processes
|
||||
from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert
|
||||
from openpilot.system.updated.updated import parse_release_notes
|
||||
from openpilot.system.version import terms_version, training_version
|
||||
from openpilot.system.version import terms_version, training_version, terms_version_sp, sunnylink_consent_version
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
AlertStatus = log.SelfdriveState.AlertStatus
|
||||
@@ -378,6 +378,8 @@ def create_screenshots():
|
||||
# Set terms and training version (to skip onboarding)
|
||||
params.put("HasAcceptedTerms", terms_version)
|
||||
params.put("CompletedTrainingVersion", training_version)
|
||||
params.put("HasAcceptedTermsSP", terms_version_sp)
|
||||
params.put("CompletedSunnylinkConsentVersion", sunnylink_consent_version)
|
||||
|
||||
if name == "homescreen_paired":
|
||||
params.put("PrimeType", 0) # NONE
|
||||
|
||||
@@ -12,7 +12,7 @@ from openpilot.selfdrive.ui.lib.prime_state import PrimeState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.hardware import HARDWARE, PC
|
||||
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP, DeviceSP
|
||||
|
||||
BACKLIGHT_OFFROAD = 65 if HARDWARE.get_device_type() == "mici" else 50
|
||||
|
||||
@@ -21,6 +21,8 @@ class UIStatus(Enum):
|
||||
DISENGAGED = "disengaged"
|
||||
ENGAGED = "engaged"
|
||||
OVERRIDE = "override"
|
||||
LAT_ONLY = "lat_only"
|
||||
LONG_ONLY = "long_only"
|
||||
|
||||
|
||||
class UIState(UIStateSP):
|
||||
@@ -98,7 +100,7 @@ class UIState(UIStateSP):
|
||||
|
||||
@property
|
||||
def engaged(self) -> bool:
|
||||
return self.started and self.sm["selfdriveState"].enabled
|
||||
return self.started and (self.sm["selfdriveState"].enabled or self.sm["selfdriveStateSP"].mads.enabled)
|
||||
|
||||
def is_onroad(self) -> bool:
|
||||
return self.started
|
||||
@@ -156,6 +158,8 @@ class UIState(UIStateSP):
|
||||
else:
|
||||
self.status = UIStatus.ENGAGED if ss.enabled else UIStatus.DISENGAGED
|
||||
|
||||
self.status = UIStatus(UIStateSP.update_status(ss, self.sm["selfdriveStateSP"], self.sm["onroadEvents"]))
|
||||
|
||||
# Check for engagement state changes
|
||||
if self.engaged != self._engaged_prev:
|
||||
for callback in self._engaged_transition_callbacks:
|
||||
@@ -188,10 +192,12 @@ class UIState(UIStateSP):
|
||||
self._param_update_time = time.monotonic()
|
||||
|
||||
|
||||
class Device:
|
||||
class Device(DeviceSP):
|
||||
def __init__(self):
|
||||
DeviceSP.__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
|
||||
@@ -205,11 +211,24 @@ 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
|
||||
|
||||
if gui_app.sunnypilot_ui() and ui_state.custom_interactive_timeout != 0:
|
||||
return ui_state.custom_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)
|
||||
@@ -217,7 +236,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()
|
||||
@@ -239,9 +258,18 @@ class Device:
|
||||
else:
|
||||
clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0
|
||||
|
||||
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
|
||||
if gui_app.sunnypilot_ui():
|
||||
if ui_state.global_brightness_override <= 0:
|
||||
min_global_brightness = 1 if ui_state.global_brightness_override < 0 else 30
|
||||
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [min_global_brightness, 100]))
|
||||
else:
|
||||
clipped_brightness = float(np.interp(clipped_brightness, [0, 1], [30, 100]))
|
||||
|
||||
brightness = round(self._brightness_filter.update(clipped_brightness))
|
||||
|
||||
if gui_app.sunnypilot_ui() and ui_state.global_brightness_override > 0:
|
||||
brightness = ui_state.global_brightness_override
|
||||
|
||||
if not self._awake:
|
||||
brightness = 0
|
||||
|
||||
@@ -257,7 +285,7 @@ class Device:
|
||||
self._ignition = ui_state.ignition
|
||||
|
||||
if ignition_just_turned_off 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:
|
||||
@@ -269,6 +297,7 @@ class Device:
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(self, on)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
|
||||
129
sunnypilot/common/README.md
Normal file
129
sunnypilot/common/README.md
Normal file
@@ -0,0 +1,129 @@
|
||||
<center>
|
||||
|
||||
# Comparative Analysis of Parameter Getter Methods: Params vs. ParamWatcher
|
||||
|
||||
James (sunnypilot Developer) <br>
|
||||
December 13, 2025
|
||||
|
||||
</center>
|
||||
|
||||
|
||||
## <br><br> Abstract
|
||||
|
||||
This research report examines the inefficiencies in standard parameter access methods within sunnypilot and proposes an optimized alternative, ParamWatcher. The standard `Params::get()` method incurs significant CPU and memory overhead due to repeated file I/O operations (sunnypilot, 2025). ParamWatcher utilizes OS-level file system events (inotify on Linux, FSEvents on macOS) to maintain an in-memory cache, reducing I/O to near zero (Linux man-pages, 2025-c; Apple Inc., n.d.-a). Empirical benchmarks with ~10 million parameter accesses demonstrate a 14.5x CPU speedup and flat memory usage (514.7 KB vs. 497.7 KB for base Params, with only 17 KB overhead). The implementation employs a process-local singleton pattern for efficiency in multi-process architectures (Gamma et al., 1994). Results indicate ParamWatcher eliminates UI stutters and GC pauses, enhancing system responsiveness without compromising data freshness.
|
||||
|
||||
**Keywords:** parameter access, file I/O optimization, event-driven caching, autonomous driving systems, performance benchmarking
|
||||
|
||||
## Introduction
|
||||
|
||||
In sunnypilot, efficient parameter management is important for real-time system access. The standard `Params::get()` method, implemented in C++ and wrapped in Python, performs full file I/O cycles for each access, leading to high CPU overhead and memory churn (sunnypilot, 2025). This is particularly problematic in UI loops where parameters are queried frequently (e.g., 50 toggles at 20 FPS equates to ~1,000 reads/second).
|
||||
|
||||
This inefficiency stems from architectural mismatches: C++ streams are designed for throughput, not latency (cppreference.com, n.d.-a). Each call triggers kernel mode switches, heap allocations, and garbage collection in Python, causing UI stutters (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
|
||||
|
||||
### Inefficiencies in Standard Parameter Access
|
||||
The standard `Params::get()` method executes a full file I/O lifecycle—opening, allocating, reading, and closing—for every function call. This results in significant CPU overhead and memory churn due to the frequency of these operations in the user interface loop.
|
||||
|
||||
#### System Overhead Analysis
|
||||
- **System Call Overhead**: Every read operation requires context switches into kernel mode. The `Params::get` function calls `util::read_file` (sunnypilot, 2025), which subsequently invokes `std::ifstream` (sunnypilot, 2025).
|
||||
- **Impact**: Frequent context switching degrades performance (Linux man-pages, 2025-a; Linux man-pages, 2025-b).
|
||||
- **C++ Stream Overhead**: The use of `std::ifstream` introduces additional overhead for maintaining stream state and buffering compared to raw file descriptors (cppreference.com, n.d.-a; Codezup, 2025).
|
||||
- **Memory Churn**: The instantiation of `std::string result(size, '\0');` forces heap allocation and deallocation during every call (sunnypilot, 2025). This stresses the memory allocator and can lead to fragmentation (cppreference.com, n.d.-b).
|
||||
|
||||
This report introduces ParamWatcher, an event-driven caching solution using OS file system events. It shifts from polling to notifications, caching converted values in static RAM. I propose that ParamWatcher achieves minimum 10x+ CPU gains with bounded non increasing memory, improving sunnypilot's performance both on latency and responsiveness.
|
||||
|
||||
## Method
|
||||
|
||||
### Materials
|
||||
- **System:** sunnypilot, running on macOS, Ubuntu/Linux, comma 3x, and comma four.
|
||||
- **Parameters:** 231 defined keys in `param_keys.h`.
|
||||
- **Tools:** Python 3, tracemalloc for memory profiling, time.perf_counter for CPU timing, ctypes for OS integration (Python Software Foundation, 2025).
|
||||
|
||||
### Implementation Details
|
||||
ParamWatcher provides cross-platform file system monitoring using ctypes for direct OS integration (Python Software Foundation, 2025).
|
||||
|
||||
#### Linux Implementation
|
||||
On Linux, ParamWatcher uses the inotify subsystem for efficient file change detection (Linux man-pages, 2025-c). It loads `libc.so.6` to access system calls, initializes an inotify instance, and watches the parameters directory for events like `IN_MODIFY` and `IN_CLOSE_WRITE` (Linux Kernel Organization, 2005). Events are polled with `select.epoll()` and parsed using `struct.unpack_from()` to avoid ctypes overhead. Filenames are extracted and passed to cache invalidation, ensuring real-time updates without polling (Codezup, 2025).
|
||||
|
||||
- **Library Loading**: `libc = ctypes.CDLL('libc.so.6')` loads the standard C library to access system calls.
|
||||
- **Initialization**: `inotify_init()` is called to create a new inotify instance, returning a file descriptor.
|
||||
- **Watch Setup**: `inotify_add_watch(fd, path, mask)` registers the parameters directory. The mask includes `IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE` (Linux Kernel Organization, 2005) to capture all relevant file changes.
|
||||
- **Event Loop**:
|
||||
- **Polling**: `select.epoll()` is used to efficiently wait for activity on the file descriptor without busy-waiting.
|
||||
- **Reading**: When events occur, `os.read(fd, 2048)` retrieves the raw binary event data.
|
||||
- **Parsing**: The code uses Python's `struct` module (`struct.unpack_from("iIII", ...)`) to parse the C-style `inotify_event` structures directly from the buffer, avoiding the overhead of defining `ctypes` structures.
|
||||
- **Handling**: Extracted filenames are passed to `_trigger_callbacks`, which invalidates the specific cache entry (`self._cache.pop(path, None)`), forcing a fresh read on the next access.
|
||||
|
||||
#### macOS Implementation
|
||||
On macOS, ParamWatcher leverages FSEvents from CoreServices for directory monitoring (Apple Inc., n.d.-a). It defines a C-compatible callback using `CFUNCTYPE`, creates an `FSEventStream` with `kFSEventStreamCreateFlagFileEvents`, and schedules it on the run loop (Apple Inc., n.d.-b). Events are filtered for modifications, creations, and renames (Apple Inc., n.d.-c), triggering cache invalidation for affected parameters.
|
||||
|
||||
- **Framework Loading**: `ctypes.cdll.LoadLibrary` loads `CoreServices` and `CoreFoundation`.
|
||||
- **Callback Definition**: `CFUNCTYPE` is used to define a C-compatible callback function. This function is invoked by the OS whenever a change occurs in the watched directory.
|
||||
- **Stream Creation**: `FSEventStreamCreate` creates a stream for the target directory. The `kFSEventStreamCreateFlagFileEvents` flag is used to request file-level granularity where available.
|
||||
- **Event Filtering**: The callback filters events using flags such as `kFSEventStreamEventFlagItemCreated` and `kFSEventStreamEventFlagItemModified` to ensure only relevant file changes trigger updates (Apple Inc., n.d.-c).
|
||||
- **Scheduling**: `FSEventStreamScheduleWithRunLoop` attaches the stream to the current thread's run loop (Apple Inc., n.d.-b).
|
||||
- **Execution**: `CFRunLoopRun()` starts the event loop. This passes control to the OS, which wakes the thread only when necessary.
|
||||
- **Handling**: Inside the callback, the code iterates through the changed paths provided by the OS. It extracts the filename and calls `_trigger_callbacks` to invalidate the cache for that specific parameter.
|
||||
|
||||
### Procedure
|
||||
Benchmarks simulated heavy load:
|
||||
- **Memory Test:** ~10 million gets (43,290 loops over 231 keys), measured with tracemalloc.
|
||||
- **CPU Test:** Same load, timed with perf_counter.
|
||||
- Comparisons: Base Params vs. ParamWatcher.
|
||||
|
||||
## Results
|
||||
|
||||
### Memory Usage
|
||||
Using tracemalloc for peak memory measurement during ~10 million parameter accesses (43,290 loops over 231 keys), base Params peaked at 497.7 KB, while ParamWatcher peaked at 514.7 KB (17 KB overhead). ParamWatcher's memory remained flat post-initialization, preventing churn.
|
||||
|
||||
| Condition | Memory (KB) | Overhead |
|
||||
|-----------|-------------|----------|
|
||||
| Base Params | 497.7 | - |
|
||||
| ParamWatcher | 514.7 | 17 KB |
|
||||
### CPU Performance
|
||||
ParamWatcher was 14.5x faster: 4.52s vs. 65.43s to complete ~10 million param gets.
|
||||
|
||||
| Condition | Time (s) | Speedup |
|
||||
|-----------|----------|---------|
|
||||
| Base Params | 65.43 | 1x |
|
||||
| ParamWatcher | 4.52 | 14.5x |
|
||||
|
||||
### Scalability
|
||||
No degradation at scale; cache invalidation maintained freshness.
|
||||
|
||||
## Discussion
|
||||
|
||||
ParamWatcher successfully optimizes parameter access, delivering substantial CPU gains with minimal memory overhead. The event-driven approach eliminates I/O bottlenecks, reducing GC pressure and UI stutters (cppreference.com, n.d.-b). The 17 KB memory overhead is negligible compared to the megabytes of churn from base Params, ensuring bounded usage in multi-process environments via the singleton pattern (Gamma et al., 1994).
|
||||
|
||||
Results demonstrate scalability without degradation, with cache invalidation maintaining data freshness. This optimization enhances system responsiveness.
|
||||
Limitations include potential event latency in high-load scenarios (<10 ms, imperceptible for UI) and increased complexity from background threads.
|
||||
Trade-offs: Static RAM (~17 KB) vs. dynamic churn; benefits outweigh costs for param-heavy workloads.
|
||||
|
||||
## <br>References
|
||||
|
||||
Apple Inc. (n.d.-a). *File System Events*. Retrieved from https://developer.apple.com/documentation/coreservices/file_system_events
|
||||
|
||||
Apple Inc. (n.d.-b). *CFRunLoop*. Retrieved from https://developer.apple.com/documentation/corefoundation/cfrunloop
|
||||
|
||||
Apple Inc. (n.d.-c). *FSEventStreamEventFlags*. Retrieved from https://developer.apple.com/documentation/coreservices/1455361-fseventstreameventflags
|
||||
|
||||
Codezup. (2025). *Efficient File I/O in C++*. Retrieved from https://codezup.com/efficient-file-io-cpp-best-practices/
|
||||
|
||||
cppreference.com. (n.d.-a). *std::basic_ifstream*. Retrieved from https://en.cppreference.com/w/cpp/io/basic_ifstream
|
||||
|
||||
cppreference.com. (n.d.-b). *std::basic_string*. Retrieved from https://en.cppreference.com/w/cpp/string/basic_string/basic_string
|
||||
|
||||
Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). *Design Patterns: Elements of Reusable Object-Oriented Software*. Addison-Wesley.
|
||||
|
||||
Linux Kernel Organization. (2005). *include/uapi/linux/inotify.h*. Retrieved from https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/inotify.h
|
||||
|
||||
Linux man-pages. (2025-a). *open(2)*. Retrieved from https://man7.org/linux/man-pages/man2/open.2.html
|
||||
|
||||
Linux man-pages. (2025-b). *read(2)*. Retrieved from https://man7.org/linux/man-pages/man2/read.2.html
|
||||
|
||||
Linux man-pages. (2025-c). *inotify(7)*. Retrieved from https://man7.org/linux/man-pages/man7/inotify.7.html
|
||||
|
||||
Python Software Foundation. (2025). *ctypes — A foreign function library for Python*. Retrieved from https://docs.python.org/3/library/ctypes.html
|
||||
|
||||
sunnypilot. (2025). *common/params.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/params.cc#L180C1-L206C2
|
||||
|
||||
sunnypilot. (2025). *common/util.cc* [Source code]. GitHub. https://github.com/sunnypilot/sunnypilot/blob/master/common/util.cc#L79C1-L117C2
|
||||
0
sunnypilot/common/__init__.py
Normal file
0
sunnypilot/common/__init__.py
Normal file
193
sunnypilot/common/param_watcher.py
Normal file
193
sunnypilot/common/param_watcher.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
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 os
|
||||
import platform
|
||||
import struct
|
||||
import select
|
||||
import threading
|
||||
import time
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import traceback
|
||||
from ctypes import c_void_p, c_size_t, POINTER, c_uint32, c_uint64
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
IN_MODIFY = 0x00000002
|
||||
IN_CREATE = 0x00000100
|
||||
IN_DELETE = 0x00000200
|
||||
IN_MOVED_TO = 0x00000080
|
||||
IN_CLOSE_WRITE = 0x00000008
|
||||
|
||||
|
||||
class ParamWatcher(Params):
|
||||
_instance = None
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super().__new__(cls)
|
||||
cls._instance._initialized = False
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
if self._initialized:
|
||||
return
|
||||
super().__init__()
|
||||
cloudlog.warning("ParamWatcher initialized")
|
||||
self._cache = {}
|
||||
self._last_trigger = {}
|
||||
self._version = {}
|
||||
self._lock = threading.Lock()
|
||||
self._callbacks = []
|
||||
self.last_accessed_param = None
|
||||
self._initialized = True
|
||||
self.start()
|
||||
|
||||
def start(self):
|
||||
if getattr(self, '_thread', None) and self._thread.is_alive():
|
||||
return
|
||||
self._thread = threading.Thread(target=self._run_watcher, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def is_watching(self):
|
||||
return getattr(self, '_thread', None) and self._thread.is_alive()
|
||||
|
||||
def add_watcher(self, callback):
|
||||
if callback not in self._callbacks:
|
||||
self._callbacks.append(callback)
|
||||
|
||||
def _trigger_callbacks(self, path):
|
||||
with self._lock:
|
||||
if (now := time.monotonic()) - self._last_trigger.get(path, 0) < 0.1:
|
||||
return
|
||||
self._last_trigger[path] = now
|
||||
self._version[path] = self._version.get(path, 0) + 1
|
||||
self._cache.pop(path, None)
|
||||
|
||||
for callback in self._callbacks:
|
||||
try:
|
||||
callback(path)
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher callback failed")
|
||||
|
||||
def _get_cached(self, key, getter, sig):
|
||||
k = str(key)
|
||||
with self._lock:
|
||||
bucket = self._cache.get(k)
|
||||
if bucket and sig in bucket:
|
||||
if bucket[sig][0] == self._version.get(k, 0):
|
||||
return bucket[sig][1]
|
||||
|
||||
start_ver = self._version.get(k, 0)
|
||||
val = getter()
|
||||
with self._lock:
|
||||
if self._version.get(k, 0) != start_ver:
|
||||
val = getter()
|
||||
self._cache.setdefault(k, {})[sig] = (self._version.get(k, 0), val)
|
||||
return val
|
||||
|
||||
def get(self, key, block=False, return_default=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get(key, block, return_default)
|
||||
fetcher = super().get
|
||||
return self._get_cached(key, lambda: fetcher(key, block, return_default), (block, return_default))
|
||||
|
||||
def get_bool(self, key, block=False):
|
||||
self.last_accessed_param = key
|
||||
if block:
|
||||
return super().get_bool(key, block)
|
||||
fetcher = super().get_bool
|
||||
return self._get_cached(key, lambda: fetcher(key, block), ("bool", block))
|
||||
|
||||
def _run_watcher(self):
|
||||
system = platform.system()
|
||||
while True:
|
||||
try:
|
||||
if system == "Linux":
|
||||
self._run_linux()
|
||||
elif system == "Darwin":
|
||||
self._run_darwin()
|
||||
except Exception:
|
||||
cloudlog.exception("Param watcher crashed")
|
||||
time.sleep(2)
|
||||
|
||||
def _run_linux(self):
|
||||
path = Paths.params_root()
|
||||
libc = ctypes.CDLL('libc.so.6')
|
||||
fd = libc.inotify_init()
|
||||
libc.inotify_add_watch(fd, path.encode(), IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE)
|
||||
|
||||
try:
|
||||
poll = select.epoll()
|
||||
poll.register(fd, select.EPOLLIN)
|
||||
while True:
|
||||
for fileno, _ in poll.poll():
|
||||
if fileno == fd:
|
||||
buffer = os.read(fd, 2048)
|
||||
i = 0
|
||||
while i + 16 <= len(buffer):
|
||||
_, mask, _, name_len = struct.unpack_from("iIII", buffer, i)
|
||||
if mask & (IN_MODIFY | IN_CREATE | IN_DELETE | IN_MOVED_TO | IN_CLOSE_WRITE):
|
||||
name = buffer[i+16:i+16+name_len].rstrip(b"\0").decode()
|
||||
if not name.startswith("."):
|
||||
self._trigger_callbacks(name)
|
||||
i += 16 + name_len
|
||||
finally:
|
||||
if 'poll' in locals():
|
||||
poll.unregister(fd)
|
||||
poll.close()
|
||||
os.close(fd)
|
||||
|
||||
def _run_darwin(self):
|
||||
CS = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreServices"))
|
||||
CF = ctypes.cdll.LoadLibrary(ctypes.util.find_library("CoreFoundation"))
|
||||
|
||||
kCFAllocatorDefault = c_void_p(0)
|
||||
kCFStringEncodingUTF8 = 0x08000100
|
||||
kFSEventStreamCreateFlagFileEvents = 0x00000010
|
||||
kFSEventStreamEventFlagItemCreated = 0x00000100
|
||||
kFSEventStreamEventFlagItemRemoved = 0x00000200
|
||||
kFSEventStreamEventFlagItemRenamed = 0x00000800
|
||||
kFSEventStreamEventFlagItemModified = 0x00001000
|
||||
|
||||
CF.CFStringCreateWithCString.restype = c_void_p
|
||||
CF.CFStringCreateWithCString.argtypes = [c_void_p, ctypes.c_char_p, c_uint32]
|
||||
CF.CFArrayCreate.restype = c_void_p
|
||||
CF.CFArrayCreate.argtypes = [c_void_p, POINTER(c_void_p), c_size_t, c_void_p]
|
||||
CS.FSEventStreamCreate.restype = c_void_p
|
||||
CS.FSEventStreamCreate.argtypes = [c_void_p, c_void_p, c_void_p, c_void_p, c_uint64, ctypes.c_double, c_uint32]
|
||||
CS.FSEventStreamScheduleWithRunLoop.argtypes = [c_void_p, c_void_p, c_void_p]
|
||||
CS.FSEventStreamStart.argtypes = [c_void_p]
|
||||
CF.CFRunLoopGetCurrent.restype = c_void_p
|
||||
|
||||
def _cb(stream, ctx, num, paths, flags, ids):
|
||||
try:
|
||||
paths_arr = ctypes.cast(paths, POINTER(c_void_p))
|
||||
flags_arr = ctypes.cast(flags, POINTER(c_uint32))
|
||||
for i in range(num):
|
||||
path = ctypes.cast(paths_arr[i], ctypes.c_char_p).value
|
||||
if path and (flags_arr[i] & (kFSEventStreamEventFlagItemCreated | kFSEventStreamEventFlagItemRemoved |
|
||||
kFSEventStreamEventFlagItemRenamed | kFSEventStreamEventFlagItemModified)):
|
||||
self._trigger_callbacks(os.path.basename(path.decode('utf-8').rstrip('/')))
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
|
||||
self._darwin_cb = ctypes.CFUNCTYPE(None, c_void_p, c_void_p, c_size_t, c_void_p, POINTER(c_uint32), POINTER(c_uint64))(_cb)
|
||||
|
||||
path_str = Paths.params_root().encode('utf-8')
|
||||
cf_path = CF.CFStringCreateWithCString(kCFAllocatorDefault, path_str, kCFStringEncodingUTF8)
|
||||
cf_paths = CF.CFArrayCreate(kCFAllocatorDefault, (c_void_p * 1)(cf_path), 1, None)
|
||||
stream = CS.FSEventStreamCreate(kCFAllocatorDefault, self._darwin_cb, None, cf_paths, -1, 0.05, kFSEventStreamCreateFlagFileEvents)
|
||||
|
||||
run_loop = CF.CFRunLoopGetCurrent()
|
||||
kDefaultMode = CF.CFStringCreateWithCString(kCFAllocatorDefault, b"kCFRunLoopDefaultMode", kCFStringEncodingUTF8)
|
||||
CS.FSEventStreamScheduleWithRunLoop(stream, run_loop, kDefaultMode)
|
||||
CS.FSEventStreamStart(stream)
|
||||
CF.CFRunLoopRun()
|
||||
4
sunnypilot/common/params.py
Normal file
4
sunnypilot/common/params.py
Normal file
@@ -0,0 +1,4 @@
|
||||
from openpilot.common.params_pyx import ParamKeyFlag, ParamKeyType, UnknownKeyName
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher as Params
|
||||
|
||||
__all__ = ["Params", "ParamKeyFlag", "ParamKeyType", "UnknownKeyName"]
|
||||
0
sunnypilot/common/tests/__init__.py
Normal file
0
sunnypilot/common/tests/__init__.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
94
sunnypilot/common/tests/test_param_watcher.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import time
|
||||
import pytest
|
||||
import threading
|
||||
import tracemalloc
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.params_pyx import UnknownKeyName
|
||||
|
||||
from openpilot.sunnypilot.common.param_watcher import ParamWatcher
|
||||
|
||||
|
||||
class TestParamWatcher:
|
||||
BYTES_KEYS = ["LocationFilterInitialState", "UpdaterCurrentReleaseNotes", "UpdaterNewReleaseNotes"]
|
||||
BOOL_KEYS = [
|
||||
"IsMetric", "AdbEnabled", "AlwaysOnDM", "ExperimentalMode",
|
||||
"ExperimentalModeConfirmed", "DisengageOnAccelerator",
|
||||
"OpenpilotEnabledToggle", "RecordAudio", "RecordFront"
|
||||
]
|
||||
_key_counter = 0
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def setup_method(self):
|
||||
self.params = Params()
|
||||
self.key_index = TestParamWatcher._key_counter
|
||||
TestParamWatcher._key_counter += 1
|
||||
self.bytes_key = self.BYTES_KEYS[self.key_index % len(self.BYTES_KEYS)]
|
||||
self.bool_key = self.BOOL_KEYS[self.key_index % len(self.BOOL_KEYS)]
|
||||
|
||||
@pytest.fixture
|
||||
def param_watcher(self):
|
||||
ParamWatcher._instance = None
|
||||
param_watch = ParamWatcher()
|
||||
param_watch.start()
|
||||
assert param_watch.is_watching(), "ParamWatcher thread died"
|
||||
return param_watch
|
||||
|
||||
def teardown_method(self):
|
||||
for key in (self.bytes_key, self.bool_key):
|
||||
try:
|
||||
self.params.remove(key)
|
||||
except UnknownKeyName:
|
||||
pass
|
||||
|
||||
def test_watcher_detects_change(self, param_watcher):
|
||||
val = b"123"
|
||||
self.params.put(self.bytes_key, val)
|
||||
assert param_watcher.get(self.bytes_key) == val
|
||||
|
||||
def test_watcher_get_bool(self, param_watcher):
|
||||
self.params.put_bool(self.bool_key, True)
|
||||
assert param_watcher.get_bool(self.bool_key) is True # First read should populate internal cache
|
||||
|
||||
def test_performance_comparison(self, param_watcher):
|
||||
plain_params = self.params
|
||||
|
||||
for key in self.BYTES_KEYS:
|
||||
plain_params.put(key, b"x" * 10000)
|
||||
param_watcher.get(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
plain_params.put_bool(key, True)
|
||||
param_watcher.get_bool(key)
|
||||
|
||||
def bench(get_bytes, get_bool):
|
||||
tracemalloc.start()
|
||||
start_time = time.process_time()
|
||||
for _ in range(1000):
|
||||
for key in self.BYTES_KEYS:
|
||||
get_bytes(key)
|
||||
for key in self.BOOL_KEYS:
|
||||
get_bool(key)
|
||||
duration = time.process_time() - start_time
|
||||
_, memory = tracemalloc.get_traced_memory()
|
||||
tracemalloc.stop()
|
||||
return duration, memory
|
||||
|
||||
plain_cpu, plain_memory = bench(plain_params.get, plain_params.get_bool)
|
||||
watcher_cpu, watcher_memory = bench(param_watcher.get, param_watcher.get_bool)
|
||||
|
||||
# ParamWatcher *should* be significantly faster and use less memory than Params()
|
||||
assert watcher_cpu < plain_cpu * 0.6, f"PW CPU ({watcher_cpu:.4f}s) should be < 60% of Param call ({plain_cpu:.4f}s)"
|
||||
assert watcher_memory < plain_memory * 0.5, f"PW Memory ({watcher_memory}B) should be < 50% of Param call ({plain_memory}B)"
|
||||
|
||||
def test_cache_invalidation_simulation(self, param_watcher):
|
||||
self.params.put(self.bytes_key, b"old")
|
||||
assert param_watcher.get(self.bytes_key) == b"old"
|
||||
time.sleep(0.2)
|
||||
|
||||
event = threading.Event()
|
||||
param_watcher.add_watcher(lambda key: event.set())
|
||||
param_watcher._trigger_callbacks(self.bytes_key)
|
||||
assert event.wait(timeout=2), "Callback not triggered"
|
||||
|
||||
self.params.put(self.bytes_key, b"new")
|
||||
assert param_watcher.get(self.bytes_key) == b"new"
|
||||
@@ -24,6 +24,7 @@ from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime
|
||||
from openpilot.sunnypilot.modeld.parse_model_outputs import Parser
|
||||
from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan
|
||||
from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants
|
||||
from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext
|
||||
@@ -195,6 +196,7 @@ def main(demo=False):
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -250,12 +252,14 @@ def main(demo=False):
|
||||
frame_id = sm["roadCameraState"].frameId
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
|
||||
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
traffic_convention = np.zeros(2)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import glob
|
||||
|
||||
Import('env', 'envCython', 'arch', 'cereal', 'messaging', 'common', 'visionipc', 'transformations')
|
||||
@@ -28,3 +29,38 @@ for pathdef, fn in {'TRANSFORM': 'transforms/transform.cl', 'LOADYUV': 'transfor
|
||||
cython_libs = envCython["LIBS"] + libs
|
||||
commonmodel_lib = lenv.Library('commonmodel', common_src)
|
||||
lenvCython.Program('models/commonmodel_pyx.so', 'models/commonmodel_pyx.pyx', LIBS=[commonmodel_lib, *cython_libs], FRAMEWORKS=frameworks)
|
||||
tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "/**", recursive=True, root_dir=env.Dir("#").abspath) if 'pycache' not in x]
|
||||
|
||||
# Get model metadata
|
||||
PC = not os.path.isfile('/TICI')
|
||||
if PC:
|
||||
inputs = tinygrad_files + [File(Dir("#sunnypilot/modeld_v2").File("install_models_pc.py").abspath)]
|
||||
outputs = []
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
outputs.append(File(f"models/{model_name}_metadata.pkl"))
|
||||
if outputs:
|
||||
lenv.Command(outputs, inputs, cmd)
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
return lenv.Command(
|
||||
fn + "_tinygrad.pkl",
|
||||
[fn + ".onnx"] + tinygrad_files,
|
||||
f'{pythonpath_string} {flags} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {fn}_tinygrad.pkl'
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
flags = {
|
||||
'larch64': 'DEV=QCOM',
|
||||
'Darwin': f'DEV=CPU HOME={os.path.expanduser("~")} IMAGE=0', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 IMAGE=0')
|
||||
tg_compile(flags, model_name)
|
||||
|
||||
39
sunnypilot/modeld_v2/camera_offset_helper.py
Normal file
39
sunnypilot/modeld_v2/camera_offset_helper.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
|
||||
|
||||
class CameraOffsetHelper:
|
||||
def __init__(self):
|
||||
self.camera_offset = 0.0
|
||||
self.actual_camera_offset = 0.0
|
||||
|
||||
@staticmethod
|
||||
def apply_camera_offset(model_transform, intrinsics, height, offset_param):
|
||||
cy = intrinsics[1, 2]
|
||||
shear = np.eye(3, dtype=np.float32)
|
||||
shear[0, 1] = offset_param / height
|
||||
shear[0, 2] = -offset_param / height * cy
|
||||
model_transform = (shear @ model_transform).astype(np.float32)
|
||||
return model_transform
|
||||
|
||||
def set_offset(self, offset):
|
||||
self.camera_offset = offset
|
||||
|
||||
def update(self, model_transform_main, model_transform_extra, sm, main_wide_camera):
|
||||
self.actual_camera_offset = (0.9 * self.actual_camera_offset) + (0.1 * self.camera_offset)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
height = sm["liveCalibration"].height[0] if sm['liveCalibration'].height else 1.22
|
||||
|
||||
intrinsics_main = dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics
|
||||
model_transform_main = self.apply_camera_offset(model_transform_main, intrinsics_main, height, self.actual_camera_offset)
|
||||
|
||||
intrinsics_extra = dc.ecam.intrinsics
|
||||
model_transform_extra = self.apply_camera_offset(model_transform_extra, intrinsics_extra, height, self.actual_camera_offset)
|
||||
return model_transform_main, model_transform_extra
|
||||
89
sunnypilot/modeld_v2/install_models_pc.py
Executable file
89
sunnypilot/modeld_v2/install_models_pc.py
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import shutil
|
||||
import pickle
|
||||
import codecs
|
||||
import onnx
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
|
||||
|
||||
def get_name_and_shape(value_info):
|
||||
shape = tuple([int(dim.dim_value) for dim in value_info.type.tensor_type.shape.dim])
|
||||
return value_info.name, shape
|
||||
|
||||
|
||||
def get_metadata_value_by_name(model, name):
|
||||
for prop in model.metadata_props:
|
||||
if prop.key == name:
|
||||
return prop.value
|
||||
return None
|
||||
|
||||
|
||||
def generate_metadata_pkl(model_path, output_path):
|
||||
try:
|
||||
model = onnx.load(str(model_path))
|
||||
output_slices = get_metadata_value_by_name(model, 'output_slices')
|
||||
|
||||
if output_slices:
|
||||
metadata = {
|
||||
'model_checkpoint': get_metadata_value_by_name(model, 'model_checkpoint'),
|
||||
'output_slices': pickle.loads(codecs.decode(output_slices.encode(), "base64")),
|
||||
'input_shapes': dict([get_name_and_shape(x) for x in model.graph.input]),
|
||||
'output_shapes': dict([get_name_and_shape(x) for x in model.graph.output])
|
||||
}
|
||||
with open(output_path, 'wb') as f:
|
||||
pickle.dump(metadata, f)
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def install_models(model_dir):
|
||||
model_dir = Path(model_dir)
|
||||
models = ["driving_policy", "driving_vision"]
|
||||
found_models = []
|
||||
|
||||
for model in models:
|
||||
if (model_dir / f"{model}.onnx").exists():
|
||||
found_models.append(model)
|
||||
|
||||
if not found_models:
|
||||
return
|
||||
|
||||
try:
|
||||
custom_name = input(f"Found models ({', '.join(found_models)}). Enter model short name (e.g. wmiv4): ").strip()
|
||||
except EOFError:
|
||||
return
|
||||
|
||||
if not custom_name:
|
||||
print("No name provided, skipping installation.")
|
||||
return
|
||||
|
||||
dest_dir = Path(Paths.model_root())
|
||||
dest_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for model in found_models:
|
||||
onnx_path = model_dir / f"{model}.onnx"
|
||||
tinygrad_pkl = model_dir / f"{model}_tinygrad.pkl"
|
||||
metadata_pkl = model_dir / f"{model}_metadata.pkl"
|
||||
|
||||
if not metadata_pkl.exists():
|
||||
generate_metadata_pkl(onnx_path, metadata_pkl)
|
||||
|
||||
dest_tinygrad = dest_dir / f"{model}_{custom_name}_tinygrad.pkl"
|
||||
dest_metadata = dest_dir / f"{model}_{custom_name}_metadata.pkl"
|
||||
|
||||
if tinygrad_pkl.exists():
|
||||
shutil.move(str(tinygrad_pkl), str(dest_tinygrad))
|
||||
if metadata_pkl.exists():
|
||||
shutil.move(str(metadata_pkl), str(dest_metadata))
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: install_models_pc.py <model_dir>")
|
||||
sys.exit(1)
|
||||
install_models(sys.argv[1])
|
||||
@@ -21,6 +21,7 @@ from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_p
|
||||
from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext
|
||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
|
||||
from openpilot.sunnypilot.livedelay.helpers import get_lat_delay
|
||||
from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase
|
||||
@@ -28,7 +29,6 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.helpers import get_model_runner
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad"
|
||||
RECOVERY_POWER = 1.0 # The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong
|
||||
|
||||
|
||||
class FrameMeta:
|
||||
@@ -63,6 +63,7 @@ class ModelState(ModelStateBase):
|
||||
self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0"))
|
||||
self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0"))
|
||||
self.MIN_LAT_CONTROL_SPEED = 0.3
|
||||
self.PLANPLUS_CONTROL: float = 1.0
|
||||
|
||||
buffer_length = 5 if self.model_runner.is_20hz else 2
|
||||
self.frames = {name: DrivingModelFrame(context, buffer_length) for name in self.model_runner.vision_input_names}
|
||||
@@ -158,7 +159,8 @@ class ModelState(ModelStateBase):
|
||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
||||
plan = model_output['plan'][0]
|
||||
if 'planplus' in model_output:
|
||||
plan = plan + RECOVERY_POWER*model_output['planplus'][0]
|
||||
recovery_power = self.PLANPLUS_CONTROL * (0.75 if v_ego > 20.0 else 1.0)
|
||||
plan = plan + recovery_power * model_output['planplus'][0]
|
||||
desired_accel, should_stop = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||
action_t=long_action_t)
|
||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||
@@ -229,6 +231,7 @@ def main(demo=False):
|
||||
buf_main, buf_extra = None, None
|
||||
meta_main = FrameMeta()
|
||||
meta_extra = FrameMeta()
|
||||
camera_offset_helper = CameraOffsetHelper()
|
||||
|
||||
|
||||
if demo:
|
||||
@@ -283,13 +286,15 @@ def main(demo=False):
|
||||
v_ego = max(sm["carState"].vEgo, 0.)
|
||||
if sm.frame % 60 == 0:
|
||||
model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay)
|
||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||
if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']:
|
||||
device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32)
|
||||
dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))]
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics,
|
||||
False).astype(np.float32)
|
||||
model_transform_main = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics if main_wide_camera else dc.fcam.intrinsics, False).astype(np.float32)
|
||||
model_transform_extra = get_warp_matrix(device_from_calib_euler, dc.ecam.intrinsics, True).astype(np.float32)
|
||||
model_transform_main, model_transform_extra = camera_offset_helper.update(model_transform_main, model_transform_extra, sm, main_wide_camera)
|
||||
live_calib_seen = True
|
||||
|
||||
traffic_convention = np.zeros(2)
|
||||
|
||||
84
sunnypilot/modeld_v2/tests/test_camera_offset_helper.py
Normal file
84
sunnypilot/modeld_v2/tests/test_camera_offset_helper.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""
|
||||
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 numpy as np
|
||||
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.transformations.model import get_warp_matrix
|
||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||
|
||||
|
||||
class MockStruct:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return getattr(self, item)
|
||||
|
||||
|
||||
class TestCameraOffset:
|
||||
def setup_method(self):
|
||||
self.camera_offset = CameraOffsetHelper()
|
||||
self.dc = DEVICE_CAMERAS[('mici', 'os04c10')]
|
||||
|
||||
def test_smoothing(self):
|
||||
self.camera_offset.set_offset(0.2)
|
||||
|
||||
sm = MockStruct(
|
||||
deviceState=MockStruct(deviceType='mici'),
|
||||
roadCameraState=MockStruct(sensor='os04c10'),
|
||||
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
|
||||
)
|
||||
|
||||
intrinsics_main = self.dc.fcam.intrinsics
|
||||
intrinsics_extra = self.dc.ecam.intrinsics
|
||||
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
|
||||
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
|
||||
|
||||
self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.02)
|
||||
self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_almost_equal(self.camera_offset.actual_camera_offset, 0.038)
|
||||
|
||||
def test_camera_offset_(self):
|
||||
intrinsics = self.dc.fcam.intrinsics
|
||||
transform = np.eye(3, dtype=np.float32)
|
||||
height = 1.22
|
||||
offset = 0.1
|
||||
|
||||
cy = intrinsics[1, 2]
|
||||
expected_shear = np.eye(3, dtype=np.float32)
|
||||
expected_shear[0, 1] = offset / height
|
||||
expected_shear[0, 2] = -offset / height * cy
|
||||
|
||||
result = CameraOffsetHelper.apply_camera_offset(transform, intrinsics, height, offset)
|
||||
np.testing.assert_array_almost_equal(result, expected_shear)
|
||||
|
||||
def test_update(self):
|
||||
sm = MockStruct(
|
||||
deviceState=MockStruct(deviceType='mici'),
|
||||
roadCameraState=MockStruct(sensor='os04c10'),
|
||||
liveCalibration=MockStruct(rpyCalib=[0.0, 0.0, 0.0], height=[1.22])
|
||||
)
|
||||
intrinsics_main = self.dc.fcam.intrinsics
|
||||
intrinsics_extra = self.dc.ecam.intrinsics
|
||||
device_from_calib_euler = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
main_transform = get_warp_matrix(device_from_calib_euler, intrinsics_main, False).astype(np.float32)
|
||||
extra_transform = get_warp_matrix(device_from_calib_euler, intrinsics_extra, True).astype(np.float32)
|
||||
|
||||
self.camera_offset.set_offset(0.0) # test default offset doesn't change transformation
|
||||
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
np.testing.assert_array_equal(main_out, main_transform)
|
||||
np.testing.assert_array_equal(extra_out, extra_transform)
|
||||
|
||||
self.camera_offset.set_offset(0.2) # test valid offset changes transformation
|
||||
main_out, extra_out = self.camera_offset.update(main_transform, extra_transform, sm, False)
|
||||
assert not np.array_equal(main_out, main_transform)
|
||||
assert not np.array_equal(extra_out, extra_transform)
|
||||
assert main_out[0, 1] != 0.0
|
||||
assert main_out[0, 2] != 0.0
|
||||
@@ -1,102 +0,0 @@
|
||||
import numpy as np
|
||||
import random
|
||||
|
||||
import cereal.messaging as messaging
|
||||
from msgq.visionipc import VisionIpcServer, VisionStreamType
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.transformations.camera import DEVICE_CAMERAS
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.system.manager.process_config import managed_processes
|
||||
from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state
|
||||
|
||||
CAM = DEVICE_CAMERAS[("tici", "ar0231")].fcam
|
||||
IMG = np.zeros(int(CAM.width*CAM.height*(3/2)), dtype=np.uint8)
|
||||
IMG_BYTES = IMG.flatten().tobytes()
|
||||
|
||||
|
||||
class TestModeld:
|
||||
|
||||
def setup_method(self):
|
||||
self.vipc_server = VisionIpcServer("camerad")
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_DRIVER, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.create_buffers(VisionStreamType.VISION_STREAM_WIDE_ROAD, 40, CAM.width, CAM.height)
|
||||
self.vipc_server.start_listener()
|
||||
Params().put("CarParams", get_demo_car_params().to_bytes())
|
||||
|
||||
self.sm = messaging.SubMaster(['modelV2', 'cameraOdometry'])
|
||||
self.pm = messaging.PubMaster(['roadCameraState', 'wideRoadCameraState', 'liveCalibration'])
|
||||
|
||||
managed_processes['modeld'].start()
|
||||
self.pm.wait_for_readers_to_update("roadCameraState", 10)
|
||||
|
||||
def teardown_method(self):
|
||||
managed_processes['modeld'].stop()
|
||||
del self.vipc_server
|
||||
|
||||
def _send_frames(self, frame_id, cams=None):
|
||||
if cams is None:
|
||||
cams = ('roadCameraState', 'wideRoadCameraState')
|
||||
|
||||
cs = None
|
||||
for cam in cams:
|
||||
msg = messaging.new_message(cam)
|
||||
cs = getattr(msg, cam)
|
||||
cs.frameId = frame_id
|
||||
cs.timestampSof = int((frame_id * DT_MDL) * 1e9)
|
||||
cs.timestampEof = int(cs.timestampSof + (DT_MDL * 1e9))
|
||||
cam_meta = meta_from_camera_state(cam)
|
||||
|
||||
self.pm.send(msg.which(), msg)
|
||||
self.vipc_server.send(cam_meta.stream, IMG_BYTES, cs.frameId,
|
||||
cs.timestampSof, cs.timestampEof)
|
||||
return cs
|
||||
|
||||
def _wait(self):
|
||||
self.sm.update(5000)
|
||||
if self.sm['modelV2'].frameId != self.sm['cameraOdometry'].frameId:
|
||||
self.sm.update(1000)
|
||||
|
||||
def test_modeld(self):
|
||||
for n in range(1, 500):
|
||||
cs = self._send_frames(n)
|
||||
self._wait()
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
assert mdl.frameId == n
|
||||
assert mdl.frameIdExtra == n
|
||||
assert mdl.timestampEof == cs.timestampEof
|
||||
assert mdl.frameAge == 0
|
||||
assert mdl.frameDropPerc == 0
|
||||
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert odo.frameId == n
|
||||
assert odo.timestampEof == cs.timestampEof
|
||||
|
||||
def test_dropped_frames(self):
|
||||
"""
|
||||
modeld should only run on consecutive road frames
|
||||
"""
|
||||
frame_id = -1
|
||||
road_frames = list()
|
||||
for n in range(1, 50):
|
||||
if (random.random() < 0.1) and n > 3:
|
||||
cams = random.choice([(), ('wideRoadCameraState', )])
|
||||
self._send_frames(n, cams)
|
||||
else:
|
||||
self._send_frames(n)
|
||||
road_frames.append(n)
|
||||
self._wait()
|
||||
|
||||
if len(road_frames) < 3 or road_frames[-1] - road_frames[-2] == 1:
|
||||
frame_id = road_frames[-1]
|
||||
|
||||
mdl = self.sm['modelV2']
|
||||
odo = self.sm['cameraOdometry']
|
||||
assert mdl.frameId == frame_id
|
||||
assert mdl.frameIdExtra == frame_id
|
||||
assert odo.frameId == frame_id
|
||||
if n != frame_id:
|
||||
assert not self.sm.updated['modelV2']
|
||||
assert not self.sm.updated['cameraOdometry']
|
||||
61
sunnypilot/modeld_v2/tests/test_recovery_power.py
Normal file
61
sunnypilot/modeld_v2/tests/test_recovery_power.py
Normal file
@@ -0,0 +1,61 @@
|
||||
import numpy as np
|
||||
|
||||
from cereal import log
|
||||
|
||||
from openpilot.sunnypilot.modeld_v2.constants import Plan
|
||||
from openpilot.sunnypilot.modeld_v2.modeld import ModelState
|
||||
import openpilot.sunnypilot.modeld_v2.modeld as modeld
|
||||
|
||||
|
||||
class MockStruct:
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
def test_recovery_power_scaling():
|
||||
state = MockStruct(
|
||||
PLANPLUS_CONTROL=1.0,
|
||||
LONG_SMOOTH_SECONDS=0.3,
|
||||
LAT_SMOOTH_SECONDS=0.1,
|
||||
MIN_LAT_CONTROL_SPEED=0.3,
|
||||
mlsim=True,
|
||||
generation=12,
|
||||
constants=MockStruct(T_IDXS=np.arange(100), DESIRE_LEN=8)
|
||||
)
|
||||
prev_action = log.ModelDataV2.Action()
|
||||
recorded_vel: list = []
|
||||
|
||||
def mock_accel(plan_vel, plan_accel, t_idxs, action_t=0.0):
|
||||
recorded_vel.append(plan_vel.copy())
|
||||
return 0.0, False
|
||||
|
||||
modeld.get_accel_from_plan = mock_accel
|
||||
modeld.get_curvature_from_output = lambda *args: 0.0
|
||||
plan = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
planplus = np.random.rand(1, 100, 15).astype(np.float32)
|
||||
|
||||
model_output: dict = {
|
||||
'plan': plan.copy(),
|
||||
'planplus': planplus.copy()
|
||||
}
|
||||
|
||||
test_cases: list = [
|
||||
# (control, v_ego, expected_factor)
|
||||
(0.55, 20.0, 1.0),
|
||||
(1.0, 25.0, .75),
|
||||
(1.5, 25.1, 0.75),
|
||||
(2.0, 20.0, 1.0),
|
||||
(0.75, 19.0, 1.0),
|
||||
(0.8, 25.1, 0.75),
|
||||
]
|
||||
|
||||
for control, v_ego, factor in test_cases:
|
||||
state.PLANPLUS_CONTROL = control
|
||||
recorded_vel.clear()
|
||||
ModelState.get_action_from_model(state, model_output, prev_action, 0.0, 0.0, v_ego)
|
||||
|
||||
expected_recovery_power = control * factor
|
||||
expected_plan_vel = plan[0, :, Plan.VELOCITY][:, 0] + expected_recovery_power * planplus[0, :, Plan.VELOCITY][:, 0]
|
||||
|
||||
np.testing.assert_allclose(recorded_vel[0], expected_plan_vel, rtol=1e-5, atol=1e-6)
|
||||
@@ -2,25 +2,17 @@ from openpilot.sunnypilot.models.helpers import get_active_bundle
|
||||
from openpilot.sunnypilot.models.runners.model_runner import ModelRunner
|
||||
from openpilot.sunnypilot.models.runners.tinygrad.tinygrad_runner import TinygradRunner, TinygradSplitRunner
|
||||
from openpilot.sunnypilot.models.runners.constants import ModelType
|
||||
from openpilot.system.hardware import TICI
|
||||
|
||||
if not TICI:
|
||||
from openpilot.sunnypilot.models.runners.onnx.onnx_runner import ONNXRunner
|
||||
|
||||
def get_model_runner() -> ModelRunner:
|
||||
"""
|
||||
Factory function to create and return the appropriate ModelRunner instance.
|
||||
|
||||
Selects between ONNXRunner (for non-TICI platforms) and TinygradRunner
|
||||
(for TICI platforms), choosing TinygradSplitRunner if separate vision/policy
|
||||
Selects TinygradRunner, choosing TinygradSplitRunner if separate vision/policy
|
||||
models are detected in the active bundle.
|
||||
|
||||
:return: An instance of a ModelRunner subclass (ONNXRunner, TinygradRunner, or TinygradSplitRunner).
|
||||
"""
|
||||
if not TICI:
|
||||
return ONNXRunner()
|
||||
|
||||
# On TICI platforms, use Tinygrad runners
|
||||
bundle = get_active_bundle()
|
||||
if bundle and bundle.models:
|
||||
model_types = {m.type.raw for m in bundle.models}
|
||||
|
||||
BIN
sunnypilot/selfdrive/assets/logo.png
LFS
Normal file
BIN
sunnypilot/selfdrive/assets/logo.png
LFS
Normal file
Binary file not shown.
@@ -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
|
||||
|
||||
|
||||
@@ -111,7 +114,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
|
||||
# hyundai
|
||||
keys.extend([
|
||||
"HyundaiLongitudinalTuning"
|
||||
"HyundaiLongitudinalTuning",
|
||||
])
|
||||
|
||||
# subaru
|
||||
@@ -125,4 +128,9 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
||||
"TeslaCoopSteering",
|
||||
])
|
||||
|
||||
# toyota
|
||||
keys.extend([
|
||||
"ToyotaEnforceStockLongitudinal",
|
||||
])
|
||||
|
||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||
|
||||
31
sunnypilot/selfdrive/car/sync_car_list_param.py
Executable file
31
sunnypilot/selfdrive/car/sync_car_list_param.py
Executable file
@@ -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()
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user