mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-10-31 05:33:44 +08:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce3eda4fe6 | |||
| 6f7a2c94c1 | |||
| 52600eccbf | |||
| 24f531b83c | |||
| 97135965ef | |||
| 90a541287f | |||
| db27a433fc | |||
| be3bd6bafd | |||
| 663e9c5e42 | |||
| a06366ce92 | |||
| 861fd118a1 | |||
| de56d68fa8 | |||
| b914f168b4 | |||
| 8e740e0ac6 | |||
| 07bb42c2bb | |||
| dd674fbc0d | |||
| 53176484bd | |||
| b2d025f543 | |||
| 35ee46abc3 | |||
| 9d029d683d | |||
| 567cffdb3e | |||
| f78c6c2377 | |||
| 7cf2f4cf2e | |||
| 1aa3a430fb | |||
| ddd7b14ebc | |||
| 1b4902a8ba | |||
| 895bfa8ea5 | |||
| 45051dab54 | |||
| 612c79760a | |||
| af93efd608 | |||
| 922513f312 |
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: LFS Fetch
|
- name: LFS Fetch
|
||||||
run: |
|
run: |
|
||||||
git lfs fetch
|
git lfs fetch || echo "::warning::some objects are missing on commaai/openpilot-lfs; mirroring the rest"
|
||||||
|
|
||||||
- name: Set up Git
|
- name: Set up Git
|
||||||
run: |
|
run: |
|
||||||
@@ -68,6 +68,7 @@ jobs:
|
|||||||
git config lfs.activitytimeout 300
|
git config lfs.activitytimeout 300
|
||||||
git config lfs.transfer.maxretries 5
|
git config lfs.transfer.maxretries 5
|
||||||
git config lfs.concurrenttransfers 4
|
git config lfs.concurrenttransfers 4
|
||||||
|
git config lfs.allowincompletepush true
|
||||||
|
|
||||||
- name: Push LFS
|
- name: Push LFS
|
||||||
id: sync-and-commit
|
id: sync-and-commit
|
||||||
@@ -81,3 +82,80 @@ jobs:
|
|||||||
done
|
done
|
||||||
echo "All push attempts failed"
|
echo "All push attempts failed"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
- name: Checkout sunnypilot
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: 'sunnypilot/sunnypilot'
|
||||||
|
path: sunnypilot
|
||||||
|
fetch-depth: 0
|
||||||
|
filter: blob:limit=1k
|
||||||
|
lfs: false
|
||||||
|
|
||||||
|
- name: Reference objects on sunnypilot-lfs
|
||||||
|
env:
|
||||||
|
HF_TOKEN: ${{ secrets.HF_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
GIT_LFS_SKIP_SMUDGE=1 git clone --quiet https://huggingface.co/sunnypilot/sunnypilot-lfs hf-lfs
|
||||||
|
mkdir -p hf-lfs/all
|
||||||
|
cat > scan_pointers.py <<'EOF'
|
||||||
|
import re, subprocess, sys, threading
|
||||||
|
repo = sys.argv[1]
|
||||||
|
chk = subprocess.run(["git", "-C", repo, "cat-file", "--batch-all-objects",
|
||||||
|
"--batch-check=%(objectname) %(objecttype) %(objectsize)"],
|
||||||
|
capture_output=True, text=True, check=True).stdout
|
||||||
|
small = [f[0] for f in (l.split() for l in chk.splitlines()) if f[1] == "blob" and int(f[2]) < 1024]
|
||||||
|
p = subprocess.Popen(["git", "-C", repo, "cat-file", "--batch=%(objectsize)"],
|
||||||
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
def feed():
|
||||||
|
for sha in small:
|
||||||
|
p.stdin.write(sha.encode() + b"\n")
|
||||||
|
p.stdin.close()
|
||||||
|
threading.Thread(target=feed, daemon=True).start()
|
||||||
|
rx = re.compile(rb"^version https://git-lfs\.github\.com/spec/v1\noid sha256:([0-9a-f]{64})\nsize ([0-9]+)\n")
|
||||||
|
seen = {}
|
||||||
|
for _ in small:
|
||||||
|
n = int(p.stdout.readline()); body = p.stdout.read(n); p.stdout.read(1)
|
||||||
|
m = rx.match(body)
|
||||||
|
if m:
|
||||||
|
seen.setdefault(m.group(1).decode(), m.group(2).decode())
|
||||||
|
for oid, size in sorted(seen.items()):
|
||||||
|
print(oid, size)
|
||||||
|
EOF
|
||||||
|
for repo in . sunnypilot; do
|
||||||
|
python3 scan_pointers.py "$repo"
|
||||||
|
done | sort -u | while read -r oid size; do
|
||||||
|
[ -e "hf-lfs/all/$oid" ] && continue
|
||||||
|
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:%s\nsize %s\n' "$oid" "$size" > "hf-lfs/all/$oid"
|
||||||
|
echo "$oid"
|
||||||
|
done > new_oids.txt
|
||||||
|
echo "new pointer files: $(wc -l < new_oids.txt)"
|
||||||
|
|
||||||
|
dropped=0
|
||||||
|
while mapfile -t -n 100 chunk && ((${#chunk[@]})); do
|
||||||
|
body=$(for oid in "${chunk[@]}"; do
|
||||||
|
size=$(awk '/^size /{print $2}' "hf-lfs/all/$oid")
|
||||||
|
printf '{"oid":"%s","size":%s}\n' "$oid" "$size"
|
||||||
|
done | jq -s '{operation:"download",transfers:["basic"],objects:.}')
|
||||||
|
for attempt in 1 2 3 4 5; do
|
||||||
|
resp=$(curl -sS -u "user:${HF_TOKEN}" -w '\n%{http_code}' \
|
||||||
|
-H 'Accept: application/vnd.git-lfs+json' -H 'Content-Type: application/vnd.git-lfs+json' \
|
||||||
|
--data "$body" "${LFS_URL}/objects/batch") && [ "$(tail -n1 <<<"$resp")" = 200 ] && break
|
||||||
|
echo "batch check attempt $attempt failed ($(tail -n1 <<<"$resp")), retrying in 60s"; sleep 60
|
||||||
|
done
|
||||||
|
[ "$(tail -n1 <<<"$resp")" = 200 ] || { echo "batch check failed"; exit 1; }
|
||||||
|
for oid in $(sed '$d' <<<"$resp" | jq -r '.objects[] | select(.error) | .oid'); do
|
||||||
|
rm -f "hf-lfs/all/$oid"; dropped=$((dropped+1))
|
||||||
|
done
|
||||||
|
done < new_oids.txt
|
||||||
|
echo "dropped (not on HF): $dropped"
|
||||||
|
|
||||||
|
cd hf-lfs
|
||||||
|
git add -A all
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "nothing new to reference"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
git commit -q -m "Reference $(git diff --cached --name-only | wc -l | tr -d ' ') new LFS objects"
|
||||||
|
git push origin HEAD:main
|
||||||
|
|||||||
@@ -3,9 +3,62 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
- 'tinygrad_repo'
|
- 'tinygrad_repo'
|
||||||
|
- 'openpilot/sunnypilot/modeld_v2/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
GIT_CONFIG_COUNT: 1
|
||||||
|
GIT_CONFIG_KEY_0: lfs.fetchexclude
|
||||||
|
GIT_CONFIG_VALUE_0: openpilot/selfdrive/modeld/models/big_driving_tinygrad.pkl
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
plain-artifact:
|
||||||
|
name: Plain artifact round trip and pickle converter
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
env:
|
||||||
|
PYTHONPATH: ${{ github.workspace }}/tinygrad_repo:${{ github.workspace }}
|
||||||
|
TINYGRAD_TESTS: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v7
|
||||||
|
- run: ./tools/op.sh setup
|
||||||
|
- name: Build openpilot
|
||||||
|
run: scons -j$(nproc)
|
||||||
|
- name: Compile, load, cache and fuse a model with the pinned tinygrad
|
||||||
|
env:
|
||||||
|
DEV: CPU
|
||||||
|
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_plain_artifact.py
|
||||||
|
- name: Install LLVM 20 and ROCm comgr for the amd mock, as tinygrad_repo/.github/actions/setup-tinygrad does
|
||||||
|
run: |
|
||||||
|
. /etc/os-release
|
||||||
|
curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc > /dev/null
|
||||||
|
echo "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-20 main" | sudo tee /etc/apt/sources.list.d/llvm.list
|
||||||
|
sudo mkdir -p /etc/apt/keyrings
|
||||||
|
curl -fsSL https://repo.radeon.com/rocm/rocm.gpg.key | gpg --dearmor | sudo tee /etc/apt/keyrings/rocm.gpg > /dev/null
|
||||||
|
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/rocm.gpg] https://repo.radeon.com/rocm/apt/7.1 $VERSION_CODENAME main" | sudo tee /etc/apt/sources.list.d/rocm.list
|
||||||
|
printf 'Package: *\nPin: release o=repo.radeon.com\nPin-Priority: 600\n' | sudo tee /etc/apt/preferences.d/rocm-pin-600
|
||||||
|
sudo apt-get -qq update && sudo apt-get -qq install -y --no-install-recommends libllvm20 clang-20 lld-20 comgr
|
||||||
|
printf '/opt/rocm/lib\n/opt/rocm/lib64\n' | sudo tee /etc/ld.so.conf.d/rocm.conf && sudo ldconfig
|
||||||
|
echo "/usr/lib/llvm-20/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Convert a pickled USB+AMD jit and replay it under tinygrad's amd mock
|
||||||
|
env:
|
||||||
|
DEV: MOCKUSB+AMD
|
||||||
|
CHECK_OOB: 0
|
||||||
|
GMMU: 0
|
||||||
|
run: tools/op.sh test -v openpilot/sunnypilot/modeld_v2/tests/test_convert_upstream_pkl.py
|
||||||
|
- name: Where tinygrad moved, for the helper or failure point named above
|
||||||
|
if: failure() && github.event_name == 'pull_request'
|
||||||
|
run: |
|
||||||
|
git fetch -q --depth=1 origin "${{ github.base_ref }}"
|
||||||
|
old=$(git rev-parse "origin/${{ github.base_ref }}:tinygrad_repo") new=$(git rev-parse HEAD:tinygrad_repo)
|
||||||
|
git -C tinygrad_repo fetch -q origin "$old" || true
|
||||||
|
echo "tinygrad_repo $old -> $new, in what helpers.py and plain_artifact_compiler.py read:"
|
||||||
|
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/uop/ops.py tinygrad/engine/jit.py tinygrad/engine/realize.py tinygrad/device.py \
|
||||||
|
tinygrad/helpers.py tinygrad/dtype.py tinygrad/renderer/__init__.py
|
||||||
|
echo "and in what convert_upstream_pkl.py decodes:"
|
||||||
|
git -C tinygrad_repo diff --stat "$old" "$new" -- tinygrad/runtime/autogen/am/pm4_nv.py tinygrad/runtime/autogen/amd_gpu.py
|
||||||
|
git -C tinygrad_repo diff "$old" "$new" -- tinygrad/runtime/support/hcq2.py tinygrad/runtime/support/usb.py tinygrad/runtime/ops_amd.py
|
||||||
|
|
||||||
generate-matrix:
|
generate-matrix:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ jobs:
|
|||||||
timeout-minutes: 3
|
timeout-minutes: 3
|
||||||
run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh
|
run: TARGET_DIR=$STRIPPED_DIR tools/release/build_stripped.sh
|
||||||
- run: ./tools/op.sh setup
|
- run: ./tools/op.sh setup
|
||||||
|
- name: Restore scons cache
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: /tmp/scons_cache
|
||||||
|
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||||
|
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
|
||||||
- name: Build openpilot and run checks
|
- name: Build openpilot and run checks
|
||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
working-directory: ${{ env.STRIPPED_DIR }}
|
working-directory: ${{ env.STRIPPED_DIR }}
|
||||||
@@ -89,6 +95,12 @@ jobs:
|
|||||||
FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':')
|
FILTERED=$(echo "$PATH" | tr ':' '\n' | grep -v '/opt/homebrew' | tr '\n' ':')
|
||||||
echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV
|
echo "PATH=${FILTERED}/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" >> $GITHUB_ENV
|
||||||
- run: ./tools/op.sh setup
|
- run: ./tools/op.sh setup
|
||||||
|
- name: Restore scons cache
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: /tmp/scons_cache
|
||||||
|
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||||
|
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
|
||||||
- name: Building openpilot
|
- name: Building openpilot
|
||||||
run: scons
|
run: scons
|
||||||
|
|
||||||
@@ -121,6 +133,12 @@ jobs:
|
|||||||
- name: Install raylib headless dependencies
|
- name: Install raylib headless dependencies
|
||||||
if: ${{ !contains(runner.name, 'nsc') }}
|
if: ${{ !contains(runner.name, 'nsc') }}
|
||||||
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
|
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
|
||||||
|
- name: Restore scons cache
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: /tmp/scons_cache
|
||||||
|
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||||
|
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
|
||||||
- name: Build openpilot
|
- name: Build openpilot
|
||||||
run: scons
|
run: scons
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
@@ -140,6 +158,12 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
- run: ./tools/op.sh setup
|
- run: ./tools/op.sh setup
|
||||||
|
- name: Restore scons cache
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: /tmp/scons_cache
|
||||||
|
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||||
|
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
|
||||||
- name: Build openpilot
|
- name: Build openpilot
|
||||||
run: scons
|
run: scons
|
||||||
- name: Run replay
|
- name: Run replay
|
||||||
@@ -228,6 +252,12 @@ jobs:
|
|||||||
- name: Install raylib headless dependencies
|
- name: Install raylib headless dependencies
|
||||||
if: ${{ !contains(runner.name, 'nsc') }}
|
if: ${{ !contains(runner.name, 'nsc') }}
|
||||||
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
|
run: sudo apt-get install -y --no-install-recommends libgles2 libegl1
|
||||||
|
- name: Restore scons cache
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: /tmp/scons_cache
|
||||||
|
key: scons-${{ runner.os }}-${{ github.job }}-${{ github.sha }}
|
||||||
|
restore-keys: scons-${{ runner.os }}-${{ github.job }}-
|
||||||
- name: Build openpilot
|
- name: Build openpilot
|
||||||
run: scons
|
run: scons
|
||||||
- name: Create UI Report
|
- name: Create UI Report
|
||||||
|
|||||||
+115
-108
@@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
|
||||||
|
|
||||||
# 345 Supported Cars
|
# 351 Supported Cars
|
||||||
|
|
||||||
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br> |Video|Setup Video|
|
||||||
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||||
|
|Acura|ADX 2025-26|All|openpilot available[<sup>1,5</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=Acura ADX 2025-26">Buy Here</a></sub></details>|||
|
||||||
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 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 ILX 2016-18">Buy Here</a></sub></details>|||
|
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 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 ILX 2016-18">Buy Here</a></sub></details>|||
|
||||||
|Acura|ILX 2019|All|openpilot|26 mph|25 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 ILX 2019">Buy Here</a></sub></details>|||
|
|Acura|ILX 2019|All|openpilot|26 mph|25 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 ILX 2019">Buy Here</a></sub></details>|||
|
||||||
|Acura|MDX 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 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 MDX 2022-24">Buy Here</a></sub></details>|||
|
|Acura|MDX 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 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 MDX 2022-24">Buy Here</a></sub></details>|||
|
||||||
@@ -16,12 +17,12 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Acura|RDX 2019-21|All|openpilot available[<sup>1,5</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|RDX 2019-21|All|openpilot available[<sup>1,5</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-22|All|openpilot available[<sup>1,5</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-22">Buy Here</a></sub></details>|||
|
|Acura|TLX 2021-22|All|openpilot available[<sup>1,5</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-22">Buy Here</a></sub></details>|||
|
||||||
|Acura|TLX 2025|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=Acura TLX 2025">Buy Here</a></sub></details>|||
|
|Acura|TLX 2025|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=Acura TLX 2025">Buy Here</a></sub></details>|||
|
||||||
|Audi[<sup>12</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|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" /></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 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|Equinox 2019-22|Adaptive Cruise Control (ACC)|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 Equinox 2019-22">Buy Here</a></sub></details>|||
|
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|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 Equinox 2019-22">Buy Here</a></sub></details>|||
|
||||||
@@ -33,8 +34,8 @@ 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 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>|||
|
|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" /></a>||
|
|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" /></a>||
|
||||||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 Born 2021-23">Buy Here</a></sub></details>|||
|
|CUPRA|Born 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 Born 2021-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>|||
|
|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|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>|||
|
|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>|||
|
||||||
@@ -86,7 +87,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Honda|Civic 2022-26|All|openpilot available[<sup>1,5</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-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
|Honda|Civic 2022-26|All|openpilot available[<sup>1,5</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-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1,5</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 2017-18|Honda Sensing|openpilot available[<sup>1,5</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,5</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>|||
|
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1,5</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>|||
|
||||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1,5</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 Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
|Honda|Civic Hatchback 2022-26|All|openpilot available[<sup>1,5</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 Hatchback 2022-26">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1,5</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 Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1,5</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 Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1,5</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 Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1,5</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 Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||||
|Honda|Civic Hybrid 2025-26|All|openpilot available[<sup>1,5</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 Hybrid 2025-26">Buy Here</a></sub></details>|||
|
|Honda|Civic Hybrid 2025-26|All|openpilot available[<sup>1,5</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 Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||||
@@ -94,7 +95,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|15 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 CR-V 2017-22">Buy Here</a></sub></details>|||
|
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|15 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 CR-V 2017-22">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V 2023-26|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 CR-V 2023-26">Buy Here</a></sub></details>|||
|
|Honda|CR-V 2023-26|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 CR-V 2023-26">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1,5</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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1,5</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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|
||||||
|Honda|CR-V Hybrid 2023-26|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 CR-V Hybrid 2023-26">Buy Here</a></sub></details>|||
|
|Honda|CR-V Hybrid 2023-27|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 CR-V Hybrid 2023-27">Buy Here</a></sub></details>|||
|
||||||
|Honda|e 2020|All|openpilot available[<sup>1,5</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=Honda e 2020">Buy Here</a></sub></details>|||
|
|Honda|e 2020|All|openpilot available[<sup>1,5</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=Honda e 2020">Buy Here</a></sub></details>|||
|
||||||
|Honda|Fit 2018-20|Honda Sensing|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=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|
|Honda|Fit 2018-20|Honda Sensing|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=Honda Fit 2018-20">Buy Here</a></sub></details>|||
|
||||||
|Honda|Freed 2020|Honda Sensing|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=Honda Freed 2020">Buy Here</a></sub></details>|||
|
|Honda|Freed 2020|Honda Sensing|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=Honda Freed 2020">Buy Here</a></sub></details>|||
|
||||||
@@ -111,7 +112,9 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Honda|Passport 2026|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 Passport 2026">Buy Here</a></sub></details>|||
|
|Honda|Passport 2026|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 Passport 2026">Buy Here</a></sub></details>|||
|
||||||
|Honda|Pilot 2016-22|Honda Sensing|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=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|
|Honda|Pilot 2016-22|Honda Sensing|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=Honda Pilot 2016-22">Buy Here</a></sub></details>|||
|
||||||
|Honda|Pilot 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 Pilot 2023-25">Buy Here</a></sub></details>|||
|
|Honda|Pilot 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 Pilot 2023-25">Buy Here</a></sub></details>|||
|
||||||
|Honda|Ridgeline 2017-25|Honda Sensing|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=Honda Ridgeline 2017-25">Buy Here</a></sub></details>|||
|
|Honda|Pilot 2026|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 Pilot 2026">Buy Here</a></sub></details>|||
|
||||||
|
|Honda|Prelude 2026|All|openpilot available[<sup>1,5</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 Prelude 2026">Buy Here</a></sub></details>|||
|
||||||
|
|Honda|Ridgeline 2017-26|Honda Sensing|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=Honda Ridgeline 2017-26">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Azera 2022|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=Hyundai Azera 2022">Buy Here</a></sub></details>|||
|
|Hyundai|Azera 2022|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=Hyundai Azera 2022">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Azera Hybrid 2019|All|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=Hyundai Azera Hybrid 2019">Buy Here</a></sub></details>|||
|
|Hyundai|Azera Hybrid 2019|All|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=Hyundai Azera Hybrid 2019">Buy Here</a></sub></details>|||
|
||||||
|Hyundai|Azera Hybrid 2020|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=Hyundai Azera Hybrid 2020">Buy Here</a></sub></details>|||
|
|Hyundai|Azera Hybrid 2020|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=Hyundai Azera Hybrid 2020">Buy Here</a></sub></details>|||
|
||||||
@@ -163,6 +166,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Carnival 2022-24">Buy Here</a></sub></details>|||
|
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Carnival 2022-24">Buy Here</a></sub></details>|||
|
||||||
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|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 Carnival (China only) 2023">Buy Here</a></sub></details>|||
|
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|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 Carnival (China only) 2023">Buy Here</a></sub></details>|||
|
||||||
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Ceed 2019-21">Buy Here</a></sub></details>|||
|
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Ceed 2019-21">Buy Here</a></sub></details>|||
|
||||||
|
|Kia|Ceed Plug-in Hybrid 2021|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Ceed Plug-in Hybrid 2021">Buy Here</a></sub></details>|||
|
||||||
|Kia|EV6 (Southeast Asia only) 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P 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 EV6 (Southeast Asia only) 2022-24">Buy Here</a></sub></details>|||
|
|Kia|EV6 (Southeast Asia only) 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P 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 EV6 (Southeast Asia only) 2022-24">Buy Here</a></sub></details>|||
|
||||||
|Kia|EV6 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P 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 EV6 (with HDA II) 2022-24">Buy Here</a></sub></details>|||
|
|Kia|EV6 (with HDA II) 2022-24|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P 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 EV6 (with HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||||
|Kia|EV6 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L 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 EV6 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|
|Kia|EV6 (without HDA II) 2022-24|Highway Driving Assist|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai L 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 EV6 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|
||||||
@@ -188,7 +192,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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 Niro Plug-in Hybrid 2022">Buy Here</a></sub></details>|||
|
|Kia|Niro Plug-in Hybrid 2022|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai F 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 Niro Plug-in Hybrid 2022">Buy Here</a></sub></details>|||
|
||||||
|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Optima 2017">Buy Here</a></sub></details>|||
|
|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Optima 2017">Buy Here</a></sub></details>|||
|
||||||
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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 Optima 2019-20">Buy Here</a></sub></details>|||
|
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai G 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 Optima 2019-20">Buy Here</a></sub></details>|||
|
||||||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|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 Optima Hybrid 2019">Buy Here</a></sub></details>|||
|
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|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 Optima Hybrid 2019">Buy Here</a></sub></details>|||
|
||||||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Seltos 2021">Buy Here</a></sub></details>|||
|
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Seltos 2021">Buy Here</a></sub></details>|||
|
||||||
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Sorento 2018">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||||
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai E 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 Sorento 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Fkh3s6WHJz8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||||
@@ -197,6 +201,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Sorento Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai 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=Kia Sorento Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||||
|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N 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 Sportage 2023-24">Buy Here</a></sub></details>|||
|
|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N 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 Sportage 2023-24">Buy Here</a></sub></details>|||
|
||||||
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N 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 Sportage Hybrid 2023">Buy Here</a></sub></details>|||
|
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N 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 Sportage Hybrid 2023">Buy Here</a></sub></details>|||
|
||||||
|
|Kia|Sportage Plug-in Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai N 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 Sportage Plug-in Hybrid 2023">Buy Here</a></sub></details>|||
|
||||||
|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" /></a>||
|
|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" /></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|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>|||
|
|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>|||
|
||||||
@@ -225,51 +230,52 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|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>|||
|
|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 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>|||
|
|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[<sup>12</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|MAN[<sup>13</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|MAN[<sup>12</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|MAN[<sup>13</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></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[<sup>6</sup>](#footnotes)|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" /></a>||
|
|Mazda[<sup>6</sup>](#footnotes)|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" /></a>||
|
||||||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|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 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-24">Buy Here</a></sub></details>|||
|
|Nissan[<sup>7</sup>](#footnotes)|Altima 2019-24|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 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-24">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 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" /></a>||
|
|Nissan[<sup>7</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 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" /></a>||
|
||||||
|Nissan[<sup>6</sup>](#footnotes)|Leaf IC 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 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 IC 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
|Nissan[<sup>7</sup>](#footnotes)|Leaf IC 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 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 IC 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></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 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>7</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 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 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>7</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 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|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|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>|||
|
|Ram|3500 2019-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 3500 2019-24">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 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/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
|Rivian|R1S 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<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/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||||
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<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 2025">Buy Here</a></sub></details>|||
|
|Rivian|R1S 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<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 2025">Buy Here</a></sub></details>|||
|
||||||
|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 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/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
|Rivian|R1T 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<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/dflSSGQwYNc" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||||
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<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 2025">Buy Here</a></sub></details>|||
|
|Rivian|R1T 2025|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian B connector<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 2025">Buy Here</a></sub></details>|||
|
||||||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>|||
|
|SEAT[<sup>13</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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)|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 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|Ascent 2019-21|All[<sup>8</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 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)|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 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" /></a>||
|
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>8</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 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" /></a>||
|
||||||
|Subaru|Crosstrek 2020-23|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 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|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>8</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 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 2017-18|EyeSight Driver Assistance[<sup>8</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)|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 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|Forester 2019-21|All[<sup>8</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 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)|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 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 2017-19|EyeSight Driver Assistance[<sup>8</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 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)|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 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|Impreza 2020-22|EyeSight Driver Assistance[<sup>8</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 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 2015-18|EyeSight Driver Assistance[<sup>8</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|Legacy 2020-22|All[<sup>8</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 2015-17|EyeSight Driver Assistance[<sup>8</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 2018-19|EyeSight Driver Assistance[<sup>8</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|Outback 2020-22|All[<sup>8</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)|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 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" /></a>||
|
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>8</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 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" /></a>||
|
||||||
|Subaru|XV 2020-21|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 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>|||
|
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>8</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 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>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Škoda|Fabia 2022-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Škoda|Kamiq 2021-23[<sup>14,16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Karoq 2019-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Kodiaq 2017-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Kodiaq 2017-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Octavia 2015-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Octavia 2015-19[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Octavia RS 2016[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Octavia RS 2016[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Octavia Scout 2017-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Octavia Scout 2017-19[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Škoda|Scala 2020-23[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>|||
|
|Škoda[<sup>13</sup>](#footnotes)|Superb 2015-22[<sup>16</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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 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>11</sup>](#footnotes)|Model 3 (with HW2.5) 2019[<sup>10</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 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 HW2.5) 2019">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 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>11</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>10</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 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 Y (with HW3) 2020-24[<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 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-24">Buy Here</a></sub></details>|||
|
|Tesla[<sup>11</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>10</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 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 HW4) 2023-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 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) 2023-25">Buy Here</a></sub></details>|||
|
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW3) 2020-24[<sup>10</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 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-24">Buy Here</a></sub></details>|||
|
||||||
|
|Tesla[<sup>11</sup>](#footnotes)|Model Y (with HW4) 2023-25[<sup>10</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 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) 2023-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 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|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|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 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>|||
|
||||||
@@ -282,8 +288,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|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 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 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|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" /></a>||
|
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>12</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" /></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 2021-24|All|openpilot|0 mph[<sup>12</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" /></a>||
|
|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" /></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|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|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 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>|||
|
||||||
@@ -315,44 +321,44 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
|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" /></a>||
|
|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" /></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" /></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" /></a>||
|
||||||
|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" /></a>||
|
|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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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" /></a>||
|
||||||
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 ID.4 2021-23">Buy Here</a></sub></details>|||
|
|Volkswagen|ID.4 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 ID.4 2021-23">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 ID.4 2024-25">Buy Here</a></sub></details>|||
|
|Volkswagen|ID.4 2024-25|Adaptive Cruise Control (ACC) & Lane Assist|openpilot[<sup>17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW MEB 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 ID.4 2024-25">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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 2019-23">Buy Here</a></sub></details>|||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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 2019-23">Buy Here</a></sub></details>|||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>17</sup>](#footnotes)|||
|
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>18</sup>](#footnotes)|||
|
||||||
|Volkswagen[<sup>12</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>13</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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[<sup>12</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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>|||
|
|Volkswagen[<sup>13</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,17</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<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
|
### Footnotes
|
||||||
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`. <br />
|
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`. <br />
|
||||||
@@ -360,18 +366,19 @@ A supported vehicle is one that just works when you install a comma device. All
|
|||||||
<sup>3</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <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>4</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
|
||||||
<sup>5</sup>Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW. <br />
|
<sup>5</sup>Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW. <br />
|
||||||
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
<sup>6</sup>Mazda vehicles <b>require</b> comma power installation. <br />
|
||||||
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
<sup>7</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
|
||||||
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
<sup>8</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
|
||||||
<sup>9</sup>Model years 2023 and 2024 can have either hardware type, depending on build date and factory. 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>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
|
||||||
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
<sup>10</sup>Model year 2019 can have HW2.5 or HW3, and model years 2023 and 2024 can have HW3 or HW4, depending on build date and factory. 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>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>11</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
|
||||||
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
<sup>12</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>13</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
<sup>13</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
|
||||||
<sup>14</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
<sup>14</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
|
||||||
<sup>15</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>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
|
||||||
<sup>16</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>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
|
||||||
<sup>17</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>17</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>18</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
|
## 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/).
|
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/).
|
||||||
|
|||||||
+1
-1
Submodule opendbc_repo updated: 5be7507143...aa9bff6f54
@@ -138,6 +138,9 @@ struct ModelManagerSP @0xaedffd8f31e7b55d {
|
|||||||
status @0 :DownloadStatus;
|
status @0 :DownloadStatus;
|
||||||
progress @1 :Float32;
|
progress @1 :Float32;
|
||||||
eta @2 :UInt32;
|
eta @2 :UInt32;
|
||||||
|
speed @3 :Float32;
|
||||||
|
canceling @4 :Bool;
|
||||||
|
resuming @5 :Bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Chunk {
|
struct Chunk {
|
||||||
@@ -465,6 +468,13 @@ struct ModelDataV2SP @0xa1680744031fdb2d {
|
|||||||
laneTurnDirection @0 :TurnDirection;
|
laneTurnDirection @0 :TurnDirection;
|
||||||
leftLaneChangeEdgeBlock @1 :Bool;
|
leftLaneChangeEdgeBlock @1 :Bool;
|
||||||
rightLaneChangeEdgeBlock @2 :Bool;
|
rightLaneChangeEdgeBlock @2 :Bool;
|
||||||
|
blendfulCurvature @3 :Float32;
|
||||||
|
blendfulTrust @4 :Float32;
|
||||||
|
modelCurvature @5 :Float32;
|
||||||
|
blendfulGoingAround @6 :Bool;
|
||||||
|
blendfulLeftProb @7 :Float32;
|
||||||
|
blendfulRightProb @8 :Float32;
|
||||||
|
blendfulLaneWidth @9 :Float32;
|
||||||
|
|
||||||
enum TurnDirection {
|
enum TurnDirection {
|
||||||
none @0;
|
none @0;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ _services: dict[str, tuple] = {
|
|||||||
"carControlSP": (True, 100., 10),
|
"carControlSP": (True, 100., 10),
|
||||||
"carStateSP": (True, 100., 10),
|
"carStateSP": (True, 100., 10),
|
||||||
"liveMapDataSP": (True, 1., 1),
|
"liveMapDataSP": (True, 1., 1),
|
||||||
"modelDataV2SP": (True, 20., None, QueueSize.BIG),
|
"modelDataV2SP": (True, 20., 10, QueueSize.BIG),
|
||||||
"liveLocationKalman": (True, 20.),
|
"liveLocationKalman": (True, 20.),
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
{"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}},
|
||||||
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
{"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}},
|
||||||
|
{"FollowDistanceAccelBoost", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"GreenLightAlert", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
{"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}},
|
||||||
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
{"HasAcceptedTermsSP", {PERSISTENT, STRING, "0"}},
|
||||||
@@ -246,6 +247,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
{"TeslaMadsScreenButton", {PERSISTENT | BACKUP, INT, "0"}},
|
||||||
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ToyotaEnforceStockLongitudinal", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"ToyotaStopAndGoHack", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
{"ToyotaTSS2Smooth", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
|
|
||||||
@@ -256,6 +258,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
|||||||
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
{"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}},
|
||||||
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
{"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
{"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}},
|
||||||
|
{"Blendful", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||||
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
{"PlanplusControl", {PERSISTENT | BACKUP, FLOAT, "1.0"}},
|
||||||
|
|
||||||
// mapd
|
// mapd
|
||||||
|
|||||||
@@ -64,16 +64,19 @@ class DesireHelper:
|
|||||||
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or
|
||||||
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
(carstate.steeringTorque < 0 and self.lane_change_direction == LaneChangeDirection.right))
|
||||||
|
|
||||||
blindspot_detected = (((carstate.leftBlindspot or left_edge_detected) and self.lane_change_direction == LaneChangeDirection.left) or
|
blindspot_detected = ((carstate.leftBlindspot and self.lane_change_direction == LaneChangeDirection.left) or
|
||||||
((carstate.rightBlindspot or right_edge_detected) and self.lane_change_direction == LaneChangeDirection.right))
|
(carstate.rightBlindspot and self.lane_change_direction == LaneChangeDirection.right))
|
||||||
|
|
||||||
self.alc.update_lane_change(blindspot_detected, carstate.brakePressed)
|
edge_detected = ((left_edge_detected and self.lane_change_direction == LaneChangeDirection.left) or
|
||||||
|
(right_edge_detected and self.lane_change_direction == LaneChangeDirection.right))
|
||||||
|
|
||||||
|
self.alc.update_lane_change(blindspot_detected, edge_detected, carstate.brakePressed, torque_applied)
|
||||||
|
|
||||||
if not one_blinker or below_lane_change_speed:
|
if not one_blinker or below_lane_change_speed:
|
||||||
self.lane_change_state = LaneChangeState.off
|
self.lane_change_state = LaneChangeState.off
|
||||||
self.lane_change_direction = LaneChangeDirection.none
|
self.lane_change_direction = LaneChangeDirection.none
|
||||||
self.lane_change_timer = 0.0
|
self.lane_change_timer = 0.0
|
||||||
elif (torque_applied or self.alc.auto_lane_change_allowed) and not blindspot_detected:
|
elif (torque_applied or self.alc.auto_lane_change_allowed) and not self.alc.lane_change_blocked:
|
||||||
self.lane_change_state = LaneChangeState.laneChangeStarting
|
self.lane_change_state = LaneChangeState.laneChangeStarting
|
||||||
self.lane_change_timer = 0.0
|
self.lane_change_timer = 0.0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from openpilot.cereal import log
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
from openpilot.selfdrive.controls.lib.accel_boost import (
|
||||||
|
AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE, ACCEL_BOOST_RATE
|
||||||
|
)
|
||||||
|
|
||||||
|
E2E_ACCEL = 0.5
|
||||||
|
PLANNER_ACCEL = 2.0
|
||||||
|
RATE_PER_STEP = ACCEL_BOOST_RATE * DT_MDL
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSubMaster:
|
||||||
|
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
|
||||||
|
personality=log.LongitudinalPersonality.standard):
|
||||||
|
self._messages = {
|
||||||
|
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
|
||||||
|
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._messages[key]
|
||||||
|
|
||||||
|
|
||||||
|
def run_boost(boost, sm, steps=1):
|
||||||
|
out = E2E_ACCEL
|
||||||
|
for _ in range(steps):
|
||||||
|
out = boost.update(sm, E2E_ACCEL, PLANNER_ACCEL, PLANNER_ACCEL)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccelBoost(OpenpilotTestCase):
|
||||||
|
def _boost_to_max(self):
|
||||||
|
boost = AccelBoost()
|
||||||
|
sm = FakeSubMaster()
|
||||||
|
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
|
||||||
|
# Eligibility is latched on a frame without gas, then applied on the next override.
|
||||||
|
run_boost(boost, sm)
|
||||||
|
for _ in range(10):
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
run_boost(boost, sm, steps)
|
||||||
|
sm['carState'].gasPressed = False
|
||||||
|
run_boost(boost, sm)
|
||||||
|
return boost
|
||||||
|
|
||||||
|
def test_no_boost_without_gas(self):
|
||||||
|
boost = AccelBoost()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
out = run_boost(boost, sm)
|
||||||
|
self.assertEqual(boost.total_boost, 0.0)
|
||||||
|
self.assertAlmostEqual(out, E2E_ACCEL)
|
||||||
|
|
||||||
|
def test_boost_requires_e2e_to_be_limiting(self):
|
||||||
|
boost = AccelBoost()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
# Eligibility is latched before the override, so keep the e2e plan at the planner limit here.
|
||||||
|
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
for _ in range(10):
|
||||||
|
boost.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||||
|
self.assertEqual(boost.total_boost, 0.0)
|
||||||
|
|
||||||
|
def test_per_override_cap(self):
|
||||||
|
boost = AccelBoost()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
run_boost(boost, sm)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
run_boost(boost, sm, 1000)
|
||||||
|
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_PER_OVERRIDE)
|
||||||
|
|
||||||
|
def test_max_boost(self):
|
||||||
|
boost = self._boost_to_max()
|
||||||
|
self.assertAlmostEqual(boost.total_boost, ACCEL_BOOST_MAX)
|
||||||
|
|
||||||
|
def test_boost_is_applied_to_e2e_accel(self):
|
||||||
|
boost = self._boost_to_max()
|
||||||
|
sm = FakeSubMaster(gas_pressed=True)
|
||||||
|
out = run_boost(boost, sm)
|
||||||
|
self.assertAlmostEqual(out, E2E_ACCEL + boost.total_boost)
|
||||||
|
|
||||||
|
def test_boost_does_not_depend_on_following_distance(self):
|
||||||
|
totals = []
|
||||||
|
for personality in (log.LongitudinalPersonality.relaxed,
|
||||||
|
log.LongitudinalPersonality.standard,
|
||||||
|
log.LongitudinalPersonality.aggressive):
|
||||||
|
boost = AccelBoost()
|
||||||
|
sm = FakeSubMaster(personality=personality)
|
||||||
|
steps = round(ACCEL_BOOST_PER_OVERRIDE / RATE_PER_STEP)
|
||||||
|
run_boost(boost, sm)
|
||||||
|
for _ in range(10):
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
run_boost(boost, sm, steps)
|
||||||
|
sm['carState'].gasPressed = False
|
||||||
|
run_boost(boost, sm)
|
||||||
|
totals.append(boost.total_boost)
|
||||||
|
|
||||||
|
self.assertEqual(len(totals), 3)
|
||||||
|
for total in totals:
|
||||||
|
self.assertAlmostEqual(total, ACCEL_BOOST_MAX)
|
||||||
@@ -21,6 +21,9 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
"Use the steering wheel distance button on supported cars.")
|
"Use the steering wheel distance button on supported cars.")
|
||||||
self._initial_cruise_speed = InitialCruiseSpeedButton()
|
self._initial_cruise_speed = InitialCruiseSpeedButton()
|
||||||
self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise)
|
self._initial_cruise_speed.set_visible(lambda: ui_state.CP is not None and ui_state.has_longitudinal_control and not ui_state.CP.pcmCruise)
|
||||||
|
self._accel_boost_toggle = BigParamControl("extra acceleration boost", "FollowDistanceAccelBoost",
|
||||||
|
description="Adds extra acceleration on top of the stock gas-override boost.\n" +
|
||||||
|
"The extra boost follows Following Distance: strongest when Aggressive, weakest when Relaxed.")
|
||||||
is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle)
|
is_metric_toggle = BigToggle("use metric units", initial_state=ui_state.params.get_bool("IsMetric"), toggle_callback=self._on_metric_toggle)
|
||||||
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
|
ldw_toggle = BigParamControl("lane departure warnings", "IsLdwEnabled",
|
||||||
description="Warn when you drift across a detected lane line.\n" +
|
description="Warn when you drift across a detected lane line.\n" +
|
||||||
@@ -40,6 +43,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
|
|
||||||
self._scroller.add_widgets([
|
self._scroller.add_widgets([
|
||||||
self._personality_toggle,
|
self._personality_toggle,
|
||||||
|
self._accel_boost_toggle,
|
||||||
self._initial_cruise_speed,
|
self._initial_cruise_speed,
|
||||||
is_metric_toggle,
|
is_metric_toggle,
|
||||||
ldw_toggle,
|
ldw_toggle,
|
||||||
@@ -51,6 +55,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
|
|
||||||
# Toggle lists
|
# Toggle lists
|
||||||
self._refresh_toggles = (
|
self._refresh_toggles = (
|
||||||
|
("FollowDistanceAccelBoost", self._accel_boost_toggle),
|
||||||
("IsMetric", is_metric_toggle),
|
("IsMetric", is_metric_toggle),
|
||||||
("IsLdwEnabled", ldw_toggle),
|
("IsLdwEnabled", ldw_toggle),
|
||||||
("AlwaysOnDM", always_on_dm_toggle),
|
("AlwaysOnDM", always_on_dm_toggle),
|
||||||
@@ -92,6 +97,7 @@ class TogglesLayoutMici(NavScroller):
|
|||||||
|
|
||||||
if ui_state.CP is not None:
|
if ui_state.CP is not None:
|
||||||
self._personality_toggle.set_visible(ui_state.has_longitudinal_control)
|
self._personality_toggle.set_visible(ui_state.has_longitudinal_control)
|
||||||
|
self._accel_boost_toggle.set_visible(ui_state.has_longitudinal_control)
|
||||||
|
|
||||||
# Refresh toggles from params to mirror external changes
|
# Refresh toggles from params to mirror external changes
|
||||||
for key, item in self._refresh_toggles:
|
for key, item in self._refresh_toggles:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from openpilot.common.transformations.orientation import rot_from_euler
|
|||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
if gui_app.sunnypilot_ui():
|
if gui_app.sunnypilot_ui():
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.accel_boost_indicator import AccelBoostIndicator
|
||||||
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.hud_renderer import HudRendererSP as HudRenderer
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import OnroadTimerStatus
|
||||||
|
|
||||||
@@ -181,6 +182,7 @@ class AugmentedRoadView(CameraView):
|
|||||||
self._alert_renderer = AlertRenderer()
|
self._alert_renderer = AlertRenderer()
|
||||||
self._driver_state_renderer = DriverStateRenderer()
|
self._driver_state_renderer = DriverStateRenderer()
|
||||||
self._confidence_ball = ConfidenceBall()
|
self._confidence_ball = ConfidenceBall()
|
||||||
|
self._accel_boost_indicator = AccelBoostIndicator() if gui_app.sunnypilot_ui() else None
|
||||||
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
self._offroad_label = UnifiedLabel("start the car to\nuse sunnypilot", 54, FontWeight.DISPLAY,
|
||||||
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
text_color=rl.Color(255, 255, 255, int(255 * 0.9)),
|
||||||
alignment=TextAlignment.CENTER,
|
alignment=TextAlignment.CENTER,
|
||||||
@@ -275,7 +277,13 @@ class AugmentedRoadView(CameraView):
|
|||||||
if show_navigation:
|
if show_navigation:
|
||||||
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
|
self._navigation.render(rl.Rectangle(self._content_rect.x + self._content_rect.width, self.rect.y, PANEL_WIDTH, self.rect.height))
|
||||||
else:
|
else:
|
||||||
self._confidence_ball.render(self.rect)
|
# The boost bar owns the side panel while the extra boost is on; the ball only shows with the feature off
|
||||||
|
if gui_app.sunnypilot_ui():
|
||||||
|
self._accel_boost_indicator.render(self.rect)
|
||||||
|
if not self._accel_boost_indicator.panel_in_use:
|
||||||
|
self._confidence_ball.render(self.rect)
|
||||||
|
else:
|
||||||
|
self._confidence_ball.render(self.rect)
|
||||||
|
|
||||||
self._bookmark_icon.render(self.rect)
|
self._bookmark_icon.render(self.rect)
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ class ModelRenderer(Widget, ModelRendererSP):
|
|||||||
if ui_state.status != UIStatus.DISENGAGED:
|
if ui_state.status != UIStatus.DISENGAGED:
|
||||||
self._draw_lane_lines()
|
self._draw_lane_lines()
|
||||||
self._draw_path()
|
self._draw_path()
|
||||||
|
self.blendful.draw(self._rect)
|
||||||
|
|
||||||
if render_lead_indicator:
|
if render_lead_indicator:
|
||||||
self._update_leads(sm)
|
self._update_leads(sm)
|
||||||
@@ -219,6 +220,7 @@ class ModelRenderer(Widget, ModelRendererSP):
|
|||||||
# Update road edges using raw points
|
# Update road edges using raw points
|
||||||
for road_edge in self._road_edges:
|
for road_edge in self._road_edges:
|
||||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx)
|
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, line_width_factor, 0.0, max_idx)
|
||||||
|
self.blendful.update(self, max_idx)
|
||||||
|
|
||||||
# Update path using raw points
|
# Update path using raw points
|
||||||
if lead and lead.present:
|
if lead and lead.present:
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ class TorqueBar(Widget):
|
|||||||
# draw curved line polygon torque bar
|
# draw curved line polygon torque bar
|
||||||
torque_line_radius = 1200 * self._scale
|
torque_line_radius = 1200 * self._scale
|
||||||
top_angle = -90
|
top_angle = -90
|
||||||
torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN * rect.width / 476
|
torque_bg_angle_span = self._torque_line_alpha_filter.x * TORQUE_ANGLE_SPAN * min(1.0, rect.width / (476 * self._scale))
|
||||||
torque_start_angle = top_angle - torque_bg_angle_span / 2
|
torque_start_angle = top_angle - torque_bg_angle_span / 2
|
||||||
torque_end_angle = top_angle + torque_bg_angle_span / 2
|
torque_end_angle = top_angle + torque_bg_angle_span / 2
|
||||||
# centerline radius & center (you already have these values)
|
# centerline radius & center (you already have these values)
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
|||||||
# Draw elements
|
# Draw elements
|
||||||
self._draw_lane_lines()
|
self._draw_lane_lines()
|
||||||
self._draw_path()
|
self._draw_path()
|
||||||
|
self.blendful.draw(self._rect)
|
||||||
|
|
||||||
if render_lead_indicator and radar_state:
|
if render_lead_indicator and radar_state:
|
||||||
self._draw_lead_indicator()
|
self._draw_lead_indicator()
|
||||||
@@ -172,6 +173,7 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
|
|||||||
# Update road edges using raw points
|
# Update road edges using raw points
|
||||||
for road_edge in self._road_edges:
|
for road_edge in self._road_edges:
|
||||||
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
road_edge.projected_points = self._map_line_to_polygon(road_edge.raw_points, 0.025, 0.0, max_idx, max_distance)
|
||||||
|
self.blendful.update(self, max_idx, max_distance)
|
||||||
|
|
||||||
# Update path using raw points
|
# Update path using raw points
|
||||||
if lead and lead.present:
|
if lead and lead.present:
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ ACC_NOLONG_DESCRIPTION = tr_noop("This feature can only be used with sunnypilot
|
|||||||
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
|
ACC_PCMCRUISE_DISABLED_DESCRIPTION = tr_noop("This feature is not supported on this platform due to vehicle limitations.")
|
||||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||||
|
|
||||||
|
ACCEL_BOOST_EXT_DESCRIPTION = tr_noop("Adds extra acceleration on top of the stock gas-override boost when you hold the gas pedal " +
|
||||||
|
"while sunnypilot longitudinal control is active. The extra boost follows the Following Distance setting: " +
|
||||||
|
"strongest when Aggressive, weakest when Relaxed. The stock boost remains active when this is off.")
|
||||||
|
ACCEL_BOOST_EXT_NOLONG_DESCRIPTION = tr_noop("This feature requires sunnypilot longitudinal control to be available.")
|
||||||
|
|
||||||
|
|
||||||
class CruiseLayout(Widget):
|
class CruiseLayout(Widget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -76,6 +81,11 @@ class CruiseLayout(Widget):
|
|||||||
min_value=1, max_value=3, value_change_step=1,
|
min_value=1, max_value=3, value_change_step=1,
|
||||||
inline=True)
|
inline=True)
|
||||||
|
|
||||||
|
self.accel_boost_toggle = toggle_item_sp(
|
||||||
|
title=tr("Extra Acceleration Boost by Following Distance"),
|
||||||
|
description="",
|
||||||
|
param="FollowDistanceAccelBoost")
|
||||||
|
|
||||||
self.sla_settings_button = simple_button_item_sp(
|
self.sla_settings_button = simple_button_item_sp(
|
||||||
button_text=lambda: tr("Speed Limit"),
|
button_text=lambda: tr("Speed Limit"),
|
||||||
button_width=800,
|
button_width=800,
|
||||||
@@ -86,6 +96,7 @@ class CruiseLayout(Widget):
|
|||||||
self.icbm_toggle,
|
self.icbm_toggle,
|
||||||
self.scc_v_toggle,
|
self.scc_v_toggle,
|
||||||
self.scc_m_toggle,
|
self.scc_m_toggle,
|
||||||
|
self.accel_boost_toggle,
|
||||||
self.custom_acc_toggle,
|
self.custom_acc_toggle,
|
||||||
self.custom_acc_short_increment,
|
self.custom_acc_short_increment,
|
||||||
self.custom_acc_long_increment,
|
self.custom_acc_long_increment,
|
||||||
@@ -104,6 +115,7 @@ class CruiseLayout(Widget):
|
|||||||
self._scroller.show_event()
|
self._scroller.show_event()
|
||||||
self.icbm_toggle.show_description(True)
|
self.icbm_toggle.show_description(True)
|
||||||
self.custom_acc_toggle.show_description(True)
|
self.custom_acc_toggle.show_description(True)
|
||||||
|
self.accel_boost_toggle.show_description(True)
|
||||||
|
|
||||||
def _set_current_panel(self, panel: PanelType):
|
def _set_current_panel(self, panel: PanelType):
|
||||||
self._current_panel = panel
|
self._current_panel = panel
|
||||||
@@ -148,10 +160,17 @@ class CruiseLayout(Widget):
|
|||||||
self.scc_v_toggle.action_item.set_enabled(False)
|
self.scc_v_toggle.action_item.set_enabled(False)
|
||||||
self.scc_m_toggle.action_item.set_enabled(False)
|
self.scc_m_toggle.action_item.set_enabled(False)
|
||||||
|
|
||||||
|
self.accel_boost_toggle.action_item.set_enabled(has_long)
|
||||||
|
boost_desc = tr(ACCEL_BOOST_EXT_DESCRIPTION) if has_long else tr(ACCEL_BOOST_EXT_NOLONG_DESCRIPTION)
|
||||||
|
if self.accel_boost_toggle.description != boost_desc:
|
||||||
|
self.accel_boost_toggle.set_description(boost_desc)
|
||||||
|
self.accel_boost_toggle.show_description(True)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
has_icbm = has_long = False
|
has_icbm = has_long = False
|
||||||
self.icbm_toggle.action_item.set_enabled(False)
|
self.icbm_toggle.action_item.set_enabled(False)
|
||||||
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
|
self.icbm_toggle.set_description(tr(ONROAD_ONLY_DESCRIPTION))
|
||||||
|
self.accel_boost_toggle.action_item.set_enabled(False)
|
||||||
|
|
||||||
show_custom_acc_desc = False
|
show_custom_acc_desc = False
|
||||||
|
|
||||||
|
|||||||
@@ -102,13 +102,18 @@ class ModelsLayout(Widget):
|
|||||||
|
|
||||||
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
self.lagd_toggle = toggle_item_sp(tr("Live Learning Steer Delay"), "", param="LagdToggle")
|
||||||
|
|
||||||
|
self._blendful_desc = tr("Adds a gentle nudge toward lane center on top of the model's own steering, stronger when lane lines are clear." +
|
||||||
|
" Yields around obstacles. Not available with the Default model.")
|
||||||
|
self.blendful_toggle = toggle_item_sp(tr("Blendful (Alpha)"), self._blendful_desc, param="Blendful")
|
||||||
|
|
||||||
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
self.camera_offset = option_item_sp(tr("Adjust Camera Offset"), "CameraOffset", -35, 35,
|
||||||
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
tr("Virtually shift camera's perspective to move model's center to Left(+ values) or Right (- values)"),
|
||||||
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
1, None, True, "", style.BUTTON_ACTION_WIDTH, None, True,
|
||||||
lambda v: f"{v / 100:.2f} m")
|
lambda v: f"{v / 100:.2f} m")
|
||||||
|
|
||||||
self.items = [self.small_model_item, self.big_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item,
|
self.items = [self.small_model_item, self.big_model_item, self.cancel_download_item, self.download_item, self.refresh_item, self.clear_cache_item,
|
||||||
self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control, self.camera_offset]
|
self.blendful_toggle, self.lane_turn_desire_toggle, self.lane_turn_value_control, self.lagd_toggle, self.delay_control,
|
||||||
|
self.camera_offset]
|
||||||
|
|
||||||
def _update_lagd_description(self, lagd_toggle: bool):
|
def _update_lagd_description(self, lagd_toggle: bool):
|
||||||
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
desc = tr("Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. " +
|
||||||
@@ -235,13 +240,17 @@ class ModelsLayout(Widget):
|
|||||||
progress = sum(p.progress for p in progresses) / len(progresses)
|
progress = sum(p.progress for p in progresses) / len(progresses)
|
||||||
ds = custom.ModelManagerSP.DownloadStatus
|
ds = custom.ModelManagerSP.DownloadStatus
|
||||||
|
|
||||||
|
if any(p.canceling for p in progresses):
|
||||||
|
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("canceling...")}
|
||||||
if ds.failed in statuses:
|
if ds.failed in statuses:
|
||||||
# close.png is authored black and a tint cannot lift it, hence close2
|
# close.png is authored black and a tint cannot lift it, hence close2
|
||||||
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
|
return {"name": name, "status_text": tr("download failed"), "text_color": rl.RED, "icon": "icons/close2.png"}
|
||||||
if ds.verifying in statuses:
|
if ds.verifying in statuses:
|
||||||
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
|
return {"name": name, "downloading": True, "progress": progress, "status_text": tr("verifying")}
|
||||||
if ds.downloading in statuses:
|
if ds.downloading in statuses:
|
||||||
return {"name": name, "downloading": True, "progress": progress}
|
speed = max((p.speed for p in progresses), default=0.0)
|
||||||
|
status_text = f"{speed / 1024 ** 2:.1f} MB/s" if speed > 0 else "--.- MB/s"
|
||||||
|
return {"name": name, "downloading": True, "progress": progress, "status_text": status_text}
|
||||||
if statuses <= {ds.downloaded, ds.cached}:
|
if statuses <= {ds.downloaded, ds.cached}:
|
||||||
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
|
return {"name": name, "text_color": ON_COLOR, "icon": "icons/checkmark.png"}
|
||||||
# circled_slash is authored grey; tinting it again only darkens it
|
# circled_slash is authored grey; tinting it again only darkens it
|
||||||
@@ -318,6 +327,21 @@ class ModelsLayout(Widget):
|
|||||||
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
self.lane_turn_desire_toggle.action_item.set_state(turn_desire)
|
||||||
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
self.lane_turn_value_control.set_visible(turn_desire and advanced_controls)
|
||||||
self.lagd_toggle.action_item.set_state(live_delay)
|
self.lagd_toggle.action_item.set_state(live_delay)
|
||||||
|
|
||||||
|
blendful_allowed = ui_state.active_runner_tinygrad
|
||||||
|
if blendful_allowed:
|
||||||
|
self.blendful_toggle.action_item.set_state(ui_state.params.get_bool("Blendful"))
|
||||||
|
self.blendful_toggle.action_item.set_enabled(True)
|
||||||
|
else:
|
||||||
|
self.blendful_toggle.action_item.set_state(False)
|
||||||
|
self.blendful_toggle.action_item.set_enabled(False)
|
||||||
|
|
||||||
|
blendful_desc = self._blendful_desc if blendful_allowed else \
|
||||||
|
f"<b>{tr('Select a non-Default model to use Blendful.')}</b><br><br>{self._blendful_desc}"
|
||||||
|
if self.blendful_toggle.description != blendful_desc:
|
||||||
|
self.blendful_toggle.set_description(blendful_desc)
|
||||||
|
self.blendful_toggle.show_description(not blendful_allowed)
|
||||||
|
|
||||||
self.delay_control.set_visible(not live_delay and advanced_controls)
|
self.delay_control.set_visible(not live_delay and advanced_controls)
|
||||||
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
new_step = int(round(100 / CV.MPH_TO_KPH)) if ui_state.is_metric else 100
|
||||||
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
if self.lane_turn_value_control.action_item is not None and self.lane_turn_value_control.action_item.value_change_step != new_step:
|
||||||
|
|||||||
@@ -11,10 +11,12 @@ from openpilot.system.ui.lib.multilang import tr, tr_noop
|
|||||||
from openpilot.system.ui.widgets import DialogResult
|
from openpilot.system.ui.widgets import DialogResult
|
||||||
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog
|
||||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp
|
||||||
|
from opendbc.car.toyota.values import ToyotaFlags
|
||||||
|
|
||||||
|
|
||||||
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
ONROAD_ONLY_DESCRIPTION = tr_noop("Start the vehicle to check vehicle compatibility.")
|
||||||
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
|
SNG_HACK_UNAVAILABLE = tr_noop("sunnypilot Longitudinal Control must be available and enabled for your vehicle to use this feature.")
|
||||||
|
PLATFORM_UNAVAILABLE = tr_noop("This feature is currently not available on this platform.")
|
||||||
|
|
||||||
DESCRIPTIONS = {
|
DESCRIPTIONS = {
|
||||||
'enforce_stock_longitudinal': tr_noop(
|
'enforce_stock_longitudinal': tr_noop(
|
||||||
@@ -23,7 +25,11 @@ DESCRIPTIONS = {
|
|||||||
'stop_and_go_hack': tr_noop(
|
'stop_and_go_hack': tr_noop(
|
||||||
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
'sunnypilot will allow some Toyota/Lexus cars to auto resume during stop and go traffic. ' +
|
||||||
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
'This feature is only applicable to certain models that are able to use longitudinal control. This is an alpha feature. Use at your own risk.'
|
||||||
)
|
),
|
||||||
|
'tss2_smooth': tr_noop(
|
||||||
|
'Enable smoother longitudinal control for TSS2 vehicles. ' +
|
||||||
|
'Uses a custom speed-dependent integral gain curve for smoother traffic driving.'
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,9 +53,18 @@ class ToyotaSettings(BrandSettings):
|
|||||||
enabled=lambda: not ui_state.engaged,
|
enabled=lambda: not ui_state.engaged,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.tss2_smooth = toggle_item_sp(
|
||||||
|
lambda: tr("TSS2 Smooth Longitudinal (Alpha)"),
|
||||||
|
description=lambda: tr(DESCRIPTIONS["tss2_smooth"]),
|
||||||
|
initial_state=ui_state.params.get_bool("ToyotaTSS2Smooth"),
|
||||||
|
callback=self._on_enable_tss2_smooth,
|
||||||
|
enabled=lambda: not ui_state.engaged,
|
||||||
|
)
|
||||||
|
|
||||||
self.items = [
|
self.items = [
|
||||||
self.enforce_stock_longitudinal,
|
self.enforce_stock_longitudinal,
|
||||||
self.stop_and_go_hack,
|
self.stop_and_go_hack,
|
||||||
|
self.tss2_smooth,
|
||||||
]
|
]
|
||||||
|
|
||||||
def _on_enable_enforce_stock_longitudinal(self, state: bool):
|
def _on_enable_enforce_stock_longitudinal(self, state: bool):
|
||||||
@@ -61,6 +76,8 @@ class ToyotaSettings(BrandSettings):
|
|||||||
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
|
ui_state.params.put_bool("AlphaLongitudinalEnabled", False)
|
||||||
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
|
ui_state.params.put_bool("ToyotaStopAndGoHack", False)
|
||||||
self.stop_and_go_hack.action_item.set_state(False)
|
self.stop_and_go_hack.action_item.set_state(False)
|
||||||
|
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
|
||||||
|
self.tss2_smooth.action_item.set_state(False)
|
||||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||||
else:
|
else:
|
||||||
self.enforce_stock_longitudinal.action_item.set_state(False)
|
self.enforce_stock_longitudinal.action_item.set_state(False)
|
||||||
@@ -75,6 +92,25 @@ class ToyotaSettings(BrandSettings):
|
|||||||
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
|
ui_state.params.put_bool("ToyotaEnforceStockLongitudinal", False)
|
||||||
ui_state.params.put_bool("OnroadCycleRequested", True)
|
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||||
|
|
||||||
|
def _on_enable_tss2_smooth(self, state: bool):
|
||||||
|
if state:
|
||||||
|
def confirm_callback(result: int):
|
||||||
|
if result == DialogResult.CONFIRM:
|
||||||
|
ui_state.params.put_bool("ToyotaTSS2Smooth", True)
|
||||||
|
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||||
|
else:
|
||||||
|
self.tss2_smooth.action_item.set_state(False)
|
||||||
|
|
||||||
|
content = (f"<h1>{self.tss2_smooth.title}</h1><br>" +
|
||||||
|
f"<p>{self.tss2_smooth.description}</p>")
|
||||||
|
|
||||||
|
dlg = ConfirmDialog(content, tr("Enable"), rich=True, callback=confirm_callback)
|
||||||
|
gui_app.push_widget(dlg)
|
||||||
|
|
||||||
|
else:
|
||||||
|
ui_state.params.put_bool("ToyotaTSS2Smooth", False)
|
||||||
|
ui_state.params.put_bool("OnroadCycleRequested", True)
|
||||||
|
|
||||||
def _on_enable_stop_and_go_hack(self, state: bool):
|
def _on_enable_stop_and_go_hack(self, state: bool):
|
||||||
if state:
|
if state:
|
||||||
def confirm_callback(result: int):
|
def confirm_callback(result: int):
|
||||||
@@ -113,9 +149,32 @@ class ToyotaSettings(BrandSettings):
|
|||||||
self.stop_and_go_hack.set_description(new_desc)
|
self.stop_and_go_hack.set_description(new_desc)
|
||||||
if show_desc:
|
if show_desc:
|
||||||
self.stop_and_go_hack.show_description(True)
|
self.stop_and_go_hack.show_description(True)
|
||||||
|
|
||||||
|
tss2 = bool(ui_state.CP.flags & ToyotaFlags.TSS2)
|
||||||
|
if longitudinal and not enforce_stock and tss2:
|
||||||
|
self.tss2_smooth.action_item.set_enabled(not ui_state.engaged)
|
||||||
|
new_desc = tr(DESCRIPTIONS["tss2_smooth"])
|
||||||
|
show_desc = False
|
||||||
|
else:
|
||||||
|
self.tss2_smooth.action_item.set_enabled(False)
|
||||||
|
self.tss2_smooth.action_item.set_state(False)
|
||||||
|
unavailable = SNG_HACK_UNAVAILABLE if tss2 else PLATFORM_UNAVAILABLE
|
||||||
|
new_desc = "<b>" + tr(unavailable) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
|
||||||
|
show_desc = True
|
||||||
|
|
||||||
|
if self.tss2_smooth.description != new_desc:
|
||||||
|
self.tss2_smooth.set_description(new_desc)
|
||||||
|
if show_desc:
|
||||||
|
self.tss2_smooth.show_description(True)
|
||||||
else:
|
else:
|
||||||
self.stop_and_go_hack.action_item.set_enabled(False)
|
self.stop_and_go_hack.action_item.set_enabled(False)
|
||||||
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
|
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["stop_and_go_hack"])
|
||||||
if self.stop_and_go_hack.description != new_desc:
|
if self.stop_and_go_hack.description != new_desc:
|
||||||
self.stop_and_go_hack.set_description(new_desc)
|
self.stop_and_go_hack.set_description(new_desc)
|
||||||
self.stop_and_go_hack.show_description(True)
|
self.stop_and_go_hack.show_description(True)
|
||||||
|
|
||||||
|
self.tss2_smooth.action_item.set_enabled(False)
|
||||||
|
new_desc = "<b>" + tr(ONROAD_ONLY_DESCRIPTION) + "</b>\n\n" + tr(DESCRIPTIONS["tss2_smooth"])
|
||||||
|
if self.tss2_smooth.description != new_desc:
|
||||||
|
self.tss2_smooth.set_description(new_desc)
|
||||||
|
self.tss2_smooth.show_description(True)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import pyray as rl
|
|||||||
from openpilot.cereal import custom
|
from openpilot.cereal import custom
|
||||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||||
from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle
|
from openpilot.sunnypilot.models.helpers import ACTIVE_BUNDLE_KEYS, get_selected_bundle
|
||||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
from openpilot.selfdrive.ui.mici.widgets.button import BigButton, BigParamControl
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||||
from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model,
|
from openpilot.selfdrive.ui.sunnypilot.model_info import (active_source, big_model_state, bundles_for_source, carrying_model,
|
||||||
default_model_name, model_cache_size_mb, model_info, queued_name,
|
default_model_name, model_cache_size_mb, model_info, queued_name,
|
||||||
@@ -95,7 +95,12 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.clear_cache_btn.set_click_callback(self._confirm_clear_cache)
|
self.clear_cache_btn.set_click_callback(self._confirm_clear_cache)
|
||||||
self._cache_size_time = 0.0
|
self._cache_size_time = 0.0
|
||||||
|
|
||||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn]
|
self.blendful_toggle = BigParamControl(tr("blendful (alpha)"), "Blendful",
|
||||||
|
description=tr("Adds a gentle nudge toward lane center on top of the model's own steering, stronger when " +
|
||||||
|
"lane lines are clear. Yields around obstacles. Not available with the Default model."))
|
||||||
|
|
||||||
|
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn, self.refresh_btn, self.clear_cache_btn,
|
||||||
|
self.blendful_toggle]
|
||||||
self._scroller.add_widgets(self.main_items)
|
self._scroller.add_widgets(self.main_items)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -205,6 +210,10 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
device.set_override_interactive_timeout(None)
|
device.set_override_interactive_timeout(None)
|
||||||
self._was_downloading = False
|
self._was_downloading = False
|
||||||
|
|
||||||
|
def show_event(self):
|
||||||
|
super().show_event()
|
||||||
|
self.blendful_toggle.refresh()
|
||||||
|
|
||||||
def _update_state(self):
|
def _update_state(self):
|
||||||
super()._update_state()
|
super()._update_state()
|
||||||
|
|
||||||
@@ -213,6 +222,17 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.current_model_info.current_model_header._shimmer = False
|
self.current_model_info.current_model_header._shimmer = False
|
||||||
self.current_model_info.info_header._shimmer = False
|
self.current_model_info.info_header._shimmer = False
|
||||||
|
|
||||||
|
blendful_allowed = ui_state.active_runner_tinygrad
|
||||||
|
if blendful_allowed:
|
||||||
|
self.blendful_toggle.set_checked(ui_state.params.get_bool("Blendful"))
|
||||||
|
self.blendful_toggle.set_enabled(True)
|
||||||
|
else:
|
||||||
|
self.blendful_toggle.set_checked(False)
|
||||||
|
self.blendful_toggle.set_enabled(False)
|
||||||
|
blendful_value = "" if blendful_allowed else tr("custom model only")
|
||||||
|
if self.blendful_toggle.value != blendful_value:
|
||||||
|
self.blendful_toggle.set_value(blendful_value)
|
||||||
|
|
||||||
manager = self.model_manager
|
manager = self.model_manager
|
||||||
self._download_frame += 1
|
self._download_frame += 1
|
||||||
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
||||||
@@ -251,11 +271,13 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||||
|
|
||||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||||
self.cancel_download_btn.set_visible(True)
|
self.cancel_download_btn.set_visible(ui_state.params.get("ModelManager_DownloadRef") is not None)
|
||||||
device.set_override_interactive_timeout(5)
|
device.set_override_interactive_timeout(5)
|
||||||
progress = 0.0
|
progress = 0.0
|
||||||
count = 0
|
count = 0
|
||||||
verifying = False
|
verifying = False
|
||||||
|
canceling = False
|
||||||
|
speed = 0.0
|
||||||
for model in manager.selectedBundle.models:
|
for model in manager.selectedBundle.models:
|
||||||
count += 1
|
count += 1
|
||||||
p = model.artifact.downloadProgress
|
p = model.artifact.downloadProgress
|
||||||
@@ -263,11 +285,14 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
custom.ModelManagerSP.DownloadStatus.verifying):
|
custom.ModelManagerSP.DownloadStatus.verifying):
|
||||||
progress += p.progress
|
progress += p.progress
|
||||||
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
|
verifying = verifying or p.status == custom.ModelManagerSP.DownloadStatus.verifying
|
||||||
|
canceling = canceling or p.canceling
|
||||||
|
speed = max(speed, p.speed) # only one file downloads at a time, so the non-zero speed is the live one
|
||||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||||
custom.ModelManagerSP.DownloadStatus.cached):
|
custom.ModelManagerSP.DownloadStatus.cached):
|
||||||
progress += 100.0
|
progress += 100.0
|
||||||
|
|
||||||
self.current_model_info.current_model_header.set_text(tr("verifying") if verifying else tr("downloading"))
|
header = tr("canceling...") if canceling else tr("verifying") if verifying else tr("downloading")
|
||||||
|
self.current_model_info.current_model_header.set_text(header)
|
||||||
self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download"))
|
self.cancel_download_btn.set_text(tr("cancel verification") if verifying else tr("cancel download"))
|
||||||
self.current_model_info.current_model_header._shimmer = True
|
self.current_model_info.current_model_header._shimmer = True
|
||||||
name_text = manager.selectedBundle.internalName.lower()
|
name_text = manager.selectedBundle.internalName.lower()
|
||||||
@@ -276,7 +301,10 @@ class ModelsLayoutMici(NavScroller):
|
|||||||
self.current_model_info.current_model_text.set_text(name_text)
|
self.current_model_info.current_model_text.set_text(name_text)
|
||||||
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
self.current_model_info.info_header.set_text(tr("progress") + self._download_progress)
|
||||||
self.current_model_info.info_header._shimmer = True
|
self.current_model_info.info_header._shimmer = True
|
||||||
self.current_model_info.info_text.set_text(f"{progress/count:.2f}%")
|
progress_text = f"{progress/count:.2f}%"
|
||||||
|
if not verifying and not canceling:
|
||||||
|
progress_text += f" | {speed / 1024 ** 2:.1f} MB/s" if speed > 0 else " | --.- MB/s"
|
||||||
|
self.current_model_info.info_text.set_text(progress_text)
|
||||||
|
|
||||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
|
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloaded:
|
||||||
self.current_model_info.info_header.set_text(tr("downloaded"))
|
self.current_model_info.info_header.set_text(tr("downloaded"))
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, 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.cereal import log
|
||||||
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.selfdrive.ui.mici.onroad import SIDE_PANEL_WIDTH
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
from openpilot.system.ui.widgets import Widget
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import get_accel_boost_max
|
||||||
|
|
||||||
|
# The readout is wider than the side panel, so the value is right-aligned and bleeds over the camera view
|
||||||
|
BAR_WIDTH = 24
|
||||||
|
# Fades to white only near the profile cap, like the stock torque bar fading toward orange at high torque
|
||||||
|
CAP_BLEND_START = 0.75
|
||||||
|
MAX_BOOST_BLEND = 0.45
|
||||||
|
|
||||||
|
COLOR_TRACK = rl.Color(255, 255, 255, 45)
|
||||||
|
COLOR_TRACK_EDGE = rl.Color(255, 255, 255, 70)
|
||||||
|
|
||||||
|
AGGRESSIVE_COLOR = rl.Color(255, 80, 60, 255)
|
||||||
|
STANDARD_COLOR = rl.Color(0, 230, 120, 255)
|
||||||
|
RELAXED_COLOR = rl.Color(70, 150, 255, 255)
|
||||||
|
|
||||||
|
FADE_THRESHOLD = 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def lighten(color: rl.Color, amount: float) -> rl.Color:
|
||||||
|
"""Lerp towards white in RGB, so the hue stays put the way HSV blending would not."""
|
||||||
|
amount = min(max(amount, 0.0), 1.0)
|
||||||
|
return rl.Color(int(color.r + (255 - color.r) * amount),
|
||||||
|
int(color.g + (255 - color.g) * amount),
|
||||||
|
int(color.b + (255 - color.b) * amount), color.a)
|
||||||
|
|
||||||
|
|
||||||
|
def get_boost_fraction(boost: float, max_boost: float) -> float:
|
||||||
|
"""How much of the active profile's boost budget is in use, clamped to a full bar."""
|
||||||
|
if max_boost <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return min(max(boost / max_boost, 0.0), 1.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_boost_style(personality) -> tuple[rl.Color, float]:
|
||||||
|
if personality == log.LongitudinalPersonality.aggressive:
|
||||||
|
color = AGGRESSIVE_COLOR
|
||||||
|
elif personality == log.LongitudinalPersonality.relaxed:
|
||||||
|
color = RELAXED_COLOR
|
||||||
|
else:
|
||||||
|
color = STANDARD_COLOR
|
||||||
|
|
||||||
|
return color, get_accel_boost_max(personality)
|
||||||
|
|
||||||
|
|
||||||
|
class AccelBoostIndicator(Widget):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self._boost_filter = FirstOrderFilter(0.0, 0.15, 1 / gui_app.target_fps)
|
||||||
|
self._alpha_filter = FirstOrderFilter(0.0, 0.1, 1 / gui_app.target_fps)
|
||||||
|
# Smoothly rescale the bar when the follow distance profile changes mid-drive
|
||||||
|
self._max_boost_filter = FirstOrderFilter(0.0, 0.25, 1 / gui_app.target_fps)
|
||||||
|
self._profile_color = STANDARD_COLOR
|
||||||
|
self._max_boost = 0.0
|
||||||
|
|
||||||
|
self.panel_in_use = False
|
||||||
|
|
||||||
|
def _update_state(self):
|
||||||
|
sm = ui_state.sm
|
||||||
|
extra_enabled = ui_state.follow_distance_accel_boost
|
||||||
|
visible = (ui_state.has_longitudinal_control and sm.alive['selfdriveState'] and sm.alive['longitudinalPlan'] and
|
||||||
|
sm['selfdriveState'].enabled)
|
||||||
|
self._alpha_filter.update(1.0 if visible and extra_enabled else 0.0)
|
||||||
|
self._boost_filter.update(float(sm['longitudinalPlan'].accelBoost) if visible else 0.0)
|
||||||
|
|
||||||
|
if sm.alive['selfdriveState']:
|
||||||
|
self._profile_color, self._max_boost = get_boost_style(sm['selfdriveState'].personality)
|
||||||
|
self._max_boost_filter.update(self._max_boost)
|
||||||
|
|
||||||
|
# The bar owns the panel only while it is actually drawn; the ball returns when the bar fades out
|
||||||
|
self.panel_in_use = visible and extra_enabled
|
||||||
|
|
||||||
|
def _render(self, _):
|
||||||
|
alpha = self._alpha_filter.x
|
||||||
|
if alpha < FADE_THRESHOLD or self._max_boost <= 0.0:
|
||||||
|
return
|
||||||
|
|
||||||
|
cx = self.rect.x + self.rect.width - SIDE_PANEL_WIDTH / 2
|
||||||
|
bar_x = cx - BAR_WIDTH / 2
|
||||||
|
track_top = self.rect.y + self.rect.height * 0.08
|
||||||
|
track_bottom = self.rect.y + self.rect.height * 0.92
|
||||||
|
track_h = max(1.0, track_bottom - track_top)
|
||||||
|
|
||||||
|
# track, light enough to read on the black side panel
|
||||||
|
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12,
|
||||||
|
rl.Color(COLOR_TRACK.r, COLOR_TRACK.g, COLOR_TRACK.b, int(COLOR_TRACK.a * alpha)))
|
||||||
|
rl.draw_rectangle_rounded_lines_ex(rl.Rectangle(bar_x, track_top, BAR_WIDTH, track_h), 0.5, 12, 1,
|
||||||
|
rl.Color(COLOR_TRACK_EDGE.r, COLOR_TRACK_EDGE.g, COLOR_TRACK_EDGE.b,
|
||||||
|
int(COLOR_TRACK_EDGE.a * alpha)))
|
||||||
|
|
||||||
|
# Scale to the current profile's cap, so a maxed boost fills the bar and a profile change rescales it
|
||||||
|
boost = max(self._boost_filter.x, 0.0)
|
||||||
|
ratio = get_boost_fraction(boost, self._max_boost_filter.x)
|
||||||
|
fill_h = ratio * track_h
|
||||||
|
cap_blend = max(0.0, (ratio - CAP_BLEND_START) / (1.0 - CAP_BLEND_START))
|
||||||
|
fill_color = lighten(self._profile_color, MAX_BOOST_BLEND * cap_blend)
|
||||||
|
if fill_h > 0.0:
|
||||||
|
# soft glow around the tip, like the confidence ball's gradient
|
||||||
|
tip_y = track_bottom - fill_h
|
||||||
|
glow = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(70 * alpha))
|
||||||
|
rl.draw_circle_gradient(rl.Vector2(cx, tip_y), int(BAR_WIDTH * 0.9), glow, rl.BLANK)
|
||||||
|
|
||||||
|
fill = rl.Color(fill_color.r, fill_color.g, fill_color.b, int(255 * alpha))
|
||||||
|
if fill_h < BAR_WIDTH:
|
||||||
|
# Keep the cap inside the track: a fixed radius would paint below the track near zero boost
|
||||||
|
radius = max(1, int(fill_h / 2))
|
||||||
|
rl.draw_circle(int(cx), int(track_bottom - radius), radius, fill)
|
||||||
|
else:
|
||||||
|
rl.draw_rectangle_rounded(rl.Rectangle(bar_x, track_bottom - fill_h, BAR_WIDTH, fill_h), 0.5, 12, fill)
|
||||||
@@ -6,6 +6,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import pyray as rl
|
import pyray as rl
|
||||||
from openpilot.selfdrive.ui.ui_state import UIStatus
|
from openpilot.selfdrive.ui.ui_state import UIStatus
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||||
|
|
||||||
LANE_LINE_COLORS_SP = {
|
LANE_LINE_COLORS_SP = {
|
||||||
@@ -17,3 +18,4 @@ LANE_LINE_COLORS_SP = {
|
|||||||
class ModelRendererSP:
|
class ModelRendererSP:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rainbow_path = RainbowPath()
|
self.rainbow_path = RainbowPath()
|
||||||
|
self.blendful = BlendfulRenderer(rect_relative=True)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, 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 unittest
|
||||||
|
|
||||||
|
from openpilot.cereal import log
|
||||||
|
from openpilot.selfdrive.controls.lib.accel_boost import ACCEL_BOOST_MAX
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.mici.onroad.accel_boost_indicator import (get_boost_style, get_boost_fraction, lighten,
|
||||||
|
AGGRESSIVE_COLOR, RELAXED_COLOR, STANDARD_COLOR)
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import (SCALE_AGGRESSIVE, SCALE_RELAXED, SCALE_STANDARD,
|
||||||
|
get_accel_boost_max)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccelBoostIndicator(unittest.TestCase):
|
||||||
|
def test_bar_scales_with_following_distance(self):
|
||||||
|
for personality, color, scale in (
|
||||||
|
(log.LongitudinalPersonality.relaxed, RELAXED_COLOR, SCALE_RELAXED),
|
||||||
|
(log.LongitudinalPersonality.standard, STANDARD_COLOR, SCALE_STANDARD),
|
||||||
|
(log.LongitudinalPersonality.aggressive, AGGRESSIVE_COLOR, SCALE_AGGRESSIVE)):
|
||||||
|
bar_color, full_scale = get_boost_style(personality)
|
||||||
|
self.assertEqual(bar_color, color)
|
||||||
|
self.assertAlmostEqual(full_scale, ACCEL_BOOST_MAX * (1.0 + scale))
|
||||||
|
|
||||||
|
def test_bar_fills_at_the_active_profile_cap(self):
|
||||||
|
caps = [get_accel_boost_max(p) for p in (log.LongitudinalPersonality.relaxed,
|
||||||
|
log.LongitudinalPersonality.standard,
|
||||||
|
log.LongitudinalPersonality.aggressive)]
|
||||||
|
self.assertEqual(caps, sorted(caps))
|
||||||
|
|
||||||
|
for cap in caps:
|
||||||
|
self.assertAlmostEqual(get_boost_fraction(cap, cap), 1.0)
|
||||||
|
self.assertAlmostEqual(get_boost_fraction(0.0, cap), 0.0)
|
||||||
|
self.assertAlmostEqual(get_boost_fraction(cap * 0.5, cap), 0.5)
|
||||||
|
|
||||||
|
# A boost beyond the current cap (e.g. after switching to a shorter profile) still fills the bar
|
||||||
|
self.assertAlmostEqual(get_boost_fraction(caps[2], caps[0]), 1.0)
|
||||||
|
self.assertAlmostEqual(get_boost_fraction(0.0, 0.0), 0.0)
|
||||||
|
|
||||||
|
def test_cap_lighten_keeps_the_profile_hue(self):
|
||||||
|
# An HSV blend towards white sweeps relaxed blue through pink, so the fill lightens in RGB instead
|
||||||
|
relaxed = lighten(RELAXED_COLOR, 0.45)
|
||||||
|
aggressive = lighten(AGGRESSIVE_COLOR, 0.45)
|
||||||
|
self.assertGreater(relaxed.b, relaxed.r)
|
||||||
|
self.assertGreaterEqual(relaxed.b, relaxed.g)
|
||||||
|
self.assertGreater(aggressive.r, aggressive.b)
|
||||||
|
# lighter in RGB means the dimmest channel came up towards white
|
||||||
|
self.assertGreater(relaxed.r, RELAXED_COLOR.r)
|
||||||
|
self.assertGreater(aggressive.g, AGGRESSIVE_COLOR.g)
|
||||||
|
|
||||||
|
def test_wire_personality_picks_the_right_profile(self):
|
||||||
|
# Over the wire personality is a capnp DynamicEnum, which compares equal to the cereal enum but hashes differently
|
||||||
|
msg = log.SelfdriveState.new_message()
|
||||||
|
msg.personality = log.LongitudinalPersonality.aggressive
|
||||||
|
color, full_scale = get_boost_style(msg.personality)
|
||||||
|
self.assertEqual(color, AGGRESSIVE_COLOR)
|
||||||
|
self.assertAlmostEqual(full_scale, get_accel_boost_max(log.LongitudinalPersonality.aggressive))
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, 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.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||||
|
from openpilot.sunnypilot.modeld_v2.blendful import LANE_WIDTH_CLIP, LOOKAHEAD_TIME, MIN_LOOKAHEAD
|
||||||
|
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||||
|
from openpilot.system.ui.lib.shader_polygon import draw_polygon
|
||||||
|
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||||
|
|
||||||
|
BLENDFUL_COLOR = (0, 200, 255)
|
||||||
|
FULL_BAR = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
class BlendfulRenderer:
|
||||||
|
def __init__(self, readout: bool = True, rect_relative: bool = False):
|
||||||
|
self._readout = readout
|
||||||
|
self._rect_relative = rect_relative
|
||||||
|
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||||
|
self._trust = 0.0
|
||||||
|
self._font = gui_app.font(FontWeight.BOLD)
|
||||||
|
self._pull_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||||
|
self._trust_display = FirstOrderFilter(0.0, 0.5, DT_MDL)
|
||||||
|
self._alpha = FirstOrderFilter(0.0, 0.3, DT_MDL)
|
||||||
|
|
||||||
|
def update(self, renderer, max_idx: int, *map_args) -> None:
|
||||||
|
blendful = ui_state.sm['modelDataV2SP']
|
||||||
|
if ui_state.sm.updated['modelDataV2SP']:
|
||||||
|
self._alpha.update(float(blendful.blendfulTrust > 0.0))
|
||||||
|
if blendful.blendfulTrust > 0.0:
|
||||||
|
self._trust = blendful.blendfulTrust
|
||||||
|
if self._readout:
|
||||||
|
lookahead = max(ui_state.sm['carState'].vEgo * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||||
|
self._pull_display.update(blendful.blendfulCurvature * lookahead ** 2 / 2)
|
||||||
|
self._trust_display.update(self._trust)
|
||||||
|
|
||||||
|
left, right = renderer._lane_lines[1].raw_points, renderer._lane_lines[2].raw_points
|
||||||
|
stds = list(ui_state.sm['modelV2'].laneLineStds)
|
||||||
|
if self._alpha.x < 0.01 or len(left) == 0 or len(right) != len(left) or len(stds) < 3:
|
||||||
|
self._center_points = np.empty((0, 2), dtype=np.float32)
|
||||||
|
return
|
||||||
|
|
||||||
|
l_prob, r_prob = blendful.blendfulLeftProb, blendful.blendfulRightProb
|
||||||
|
if blendful.blendfulTrust > 0.0 and l_prob + r_prob > 0.0:
|
||||||
|
half_width = np.clip(right[:, 1] - left[:, 1], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else blendful.blendfulLaneWidth / 2
|
||||||
|
l_var = stds[1] ** 2 / max(l_prob, 1e-4)
|
||||||
|
r_var = stds[2] ** 2 / max(r_prob, 1e-4)
|
||||||
|
center = left.copy()
|
||||||
|
center[:, 1] = (r_var * (left[:, 1] + half_width) + l_var * (right[:, 1] - half_width)) / (l_var + r_var)
|
||||||
|
self._center_points = renderer._map_line_to_polygon(center, 0.15, 0.0, max_idx, *map_args)
|
||||||
|
|
||||||
|
def _color(self, rgb, alpha: float) -> rl.Color:
|
||||||
|
return rl.Color(*rgb, int(alpha * self._alpha.x))
|
||||||
|
|
||||||
|
def draw(self, rect: rl.Rectangle) -> None:
|
||||||
|
if not UIStateSP.lateral_active(ui_state):
|
||||||
|
return
|
||||||
|
if self._center_points.size and self._trust > 0.05:
|
||||||
|
points = self._center_points + (rect.x, rect.y) if self._rect_relative else self._center_points
|
||||||
|
draw_polygon(rect, points, self._color(BLENDFUL_COLOR, np.clip(self._trust, 0.2, 1.0) * 255))
|
||||||
|
if self._center_points.size and self._readout:
|
||||||
|
self._draw_readout(rect)
|
||||||
|
|
||||||
|
def _draw_readout(self, rect: rl.Rectangle) -> None:
|
||||||
|
pull, trust = self._pull_display.x, self._trust_display.x
|
||||||
|
w, h = 190, 76
|
||||||
|
x, y = rect.x + (rect.width - w) / 2, rect.y + 10
|
||||||
|
cx, cy = x + w / 2, y + 30
|
||||||
|
rl.draw_rectangle_rounded(rl.Rectangle(x, y, w, h), 0.5, 12, self._color((0, 0, 0), 170))
|
||||||
|
|
||||||
|
text = f"{abs(pull) * 100:.0f}"
|
||||||
|
text_w = measure_text_cached(self._font, text, 44, 0).x
|
||||||
|
rl.draw_text_ex(self._font, text, rl.Vector2(cx - text_w / 2, cy - 24), 44, 0, self._color((255, 255, 255), 255))
|
||||||
|
|
||||||
|
active = self._color(BLENDFUL_COLOR, np.clip(abs(pull) / 0.1, 0.0, 1.0) * 255)
|
||||||
|
idle = self._color((255, 255, 255), 40)
|
||||||
|
lx, rx = x + 18, x + w - 18
|
||||||
|
rl.draw_triangle(rl.Vector2(lx, cy), rl.Vector2(lx + 16, cy + 12), rl.Vector2(lx + 16, cy - 12), active if pull < 0 else idle)
|
||||||
|
rl.draw_triangle(rl.Vector2(rx - 16, cy - 12), rl.Vector2(rx - 16, cy + 12), rl.Vector2(rx, cy), active if pull > 0 else idle)
|
||||||
|
|
||||||
|
bar = rl.Rectangle(x + 24, y + h - 16, w - 48, 6)
|
||||||
|
rl.draw_rectangle_rounded(bar, 1.0, 6, self._color((255, 255, 255), 40))
|
||||||
|
rl.draw_rectangle(int(cx - 1), int(bar.y - 2), 2, int(bar.height + 4), self._color((255, 255, 255), 120))
|
||||||
|
fill = bar.width / 2 * float(np.sqrt(np.clip(abs(pull) / FULL_BAR, 0.0, 1.0)))
|
||||||
|
if fill > 1:
|
||||||
|
fill_x = cx if pull > 0 else cx - fill
|
||||||
|
rl.draw_rectangle_rounded(rl.Rectangle(fill_x, bar.y, fill, bar.height), 1.0, 6,
|
||||||
|
self._color(BLENDFUL_COLOR, np.clip(0.3 + 0.7 * trust, 0.0, 1.0) * 255))
|
||||||
@@ -5,10 +5,11 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
|||||||
See the LICENSE.md file in the root directory for more details.
|
See the LICENSE.md file in the root directory for more details.
|
||||||
"""
|
"""
|
||||||
from openpilot.common.filter_simple import FirstOrderFilter
|
from openpilot.common.filter_simple import FirstOrderFilter
|
||||||
from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus
|
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||||
|
from openpilot.selfdrive.ui.sunnypilot.onroad.blendful import BlendfulRenderer
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
|
||||||
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
|
||||||
from openpilot.selfdrive.ui.sunnypilot.ui_state import MADSState
|
from openpilot.selfdrive.ui.sunnypilot.ui_state import UIStateSP
|
||||||
from openpilot.system.ui.lib.application import gui_app
|
from openpilot.system.ui.lib.application import gui_app
|
||||||
|
|
||||||
|
|
||||||
@@ -16,16 +17,12 @@ class ModelRendererSP:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.rainbow_path = RainbowPath()
|
self.rainbow_path = RainbowPath()
|
||||||
self.chevron_metrics = ChevronMetrics()
|
self.chevron_metrics = ChevronMetrics()
|
||||||
|
self.blendful = BlendfulRenderer(readout=False)
|
||||||
self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps)
|
self._width_filter = FirstOrderFilter(0.9, 0.1, 1 / gui_app.target_fps)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _lateral_active(self) -> bool:
|
def _lateral_active(self) -> bool:
|
||||||
sm = ui_state.sm
|
return UIStateSP.lateral_active(ui_state)
|
||||||
if sm.valid["selfdriveStateSP"]:
|
|
||||||
mads = sm["selfdriveStateSP"].mads
|
|
||||||
if mads.available:
|
|
||||||
return mads.enabled and mads.state != MADSState.paused
|
|
||||||
return ui_state.status in (UIStatus.ENGAGED, UIStatus.LAT_ONLY)
|
|
||||||
|
|
||||||
def _get_path_half_width(self) -> float:
|
def _get_path_half_width(self) -> float:
|
||||||
target = 0.9 if self._lateral_active else 0.40
|
target = 0.9 if self._lateral_active else 0.40
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class UIStateSP:
|
|||||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||||
self.sm_services_ext = [
|
self.sm_services_ext = [
|
||||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||||
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay"
|
"lateralTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "lateralDelay", "modelDataV2SP"
|
||||||
]
|
]
|
||||||
|
|
||||||
self.sunnylink_state = SunnylinkState()
|
self.sunnylink_state = SunnylinkState()
|
||||||
@@ -46,6 +46,7 @@ class UIStateSP:
|
|||||||
|
|
||||||
self.active_bundle = None
|
self.active_bundle = None
|
||||||
self.model_runner_tinygrad: bool = False
|
self.model_runner_tinygrad: bool = False
|
||||||
|
self.active_runner_tinygrad: bool = False
|
||||||
self.blindspot: bool = False
|
self.blindspot: bool = False
|
||||||
self.chevron_metrics = None
|
self.chevron_metrics = None
|
||||||
self.custom_interactive_timeout: int = 0
|
self.custom_interactive_timeout: int = 0
|
||||||
@@ -64,6 +65,7 @@ class UIStateSP:
|
|||||||
self.enforce_torque_control: bool = False
|
self.enforce_torque_control: bool = False
|
||||||
self.custom_torque_params: bool = False
|
self.custom_torque_params: bool = False
|
||||||
self.torque_override_enabled: bool = False
|
self.torque_override_enabled: bool = False
|
||||||
|
self.follow_distance_accel_boost: bool = False
|
||||||
self._sp_initialized: bool = False
|
self._sp_initialized: bool = False
|
||||||
|
|
||||||
def update(self) -> None:
|
def update(self) -> None:
|
||||||
@@ -109,6 +111,14 @@ class UIStateSP:
|
|||||||
def auto_onroad_brightness(self) -> bool:
|
def auto_onroad_brightness(self) -> bool:
|
||||||
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
|
return self.onroad_brightness in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def lateral_active(_ui_state) -> bool:
|
||||||
|
if _ui_state.sm.valid["selfdriveStateSP"]:
|
||||||
|
mads = _ui_state.sm["selfdriveStateSP"].mads
|
||||||
|
if mads.available:
|
||||||
|
return mads.enabled and mads.state != MADSState.paused
|
||||||
|
return _ui_state.status.value in ("engaged", "lat_only")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_status(ss, ss_sp, onroad_evt) -> str:
|
def update_status(ss, ss_sp, onroad_evt) -> str:
|
||||||
state = ss.state
|
state = ss.state
|
||||||
@@ -157,6 +167,8 @@ class UIStateSP:
|
|||||||
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
|
chestnut_loading=self.chestnut_loading, offroad=self.is_offroad())
|
||||||
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
|
self.active_bundle = self.params.get(ACTIVE_BUNDLE_KEYS[source])
|
||||||
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
|
self.model_runner_tinygrad = self.active_bundle is not None and self.active_bundle.get("runner") == "tinygrad"
|
||||||
|
# runner the manager launches (process_config.is_tinygrad_model); with a chestnut the big slot decides
|
||||||
|
self.active_runner_tinygrad = self.params.get("ModelRunnerTypeCache") == custom.ModelManagerSP.Runner.tinygrad
|
||||||
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
|
# stock only counts the default big model's compiled pkl. a downloaded big bundle runs on the
|
||||||
# chestnut just the same, so ChestnutState has to see it as available too.
|
# chestnut just the same, so ChestnutState has to see it as available too.
|
||||||
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
|
self.chestnut_compiled = self.chestnut_compiled or self.model_runner_tinygrad
|
||||||
@@ -177,6 +189,7 @@ class UIStateSP:
|
|||||||
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||||
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
|
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
|
||||||
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
|
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
|
||||||
|
self.follow_distance_accel_boost = self.params.get_bool("FollowDistanceAccelBoost")
|
||||||
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
|
self.torque_override_lat_accel_factor = float(self.params.get("TorqueParamsOverrideLatAccelFactor", return_default=True))
|
||||||
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
|
self.torque_override_friction = float(self.params.get("TorqueParamsOverrideFriction", return_default=True))
|
||||||
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
self.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||||
@@ -231,6 +244,10 @@ class UIStateSP:
|
|||||||
self.params.remove("IntelligentCruiseButtonManagement")
|
self.params.remove("IntelligentCruiseButtonManagement")
|
||||||
self.has_icbm = False
|
self.has_icbm = False
|
||||||
|
|
||||||
|
# Extra boost needs sunnypilot longitudinal control; ICBM alone is not enough
|
||||||
|
if not has_long:
|
||||||
|
self.params.remove("FollowDistanceAccelBoost")
|
||||||
|
|
||||||
# Cruise features requiring longitudinal or ICBM
|
# Cruise features requiring longitudinal or ICBM
|
||||||
if not (has_long or self.has_icbm):
|
if not (has_long or self.has_icbm):
|
||||||
self.params.remove("CustomAccIncrementsEnabled")
|
self.params.remove("CustomAccIncrementsEnabled")
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
"""
|
||||||
|
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.filter_simple import FirstOrderFilter
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||||
|
|
||||||
|
LANE_WIDTH_DEFAULT = 3.6 # m
|
||||||
|
LANE_WIDTH_CLIP = (2.8, 4.5) # m
|
||||||
|
LOOKAHEAD_TIME = 1.5 # s
|
||||||
|
MIN_LOOKAHEAD = 10.0 # m
|
||||||
|
MIN_SPEED = 5.0 # m/s
|
||||||
|
LINE_STD = (0.2, 0.6)
|
||||||
|
POINT_STD = 0.1
|
||||||
|
YIELD = (0.2, 0.45)
|
||||||
|
X_IDXS = np.array(ModelConstants.X_IDXS)
|
||||||
|
|
||||||
|
|
||||||
|
class Blendful:
|
||||||
|
def __init__(self):
|
||||||
|
self.lane_width = FirstOrderFilter(LANE_WIDTH_DEFAULT, 10.0, DT_MDL, initialized=False)
|
||||||
|
self.line_gone = FirstOrderFilter(np.zeros(2), 2.0, DT_MDL)
|
||||||
|
self.line_probs_slow = FirstOrderFilter(np.zeros(2), 3.0, DT_MDL)
|
||||||
|
self.line_probs = FirstOrderFilter(np.zeros(2), 0.5, DT_MDL)
|
||||||
|
self.usual_offset = FirstOrderFilter(0.0, 10.0, DT_MDL, initialized=False)
|
||||||
|
self.going_around = False
|
||||||
|
self.since_turning = 0.0
|
||||||
|
self.trust = 0.0
|
||||||
|
self.line_trust = 0.0
|
||||||
|
self.dk = 0.0 # delta kappa; difference between lane center and model plan
|
||||||
|
self.l_prob = 0.0
|
||||||
|
self.r_prob = 0.0
|
||||||
|
self.model_curvature = 0.0
|
||||||
|
self.smooth = 0.0 # smooths the correction target below .53 lat accel
|
||||||
|
self.curvature = 0.0
|
||||||
|
|
||||||
|
def update(self, model_output: dict[str, np.ndarray], v_ego: float, active: bool, model_curvature: float, enabled: bool = True) -> float:
|
||||||
|
self.l_prob = self.r_prob = 0.0
|
||||||
|
self.model_curvature = model_curvature
|
||||||
|
if not enabled:
|
||||||
|
if self.curvature or self.trust:
|
||||||
|
self.__init__()
|
||||||
|
return 0.0
|
||||||
|
fading = not active or v_ego < MIN_SPEED
|
||||||
|
if fading:
|
||||||
|
self.trust = self.line_trust = 0.0
|
||||||
|
self.usual_offset.initialized = False
|
||||||
|
self.lane_width.initialized = False
|
||||||
|
self.going_around = False
|
||||||
|
target = 0.0 if fading else self._target(model_output, v_ego, model_curvature)
|
||||||
|
target = self.smooth = self.smooth + (target - self.smooth) * DT_MDL / (DT_MDL + np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.5, 0.0]))
|
||||||
|
build = 0.5 / max(v_ego, 1.0) ** 2 * DT_MDL # jerk over 1 second
|
||||||
|
release = (0.5 if fading else 2.0) / max(v_ego, 1.0) ** 2 * DT_MDL # lets go fast, but past zero it builds like any other pull
|
||||||
|
k = self.curvature
|
||||||
|
self.curvature = float(np.clip(target, max(k - release, -build) if k > 0 else k - build, min(k + release, build) if k < 0 else k + build))
|
||||||
|
return self.curvature
|
||||||
|
|
||||||
|
def _target(self, model_output: dict[str, np.ndarray], v_ego: float, model_curvature: float) -> float:
|
||||||
|
lookahead = max(v_ego * LOOKAHEAD_TIME, MIN_LOOKAHEAD)
|
||||||
|
lines = model_output['lane_lines'][0, :, :, 0]
|
||||||
|
stds = model_output['lane_lines_stds'][0, :, :, 0]
|
||||||
|
|
||||||
|
lone_weight = np.interp(abs(model_curvature) * v_ego ** 2, [0.18, 0.53], [0.5, 1.0]) # 0.5 on straights, 1.0 in curves
|
||||||
|
probs = self.line_probs.update(model_output['lane_lines_prob'][0, 1::2][1:3])
|
||||||
|
absent = (probs < 0.3) & (lone_weight > 0.5)
|
||||||
|
gone = self.line_gone.update((model_output['lane_lines_prob'][0, 1::2][1:3] < 0.3).astype(float))
|
||||||
|
missing = np.where(absent, 1.0, gone)
|
||||||
|
dropping = bool(np.any((probs < 0.6 * self.line_probs_slow.update(model_output['lane_lines_prob'][0, 1::2][1:3])) & ~absent))
|
||||||
|
l_prob = float(probs[0] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[1]), LINE_STD, [1.0, 0.0]))
|
||||||
|
r_prob = float(probs[1] * np.interp(np.interp(0.5 * lookahead, X_IDXS, stds[2]), LINE_STD, [1.0, 0.0]))
|
||||||
|
self.l_prob, self.r_prob = l_prob, r_prob
|
||||||
|
# one line steadily on its own still centers at lone_weight. a line that is disappearing gives control back to laneless
|
||||||
|
if missing[1] > 0.8 and l_prob > 0.5:
|
||||||
|
lane_trust = l_prob * lone_weight
|
||||||
|
elif missing[0] > 0.8 and r_prob > 0.5:
|
||||||
|
lane_trust = r_prob * lone_weight
|
||||||
|
elif dropping:
|
||||||
|
lane_trust = min(l_prob, r_prob)
|
||||||
|
else:
|
||||||
|
lane_trust = l_prob + r_prob - l_prob * r_prob
|
||||||
|
if lone_weight > 0.5 and lane_trust < self.line_trust:
|
||||||
|
lane_trust = self.line_trust + (lane_trust - self.line_trust) * DT_MDL / (lone_weight - 0.5 + DT_MDL)
|
||||||
|
self.line_trust = lane_trust
|
||||||
|
if lane_trust <= 0.0:
|
||||||
|
self.trust = 0.0
|
||||||
|
return 0.0
|
||||||
|
if l_prob + r_prob <= 0.0: # keep the last fit while trust lets go
|
||||||
|
self.trust = min(self.trust, lane_trust)
|
||||||
|
return float(self.trust * self.dk)
|
||||||
|
|
||||||
|
win = (X_IDXS >= 0.5 * lookahead) & (X_IDXS <= 1.5 * lookahead)
|
||||||
|
x = X_IDXS[win]
|
||||||
|
half_width = self.lane_width.x / 2
|
||||||
|
if l_prob * r_prob > 0.5:
|
||||||
|
width = np.interp(lookahead, X_IDXS, lines[2]) - np.interp(lookahead, X_IDXS, lines[1])
|
||||||
|
self.lane_width.update(float(np.clip(width, *LANE_WIDTH_CLIP)))
|
||||||
|
half_width = np.clip(lines[2, win] - lines[1, win], *LANE_WIDTH_CLIP) / 2
|
||||||
|
l_var = stds[1, win] ** 2 / max(l_prob, 1e-4)
|
||||||
|
r_var = stds[2, win] ** 2 / max(r_prob, 1e-4)
|
||||||
|
center = (r_var * (lines[1, win] + half_width) + l_var * (lines[2, win] - half_width)) / (l_var + r_var)
|
||||||
|
weight = 1.0 / (l_var * r_var / (l_var + r_var) + POINT_STD ** 2)
|
||||||
|
|
||||||
|
# Fit curvature to place the plant in center: e ~ dk x^2 / 2
|
||||||
|
plan_x = np.interp(x, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||||
|
plan_share = np.interp(abs(model_curvature) * lookahead ** 2 / 2, [0.5, 2.0], [0.3, 0.5]) # up to 50% plan y vs curvature parabola in sharp curves
|
||||||
|
error = np.clip(center - ((1 - plan_share) * model_curvature * x ** 2 / 2 + plan_share * plan_x), -1.0, 1.0)
|
||||||
|
dk = 2.0 * np.sum(weight * error * x ** 2) / np.sum(weight * x ** 4)
|
||||||
|
|
||||||
|
# A model planning to end up further from its usual spot in the lane than the plant is now is likely going around something
|
||||||
|
hw_now = np.clip(lines[2, 0] - lines[1, 0], *LANE_WIDTH_CLIP) / 2 if l_prob * r_prob > 0.5 else self.lane_width.x / 2
|
||||||
|
center_now = (l_prob * (lines[1, 0] + hw_now) + r_prob * (lines[2, 0] - hw_now)) / (l_prob + r_prob)
|
||||||
|
plan_y = np.interp(lookahead, model_output['plan'][0, :, 0], model_output['plan'][0, :, 1])
|
||||||
|
offset = plan_y - np.interp(lookahead, x, center)
|
||||||
|
if min(gone) > 0.5: # both lines gone: turns, intersections, relearn ideal model position
|
||||||
|
self.usual_offset.initialized = False
|
||||||
|
usual = self.usual_offset.x if self.usual_offset.initialized else offset
|
||||||
|
swerve = abs(offset - usual) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2
|
||||||
|
away = swerve - abs(-center_now - usual)
|
||||||
|
lane_curvature = 2.0 * np.polyfit(x, center, 2)[0]
|
||||||
|
inside_curve = abs(lane_curvature) * lookahead ** 2 / 2 > 0.1 and (offset - usual) * lane_curvature > 0
|
||||||
|
self.since_turning = 0.0 if abs(model_curvature) * lookahead ** 2 / 2 > 0.5 else self.since_turning + DT_MDL
|
||||||
|
if inside_curve:
|
||||||
|
self.going_around = False
|
||||||
|
elif away > YIELD[0] and self.since_turning > 2.0:
|
||||||
|
self.going_around = True
|
||||||
|
elif swerve < YIELD[0] / 2:
|
||||||
|
self.going_around = False
|
||||||
|
self.usual_offset.update_alpha(20.0 if self.going_around else 10.0)
|
||||||
|
self.usual_offset.update(offset)
|
||||||
|
|
||||||
|
if inside_curve:
|
||||||
|
lane_trust *= np.interp(abs(offset) - abs(center_now) - 0.25 * abs(model_curvature) * lookahead ** 2 / 2, (0.5, 1.0), [1.0, 0.0])
|
||||||
|
else:
|
||||||
|
lane_trust *= np.interp(swerve, (YIELD[0] / 2, YIELD[0]), [1.0, 0.0]) if self.going_around else np.interp(away, YIELD, [1.0, 0.0])
|
||||||
|
|
||||||
|
if lane_trust < self.trust:
|
||||||
|
dk = float(np.clip(dk, min(self.dk, 0.0), max(self.dk, 0.0)))
|
||||||
|
self.trust = lane_trust if lane_trust < self.trust else self.trust + (lane_trust - self.trust) * DT_MDL / (1.5 + DT_MDL)
|
||||||
|
self.dk = dk
|
||||||
|
return float(self.trust * dk)
|
||||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
import argparse
|
import argparse
|
||||||
import dataclasses
|
import dataclasses
|
||||||
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import mmap
|
import mmap
|
||||||
import re
|
import re
|
||||||
@@ -45,6 +46,11 @@ def _address(v):
|
|||||||
return _root(v.src[0]), add + start, size
|
return _root(v.src[0]), add + start, size
|
||||||
|
|
||||||
|
|
||||||
|
def _buffer(u):
|
||||||
|
from tinygrad.uop.ops import Ops
|
||||||
|
return u.buffer if u.op is Ops.BUFFER else getattr(u.arg, "buffer", None)
|
||||||
|
|
||||||
|
|
||||||
def _patched(buf, topo) -> tuple[bytearray, dict]:
|
def _patched(buf, topo) -> tuple[bytearray, dict]:
|
||||||
from tinygrad.uop.ops import Ops
|
from tinygrad.uop.ops import Ops
|
||||||
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
|
blob, rows = bytearray(buf.max_numel() * buf.dtype.itemsize), {}
|
||||||
@@ -72,7 +78,7 @@ def _evaluate(u, env):
|
|||||||
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
|
table, index = _root(u.src[0].src[0]), _evaluate(u.src[0].src[1], env)
|
||||||
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
|
return struct.unpack_from(u.dtype.fmt, table.arg, index * u.dtype.itemsize)[0] if table.op is Ops.BINARY and index is not None else None
|
||||||
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
|
if u.op in GroupOp.ALU and None not in (values := [_evaluate(x, env) for x in u.src]):
|
||||||
return exec_alu(u.op, u.dtype, values, False)
|
return exec_alu(u.op, u.dtype, values, *([False] if "truncate_output" in inspect.signature(exec_alu).parameters else []))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
|||||||
from tinygrad.uop.ops import Ops
|
from tinygrad.uop.ops import Ops
|
||||||
submit = jit.captured._linear.src[0].without_after
|
submit = jit.captured._linear.src[0].without_after
|
||||||
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
|
kernels, topo = submit.arg.aux.kernels, list(jit.captured._linear.toposort(enter_calls=True))
|
||||||
allocs = {str(u.tag): u for u in topo if u.op is Ops.ALLOC and u.tag is not None}
|
allocs = {str(name): u for u in topo if u.op is Ops.ALLOC and (name := getattr(u, "tag", None) or u.arg.name) is not None}
|
||||||
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
|
(commands, patched), kernargs = _patched(allocs["cmdbuf_compute_0"], topo), allocs["kernargs_compute_0"]
|
||||||
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
|
argument_words, pokes = _patched(kernargs, topo)[1], _input_pokes(submit, kernargs)
|
||||||
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
|
sh = {name: getattr(amd_gpu, f"reg{name}") + amd_gpu.GC_BASE__INST0_SEG0 - pm4.PACKET3_SET_SH_REG_START
|
||||||
@@ -158,7 +164,7 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
|||||||
args.append({"input": pokes[slot][0]})
|
args.append({"input": pokes[slot][0]})
|
||||||
elif slot in argument_words:
|
elif slot in argument_words:
|
||||||
base, at, nbytes = _address(argument_words[slot])
|
base, at, nbytes = _address(argument_words[slot])
|
||||||
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.arg.buffer.nbytes, "dtype": u8})
|
args.append({"buffer": buffers.setdefault(base, len(buffers)), "offset": at, "nbytes": nbytes or base.max_numel() * base.dtype.itemsize, "dtype": u8})
|
||||||
outs, ins = kernel[6]
|
outs, ins = kernel[6]
|
||||||
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
|
calls.append({"kind": "program", "name": re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]), "lib": programs[image_alloc], "target": dataclasses.asdict(target),
|
||||||
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
|
"signature": [[f"data{i}", i, u8, [a.get("nbytes", 1)]] for i, a in enumerate(args)], "global_size": grid, "local_size": local,
|
||||||
@@ -166,12 +172,13 @@ def convert(jit, kwargs: dict, frame: dict, target) -> tuple[dict, memoryview]:
|
|||||||
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
|
needs_init |= {args[i]["buffer"] for i in ins if "buffer" in args[i] and args[i]["buffer"] not in written}
|
||||||
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
|
written |= {args[i]["buffer"] for i in outs if "buffer" in args[i]}
|
||||||
|
|
||||||
|
assert all(_buffer(b) is not None for b, i in buffers.items() if i in needs_init), "a kernel reads a scratch before any kernel writes it"
|
||||||
names = list(jit.captured.expected_names)
|
names = list(jit.captured.expected_names)
|
||||||
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
|
inputs = [{"slot": slot, "name": names[slot] if slot < len(names) else None, "dtype": repr(u.dtype), "numel": int(u.max_numel()), "device": u.device}
|
||||||
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
|
for slot, u in enumerate(_prepare_jit_inputs((), kwargs)[0])]
|
||||||
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
|
header = {"input_names": names, "calls": calls, "returns": {"kind": "NoneType", "tensors": []}, "inputs": inputs,
|
||||||
"buffers": [{"nbytes": b.arg.buffer.nbytes, "device": b.arg.buffer.device, "dtype": u8}
|
"buffers": [{"nbytes": bb.nbytes if (bb := _buffer(b)) else b.max_numel() * b.dtype.itemsize, "device": bb.device if bb else b.device, "dtype": u8}
|
||||||
| ({"init": blob.add(b.arg.buffer.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
| ({"init": blob.add(bb.ensure_allocated().as_memoryview())} if i in needs_init else {}) for b, i in buffers.items()],
|
||||||
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
|
"check": {"inputs": [[path, store(array), device] for path, array, device in frame["inputs"]],
|
||||||
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
|
"outputs": [store(array) for array in frame["outputs"]], "output_container": frame["output_container"]}}
|
||||||
with sink:
|
with sink:
|
||||||
|
|||||||
@@ -250,6 +250,12 @@ def _allocate_buffers(header: dict, blob: memoryview, device_map: dict | None, s
|
|||||||
return buffers
|
return buffers
|
||||||
|
|
||||||
|
|
||||||
|
def _fields(what, /, **values): # a tinygrad dataclass built or replaced with the fields it has today: a dropped field is fine, a new required one is not
|
||||||
|
known = {field.name for field in dataclasses.fields(what)}
|
||||||
|
values = {name: value for name, value in values.items() if name in known}
|
||||||
|
return what(**values) if isinstance(what, type) else dataclasses.replace(what, **values)
|
||||||
|
|
||||||
|
|
||||||
def _program_uop(call: dict, lib: bytes):
|
def _program_uop(call: dict, lib: bytes):
|
||||||
from tinygrad.helpers import Target
|
from tinygrad.helpers import Target
|
||||||
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
|
from tinygrad.uop.ops import KernelInfo, Ops, ProgramInfo, UOp
|
||||||
@@ -257,9 +263,9 @@ def _program_uop(call: dict, lib: bytes):
|
|||||||
for name, compact_slot, dtype, shape in call["signature"]:
|
for name, compact_slot, dtype, shape in call["signature"]:
|
||||||
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
|
parameter = UOp.param(call["globals"][compact_slot], _dtype(dtype), math.prod(shape))
|
||||||
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
|
image = (shape[0], shape[1]) if len(shape) == 3 and shape[2] == 4 else None
|
||||||
parameters.append(parameter.replace(arg=dataclasses.replace(parameter.arg, name=name, image=image)))
|
parameters.append(parameter.replace(arg=_fields(parameter.arg, name=name, image=image)))
|
||||||
info = ProgramInfo(global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), vars=(), globals=tuple(call["globals"]),
|
info = _fields(ProgramInfo, global_size=tuple(call["global_size"]), local_size=tuple(call["local_size"]), globals=tuple(call["globals"]),
|
||||||
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=Target(**call["target"]))
|
outs=tuple(call["outs"]), ins=tuple(call["ins"]), target=_fields(Target, **call["target"]))
|
||||||
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
|
return UOp(Ops.PROGRAM, src=(UOp(Ops.SINK, arg=KernelInfo(name=call["name"])), UOp(Ops.LINEAR, src=tuple(parameters)),
|
||||||
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
|
UOp(Ops.SOURCE, arg=""), UOp(Ops.BINARY, arg=lib)), arg=info)
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
|
from functools import partial
|
||||||
import gc
|
import gc
|
||||||
import os
|
import os
|
||||||
os.environ['GMMU'] = '0'
|
os.environ['GMMU'] = '0'
|
||||||
@@ -39,6 +40,7 @@ from openpilot.selfdrive.modeld.modeld import ChestnutGpuState
|
|||||||
|
|
||||||
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
|
from openpilot.sunnypilot.modeld_v2.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState, get_curvature_from_output
|
||||||
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser
|
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser
|
||||||
|
from openpilot.sunnypilot.modeld_v2.blendful import Blendful
|
||||||
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan
|
||||||
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants
|
||||||
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
from openpilot.sunnypilot.modeld_v2.camera_offset_helper import CameraOffsetHelper
|
||||||
@@ -228,7 +230,8 @@ class ModelState(ModelStateBase):
|
|||||||
return outputs
|
return outputs
|
||||||
|
|
||||||
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
|
||||||
lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action:
|
lat_action_t: float, long_action_t: float, v_ego: float,
|
||||||
|
blendful: Callable[[float], float] | None = None) -> log.ModelDataV2.Action:
|
||||||
if 'action' not in model_output:
|
if 'action' not in model_output:
|
||||||
plan = model_output['plan'][0]
|
plan = model_output['plan'][0]
|
||||||
desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
desired_accel = get_accel_from_plan(plan[:, Plan.VELOCITY][:, 0], plan[:, Plan.ACCELERATION][:, 0], self.constants.T_IDXS,
|
||||||
@@ -240,6 +243,8 @@ class ModelState(ModelStateBase):
|
|||||||
else:
|
else:
|
||||||
desired_accel = model_output['action'][0, 1]
|
desired_accel = model_output['action'][0, 1]
|
||||||
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
desired_curvature = model_output['action'][0, 0] / (max(1.0, v_ego))**2
|
||||||
|
if blendful is not None:
|
||||||
|
desired_curvature += blendful(desired_curvature)
|
||||||
|
|
||||||
stop = v_ego < 0.3 and desired_accel < 0.1
|
stop = v_ego < 0.3 and desired_accel < 0.1
|
||||||
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS)
|
||||||
@@ -366,6 +371,8 @@ def main(demo=False):
|
|||||||
DH = DesireHelper()
|
DH = DesireHelper()
|
||||||
meta_constants = load_meta_constants()
|
meta_constants = load_meta_constants()
|
||||||
RELC = RoadEdgeLaneChangeController()
|
RELC = RoadEdgeLaneChangeController()
|
||||||
|
blendful = Blendful()
|
||||||
|
blendful_enabled = False
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
# Keep receiving frames until we are at least 1 frame ahead of previous extra frame
|
||||||
@@ -409,6 +416,7 @@ def main(demo=False):
|
|||||||
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
|
model.lat_delay = get_lat_delay(params, sm["lateralDelay"].lateralDelay)
|
||||||
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
model.PLANPLUS_CONTROL = params.get("PlanplusControl", return_default=True)
|
||||||
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
camera_offset_helper.set_offset(params.get("CameraOffset", return_default=True))
|
||||||
|
blendful_enabled = params.get_bool("Blendful")
|
||||||
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS
|
||||||
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
|
if sm.updated["extrinsicsCalibration"] and sm.seen['narrowRoadCameraState'] and sm.seen['deviceState']:
|
||||||
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
|
device_from_calib_euler = np.array(sm["extrinsicsCalibration"].rpyCalib, dtype=np.float32)
|
||||||
@@ -482,7 +490,9 @@ def main(demo=False):
|
|||||||
|
|
||||||
if not sm['carControl'].latActive:
|
if not sm['carControl'].latActive:
|
||||||
prev_action.desiredCurvature = sm['controlsState'].curvature
|
prev_action.desiredCurvature = sm['controlsState'].curvature
|
||||||
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego)
|
blendful_active = desire == log.Desire.none and DH.lane_change_state == log.LaneChangeState.off
|
||||||
|
action = model.get_action_from_model(model_output, prev_action, lat_action_t, long_action_t, v_ego,
|
||||||
|
partial(blendful.update, model_output, v_ego, blendful_active, enabled=blendful_enabled))
|
||||||
prev_action = action
|
prev_action = action
|
||||||
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
fill_model_msg(drivingdata_send, modelv2_send, model_output, action,
|
||||||
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id,
|
||||||
@@ -499,6 +509,13 @@ def main(demo=False):
|
|||||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
mdv2sp_send.valid = modelv2_send.valid
|
mdv2sp_send.valid = modelv2_send.valid
|
||||||
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulCurvature = blendful.curvature
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulTrust = float(blendful.trust)
|
||||||
|
mdv2sp_send.modelDataV2SP.modelCurvature = float(blendful.model_curvature)
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulGoingAround = bool(blendful.going_around)
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulLeftProb = blendful.l_prob
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulRightProb = blendful.r_prob
|
||||||
|
mdv2sp_send.modelDataV2SP.blendfulLaneWidth = float(blendful.lane_width.x)
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state
|
||||||
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ def record_kernel_linear():
|
|||||||
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
|
recorded, hcq_compile, compile_linear = [], realize.hcq_compile, jit.compile_linear
|
||||||
|
|
||||||
def recording_compile_linear(*args, **kwargs):
|
def recording_compile_linear(*args, **kwargs):
|
||||||
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
with mock.patch.object(realize, "hcq_compile", lambda linear, *a, **k: (linear.src and recorded.append(linear), hcq_compile(linear, *a, **k))[1]):
|
||||||
return compile_linear(*args, **kwargs)
|
return compile_linear(*args, **kwargs)
|
||||||
|
|
||||||
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
|
with mock.patch.object(jit, "compile_linear", recording_compile_linear):
|
||||||
@@ -38,6 +38,18 @@ def capture_frame(jit, kwargs: dict, output_container: str | None = None):
|
|||||||
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
|
return {"inputs": inputs, "outputs": [tensor.numpy().copy() for tensor in outputs], "output_container": output_container}, result
|
||||||
|
|
||||||
|
|
||||||
|
def kernel_signature(ast, elf) -> list:
|
||||||
|
from tinygrad.dtype import AddrSpace
|
||||||
|
if hasattr(ast, "kernel_params"):
|
||||||
|
if any(p.arg.addrspace is AddrSpace.ALU for p in ast.kernel_params):
|
||||||
|
raise ValueError(f"{elf.name} takes a variable, which plain artifacts don't support")
|
||||||
|
compact = {slot: i for i, slot in enumerate(ast.arg.globals)}
|
||||||
|
return [[p.arg.name, compact[p.arg.slot], repr(p.dtype), [int(x) for x in p._shape]] for p in ast.kernel_params]
|
||||||
|
if not all(isinstance(slot, int) for _, slot, _, _ in elf.signature):
|
||||||
|
raise ValueError("TinyELF.signature lost its slots and this tinygrad has no UOp.kernel_params: update kernel_signature")
|
||||||
|
return [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature]
|
||||||
|
|
||||||
|
|
||||||
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
|
def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict, memoryview]:
|
||||||
from tinygrad.uop.ops import Ops
|
from tinygrad.uop.ops import Ops
|
||||||
sink = tempfile.TemporaryFile(dir=".")
|
sink = tempfile.TemporaryFile(dir=".")
|
||||||
@@ -51,6 +63,12 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
|||||||
if uop.op is Ops.PARAM:
|
if uop.op is Ops.PARAM:
|
||||||
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
|
input_specs.setdefault(uop.arg.slot, {"dtype": repr(uop.dtype), "numel": int(uop.max_numel()), "device": uop.device})
|
||||||
return {"input": uop.arg.slot}
|
return {"input": uop.arg.slot}
|
||||||
|
root, offset = uop, 0
|
||||||
|
while root.op in (Ops.AFTER, Ops.BITCAST, Ops.SHRINK):
|
||||||
|
offset, root = offset + (root.src[1].arg * root.src[0].dtype.itemsize if root.op is Ops.SHRINK else 0), root.src[0]
|
||||||
|
if root.op is Ops.ALLOC:
|
||||||
|
base = bases.setdefault(id(root), (len(bases), root, root.dtype))
|
||||||
|
return {"buffer": base[0], "offset": offset, "nbytes": int(uop.max_numel()) * uop.dtype.itemsize, "dtype": repr(uop.dtype)}
|
||||||
buffer = uop.buffer
|
buffer = uop.buffer
|
||||||
root = buffer._base if buffer._base is not None else buffer
|
root = buffer._base if buffer._base is not None else buffer
|
||||||
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
|
base = bases.setdefault(id(root), (len(bases), root, uop.base.dtype if buffer._base is None else uop.dtype))
|
||||||
@@ -63,7 +81,7 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
|||||||
elf, info = ast.to_elf(), ast.arg
|
elf, info = ast.to_elf(), ast.arg
|
||||||
global_size, local_size = info.launch_dims({})
|
global_size, local_size = info.launch_dims({})
|
||||||
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
|
calls.append({"kind": "program", "name": elf.name, "lib": blob.add(bytes(elf.lib)), "target": dataclasses.asdict(elf.target),
|
||||||
"signature": [[name, slot, repr(dtype), [int(x) for x in shape]] for name, slot, dtype, shape in elf.signature],
|
"signature": kernel_signature(ast, elf),
|
||||||
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
|
"global_size": [int(x) for x in global_size], "local_size": [int(x) for x in local_size],
|
||||||
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
|
"globals": list(info.globals), "outs": list(info.outs), "ins": list(info.ins), "args": arguments})
|
||||||
outs, ins = info.outs, info.ins
|
outs, ins = info.outs, info.ins
|
||||||
@@ -77,7 +95,9 @@ def serialize_jit(jit, kernel_linear, check: dict | None = None) -> tuple[dict,
|
|||||||
|
|
||||||
ret = jit.captured.ret
|
ret = jit.captured.ret
|
||||||
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
|
returns = {"kind": type(ret).__name__, "tensors": [describe(t.uop.base) | {"shape": [int(x) for x in t.shape]} for t in flatten(ret)]}
|
||||||
buffers = [{"nbytes": base.nbytes, "device": base.device, "dtype": repr(dtype)}
|
from tinygrad.device import Buffer
|
||||||
|
assert not [base for index, base, _ in bases.values() if index in needs_init and not isinstance(base, Buffer)], "a kernel reads a scratch before writing it"
|
||||||
|
buffers = [{"nbytes": base.nbytes if isinstance(base, Buffer) else int(base.max_numel()) * dtype.itemsize, "device": base.device, "dtype": repr(dtype)}
|
||||||
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
|
| ({"init": blob.add(base.as_memoryview())} if index in needs_init else {}) for index, base, dtype in bases.values()]
|
||||||
names = list(jit.captured.expected_names)
|
names = list(jit.captured.expected_names)
|
||||||
if check is not None:
|
if check is not None:
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
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.realtime import DT_MDL
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
from openpilot.sunnypilot.modeld_v2.blendful import Blendful
|
||||||
|
from openpilot.sunnypilot.modeld_v2.constants import ModelConstants
|
||||||
|
|
||||||
|
X = np.array(ModelConstants.X_IDXS)
|
||||||
|
BUILD, RELEASE = 0.5, 2.0
|
||||||
|
|
||||||
|
|
||||||
|
def frame(center=0.0, plan=0.0, k=0.0, probs=(0.9, 0.9), stds=(0.1, 0.1), width=3.5):
|
||||||
|
road = k * X ** 2 / 2
|
||||||
|
lines = np.zeros((1, 4, 33, 2))
|
||||||
|
lane_stds = np.ones((1, 4, 33, 2))
|
||||||
|
for i, off in enumerate((-1.5 * width, -width / 2, width / 2, 1.5 * width)):
|
||||||
|
lines[0, i, :, 0] = road + center + off
|
||||||
|
lane_stds[0, 1, :, 0], lane_stds[0, 2, :, 0] = stds
|
||||||
|
lane_probs = np.zeros((1, 8))
|
||||||
|
lane_probs[0, 3], lane_probs[0, 5] = probs
|
||||||
|
path = np.zeros((1, 33, 3))
|
||||||
|
path[0, :, 0] = X
|
||||||
|
path[0, :, 1] = road + plan * np.clip(X / 10, 0, 1)
|
||||||
|
return {'lane_lines': lines, 'lane_lines_stds': lane_stds, 'lane_lines_prob': lane_probs, 'plan': path}
|
||||||
|
|
||||||
|
|
||||||
|
def drive(bf, seconds, v=20.0, k=0.0, **kw):
|
||||||
|
out = [bf.update(frame(k=k, **kw), v, True, k) for _ in range(int(seconds / DT_MDL))]
|
||||||
|
return np.array(out) * v ** 2 # m/s^2
|
||||||
|
|
||||||
|
|
||||||
|
class TestBlendful(OpenpilotTestCase):
|
||||||
|
def setup_method(self):
|
||||||
|
self.bf = Blendful()
|
||||||
|
|
||||||
|
def test_centered_adds_nothing(self):
|
||||||
|
push = drive(self.bf, 5.0)
|
||||||
|
assert self.bf.trust > 0.9
|
||||||
|
assert np.abs(push).max() < 1e-3
|
||||||
|
|
||||||
|
def test_pulls_toward_lane_center(self):
|
||||||
|
push = drive(self.bf, 5.0, center=0.5)
|
||||||
|
assert push[-1] > 0.02 # + y side, where the lane center is
|
||||||
|
assert self.bf.trust > 0.9
|
||||||
|
assert np.all(np.diff(push) <= BUILD * DT_MDL + 1e-9)
|
||||||
|
|
||||||
|
def test_flipping_sides_builds_slowly(self):
|
||||||
|
k = 0.002 # 0.8 m/s^2 at 20 m/s
|
||||||
|
drive(self.bf, 5.0, k=k, center=0.5)
|
||||||
|
push = drive(self.bf, 5.0, k=k, center=-0.5)
|
||||||
|
rate = np.diff(push) / DT_MDL
|
||||||
|
assert push[-1] < 0
|
||||||
|
assert np.all(rate >= -RELEASE - 1e-6)
|
||||||
|
assert np.all(rate[push[:-1] <= 0] >= -BUILD - 1e-6) # once past zero it builds
|
||||||
|
crossing = push[1:][(push[:-1] > 0) & (push[1:] <= 0)]
|
||||||
|
assert len(crossing) and np.all(crossing >= -BUILD * DT_MDL - 1e-9)
|
||||||
|
|
||||||
|
def test_weak_lines_drive_laneless(self):
|
||||||
|
clear = drive(Blendful(), 5.0, center=0.5)[-1]
|
||||||
|
faint = Blendful()
|
||||||
|
push = drive(faint, 5.0, center=0.5, probs=(0.1, 0.1))
|
||||||
|
assert faint.trust < 0.25 and abs(push[-1]) < 0.25 * clear # faint lines = trust and push scale down
|
||||||
|
blurry = Blendful()
|
||||||
|
push = drive(blurry, 5.0, center=0.5, stds=(0.8, 0.8))
|
||||||
|
assert blurry.trust == 0.0 and np.abs(push).max() < 1e-6 # lines too uncertain = laneless
|
||||||
|
|
||||||
|
def test_lone_line_holds_through_a_blink_in_a_curve(self):
|
||||||
|
k, v = 1 / 20, 6.0
|
||||||
|
drive(self.bf, 6.0, v=v, k=k, plan=0.4, probs=(0.9, 0.05))
|
||||||
|
before = self.bf.trust
|
||||||
|
drive(self.bf, 0.25, v=v, k=k, plan=0.4, probs=(0.1, 0.05), stds=(1.0, 0.1))
|
||||||
|
bottom = self.bf.trust
|
||||||
|
drive(self.bf, 2.0, v=v, k=k, plan=0.4, probs=(0.9, 0.05))
|
||||||
|
assert before > 0.5
|
||||||
|
assert bottom > 0.5 * before # keeps latched through flicker
|
||||||
|
assert self.bf.trust > 0.85 * before
|
||||||
|
|
||||||
|
def test_yields_when_the_model_goes_around(self):
|
||||||
|
drive(self.bf, 6.0)
|
||||||
|
push = drive(self.bf, 3.0, plan=0.8)
|
||||||
|
assert self.bf.going_around
|
||||||
|
assert self.bf.trust < 0.05
|
||||||
|
assert abs(push[-1]) < 0.01
|
||||||
|
|
||||||
|
def test_fit_does_not_grow_while_trust_falls(self):
|
||||||
|
drive(self.bf, 5.0, center=0.3)
|
||||||
|
for i in range(40):
|
||||||
|
dk, trust = self.bf.dk, self.bf.trust
|
||||||
|
p = 0.9 - 0.02 * i
|
||||||
|
self.bf.update(frame(center=0.3, plan=-0.02 * i, probs=(p, p)), 20.0, True, 0.0)
|
||||||
|
if self.bf.trust < trust:
|
||||||
|
assert abs(self.bf.dk) <= abs(dk) + 1e-12
|
||||||
|
|
||||||
|
def test_toggle_off_returns_zero_and_resets(self):
|
||||||
|
drive(self.bf, 5.0, center=0.5)
|
||||||
|
assert self.bf.curvature != 0.0
|
||||||
|
assert self.bf.update(frame(center=0.5), 20.0, True, 0.0, enabled=False) == 0.0
|
||||||
|
assert self.bf.curvature == 0.0 and self.bf.trust == 0.0
|
||||||
|
|
||||||
|
def test_fades_below_min_speed(self):
|
||||||
|
drive(self.bf, 5.0, v=10.0, center=0.5)
|
||||||
|
start = self.bf.curvature
|
||||||
|
out = np.array([self.bf.update(frame(center=0.5), 4.0, True, 0.0) for _ in range(100)])
|
||||||
|
assert self.bf.trust == 0.0
|
||||||
|
assert abs(out[0]) < abs(start)
|
||||||
|
assert abs(out[-1]) < 1e-6
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"""
|
||||||
|
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 re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest import mock
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
os.environ.setdefault("CHECK_OOB", "0")
|
||||||
|
os.environ.setdefault("GMMU", "0")
|
||||||
|
if sys.platform == "darwin" and os.environ.get("DEV", "").startswith("MOCKUSB"): # the mock's VRAM is a memfd, which macOS lacks: an unlinked file is the same
|
||||||
|
from tinygrad.runtime.autogen import libc
|
||||||
|
|
||||||
|
def _memfd_create(name, flags):
|
||||||
|
fd, path = tempfile.mkstemp()
|
||||||
|
os.unlink(path)
|
||||||
|
return fd
|
||||||
|
libc.memfd_create = _memfd_create
|
||||||
|
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
from openpilot.sunnypilot.modeld_v2 import convert_upstream_pkl
|
||||||
|
from openpilot.sunnypilot.modeld_v2.convert_upstream_pkl import convert
|
||||||
|
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, load_model
|
||||||
|
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame
|
||||||
|
|
||||||
|
IMG, DESIRE, STATE, OUT = (6, 8, 8), (8,), (4, 16), (1, 32)
|
||||||
|
|
||||||
|
|
||||||
|
class TestConvertUpstreamPkl(OpenpilotTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
if not os.environ.get("TINYGRAD_TESTS"):
|
||||||
|
raise unittest.SkipTest("TINYGRAD_TESTS not set. skipping converter test")
|
||||||
|
from tinygrad import Device, Tensor
|
||||||
|
assert Device.DEFAULT == "AMD" and getattr(Device["AMD"], "is_usb", False), f"{Device.DEFAULT} is not an AMD device behind the USB bridge"
|
||||||
|
assert (Tensor([1, 2, 3]) * 2).tolist() == [2, 4, 6]
|
||||||
|
cls.tmp = tempfile.TemporaryDirectory()
|
||||||
|
cls.rng = np.random.default_rng(0)
|
||||||
|
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
|
||||||
|
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
|
||||||
|
cls.input_specs = {"input_imgs": (IMG, "uint8", Device.DEFAULT), "desire": (DESIRE, "float32", Device.DEFAULT),
|
||||||
|
"features": (STATE, "float32", Device.DEFAULT)}
|
||||||
|
cls.output_specs = {"outputs": (OUT, "float32", Device.DEFAULT), "next_features": (STATE, "float32", Device.DEFAULT)}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.tmp.cleanup()
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def model(self, input_imgs, desire, features, output_buffers):
|
||||||
|
from tinygrad import Tensor
|
||||||
|
feat = features.mean(axis=0).reshape(1, 16) + input_imgs.float().mean() + desire.sum()
|
||||||
|
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
|
||||||
|
output_buffers["next_features"].assign(features[1:].cat(feat * 2)))
|
||||||
|
|
||||||
|
def inputs(self, seed: int) -> dict:
|
||||||
|
from examples.openpilot.helpers import allocate_inputs
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
|
||||||
|
def initialize(arrays: dict):
|
||||||
|
for value in arrays.values():
|
||||||
|
value[...] = rng.standard_normal(value.shape) if value.dtype.kind == "f" else rng.integers(0, 256, value.shape)
|
||||||
|
return allocate_inputs(self.input_specs, initialize) | {"output_buffers": allocate_inputs(self.output_specs)}
|
||||||
|
|
||||||
|
def jit(self):
|
||||||
|
from tinygrad import TinyJit
|
||||||
|
run = TinyJit(self.model, prune=True)
|
||||||
|
kwargs = self.inputs(1)
|
||||||
|
run(**kwargs)
|
||||||
|
run(**kwargs)
|
||||||
|
return run
|
||||||
|
|
||||||
|
def assert_matches_eager(self, plain, seed: int):
|
||||||
|
fresh, expected = self.inputs(seed), self.inputs(seed)
|
||||||
|
plain(**fresh)
|
||||||
|
self.model(**expected)
|
||||||
|
for name in self.output_specs:
|
||||||
|
assert np.array_equal(fresh["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
|
||||||
|
|
||||||
|
def convert(self, jit) -> tuple[dict, memoryview]:
|
||||||
|
from tinygrad import Device
|
||||||
|
kwargs = self.inputs(2)
|
||||||
|
frame, _ = capture_frame(jit, kwargs, "output_buffers")
|
||||||
|
header, blob = convert(jit, kwargs, frame, Device[Device.DEFAULT].renderer.target)
|
||||||
|
kernels = jit.captured._linear.src[0].without_after.arg.aux.kernels
|
||||||
|
assert [call["name"] for call in header["calls"]] == [re.sub(r"\x1b\[[0-9;]*m", "", kernel[1]) for kernel in kernels]
|
||||||
|
assert all(re.fullmatch(r"\w+", call["name"]) for call in header["calls"]), "kernel names: failure point 1"
|
||||||
|
assert [(tuple(call["outs"]), tuple(call["ins"])) for call in header["calls"]] == [tuple(kernel[6]) for kernel in kernels], "outs/ins: failure point 1"
|
||||||
|
bound = {argument["input"] for call in header["calls"] for argument in call["args"] if "input" in argument}
|
||||||
|
assert bound == {spec["slot"] for spec in header["inputs"]}, f"input slots bound by pokes {sorted(bound)}: failure point 4"
|
||||||
|
assert [spec["name"] for spec in header["inputs"]][:len(self.input_specs)] == sorted(self.input_specs)
|
||||||
|
return header, blob
|
||||||
|
|
||||||
|
def test_converted_jit_matches_eager(self): # the submit decoded into plain calls, replayed against its own frame
|
||||||
|
jit = self.jit()
|
||||||
|
plain = PlainJit(*self.convert(jit))
|
||||||
|
self.assert_matches_eager(plain, 3)
|
||||||
|
self.assert_matches_eager(plain, 4)
|
||||||
|
|
||||||
|
def test_pickle_round_trip_through_main(self):
|
||||||
|
from examples.openpilot.helpers import dump_pickle, load_pickle
|
||||||
|
from tinygrad.engine.realize import lower_and_compile
|
||||||
|
pkl, out = os.path.join(self.tmp.name, "tiny_tinygrad.pkl"), os.path.join(self.tmp.name, "tiny_plain.pkl")
|
||||||
|
dump_pickle({"run": self.jit(), "input_specs": self.input_specs, "output_specs": self.output_specs, "metadata": {"name": "tiny"}}, pkl)
|
||||||
|
|
||||||
|
artifact = load_pickle(pkl, out_of_band=True)
|
||||||
|
artifact["run"].captured._linear = lower_and_compile(artifact["run"].captured._linear)
|
||||||
|
self.assert_matches_eager(PlainJit(*self.convert(artifact["run"])), 5)
|
||||||
|
|
||||||
|
with mock.patch.object(sys, "argv", ["convert_upstream_pkl", pkl, out]):
|
||||||
|
convert_upstream_pkl.main()
|
||||||
|
bundle = load_model(out, cache=False, verify=True)
|
||||||
|
assert bundle["metadata"] == {"name": "tiny"} and bundle["input_specs"] == self.input_specs and bundle["output_specs"] == self.output_specs
|
||||||
|
self.assert_matches_eager(bundle["run"], 6)
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
"""
|
||||||
|
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 dataclasses
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from unittest import mock
|
||||||
|
import numpy as np
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import openpilot.sunnypilot.modeld_v2.helpers as helpers
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
from openpilot.sunnypilot.modeld_v2 import compile_warp
|
||||||
|
from openpilot.sunnypilot.modeld_v2.frame_fusion import frame_part, pack_layout
|
||||||
|
from openpilot.sunnypilot.modeld_v2.helpers import PlainJit, cache_is_fresh, load_model, read_parts, write_bundle
|
||||||
|
from openpilot.sunnypilot.modeld_v2.plain_artifact_compiler import capture_frame, record_kernel_linear, serialize_jit
|
||||||
|
|
||||||
|
CAM_W, CAM_H, MODEL_W, MODEL_H = 32, 16, 16, 8
|
||||||
|
FRAME = f"{CAM_W},{CAM_H},{CAM_W},{CAM_H},{CAM_H // 2},{CAM_W * CAM_H * 3 // 2}"
|
||||||
|
FRAME_SIZE = CAM_W * CAM_H * 3 // 2
|
||||||
|
HOST = {"desire": (8,), "traffic_convention": (2,)}
|
||||||
|
STATE, OUT = (4, 16), (1, 32)
|
||||||
|
|
||||||
|
|
||||||
|
def compile_jit(fn, inputs: dict, output_container: str | None = None) -> tuple[dict, memoryview]:
|
||||||
|
from tinygrad import TinyJit
|
||||||
|
run = TinyJit(fn, prune=True)
|
||||||
|
with record_kernel_linear() as recorded:
|
||||||
|
run(**inputs)
|
||||||
|
run(**inputs)
|
||||||
|
check, _ = capture_frame(run, inputs, output_container)
|
||||||
|
assert len(recorded) == 1, f"expected one captured kernel list, got {len(recorded)}"
|
||||||
|
return serialize_jit(run, recorded[0], check)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPlainArtifact(OpenpilotTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
if not os.environ.get("TINYGRAD_TESTS"):
|
||||||
|
raise unittest.SkipTest("TINYGRAD_TESTS env var not set, skipping artifact test.")
|
||||||
|
from tinygrad import Tensor
|
||||||
|
cls.tmp = tempfile.TemporaryDirectory()
|
||||||
|
cls.rng = np.random.default_rng(0)
|
||||||
|
cls.weights = Tensor(cls.rng.standard_normal((16, 32)).astype(np.float32)).realize()
|
||||||
|
cls.bias = Tensor(cls.rng.standard_normal((1, 32)).astype(np.float32)).realize()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
cls.tmp.cleanup()
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
|
def model(self, x, h): # a two input, one weight model: the shape of a policy without the size
|
||||||
|
return ((x @ self.weights + self.bias).relu() + h.mean()).contiguous()
|
||||||
|
|
||||||
|
def tensors(self, seed: int):
|
||||||
|
from tinygrad import Tensor
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
return {"x": Tensor(rng.standard_normal((1, 16)).astype(np.float32)).realize(), "h": Tensor(rng.standard_normal((4, 16)).astype(np.float32)).realize()}
|
||||||
|
|
||||||
|
def artifact(self, name: str = "model") -> str:
|
||||||
|
path = os.path.join(self.tmp.name, f"{name}_tinygrad.pkl")
|
||||||
|
if not os.path.exists(path):
|
||||||
|
write_bundle(path, {"run": compile_jit(self.model, self.tensors(1))}, {"metadata": {"name": name}})
|
||||||
|
return path
|
||||||
|
|
||||||
|
def test_round_trip_matches_jit(self): # compiler -> bundle -> PlainJit, replay verified, then fresh inputs bit exact with eager tinygrad
|
||||||
|
bundle = load_model(self.artifact(), cache=False, verify=True)
|
||||||
|
assert bundle["metadata"] == {"name": "model"} and isinstance(bundle["run"], PlainJit)
|
||||||
|
fresh = self.tensors(2)
|
||||||
|
assert np.array_equal(bundle["run"](**fresh).numpy(), self.model(**fresh).numpy())
|
||||||
|
|
||||||
|
def test_replay_check_catches_a_changed_kernel(self):
|
||||||
|
parts, extras = read_parts(self.artifact())
|
||||||
|
header, blob = parts["run"]
|
||||||
|
poisoned = dict(header, check=dict(header["check"], outputs=[header["check"]["inputs"][0][1]]))
|
||||||
|
with self.assertRaises(helpers.ArtifactMismatch):
|
||||||
|
PlainJit(poisoned, blob)
|
||||||
|
|
||||||
|
def test_cache_round_trip_holds_no_weights_and_is_rebuilt_when_stale(self):
|
||||||
|
path = self.artifact("cached")
|
||||||
|
expected = load_model(path, cache=False, verify=False)["run"](**self.tensors(3)).numpy()
|
||||||
|
load_model(path)
|
||||||
|
assert cache_is_fresh(path)
|
||||||
|
with open(f"{path}.cache", "rb") as file:
|
||||||
|
cache = file.read()
|
||||||
|
assert self.weights.numpy().tobytes() not in cache and len(cache) < os.path.getsize(path)
|
||||||
|
with mock.patch.object(helpers, "PlainJit", side_effect=AssertionError("a fresh cache must not rebuild")):
|
||||||
|
cached = load_model(path)
|
||||||
|
assert np.array_equal(cached["run"](**self.tensors(3)).numpy(), expected)
|
||||||
|
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
|
||||||
|
assert not cache_is_fresh(path)
|
||||||
|
assert isinstance(load_model(path)["run"], PlainJit)
|
||||||
|
assert cache_is_fresh(path)
|
||||||
|
assert not cache_is_fresh(path) # keyed on the tinygrad that wrote it, so the next boot after a bump rebuilds once
|
||||||
|
os.utime(path)
|
||||||
|
with mock.patch.object(helpers, "tinygrad_identity", return_value="another tinygrad"):
|
||||||
|
assert not cache_is_fresh(path)
|
||||||
|
|
||||||
|
def test_fused_with_warp_matches_warp_then_model(self):
|
||||||
|
from tinygrad import Tensor
|
||||||
|
warp_path = os.path.join(self.tmp.name, "warp_tinygrad.pkl")
|
||||||
|
with mock.patch.object(sys, "argv", ["compile_warp", "--frame", FRAME, "--warp-to", f"{MODEL_W}x{MODEL_H}", "--output", warp_path]):
|
||||||
|
compile_warp.main()
|
||||||
|
warp = load_model(warp_path, cache=False)["run"]
|
||||||
|
|
||||||
|
def native(new_img, desire, traffic_convention, state_feat_q, output_buffers):
|
||||||
|
feat = state_feat_q.mean(axis=0).reshape(1, 16) + new_img.float().mean() + desire.sum() + traffic_convention[0]
|
||||||
|
Tensor.realize(output_buffers["outputs"].assign((feat @ self.weights + self.bias).relu()),
|
||||||
|
output_buffers["next_state_feat_q"].assign(state_feat_q[1:].cat(feat * 2)))
|
||||||
|
|
||||||
|
def model_inputs(new_img):
|
||||||
|
return {"new_img": new_img} | {name: Tensor(np.ones(shape, np.float32) * 0.5).realize() for name, shape in HOST.items()} | \
|
||||||
|
{"state_feat_q": Tensor(self.rng.standard_normal(STATE).astype(np.float32)).realize(),
|
||||||
|
"output_buffers": {"outputs": Tensor.zeros(OUT).contiguous().realize(), "next_state_feat_q": Tensor.zeros(STATE).contiguous().realize()}}
|
||||||
|
frames = Tensor(self.rng.integers(0, 256, (2, FRAME_SIZE), dtype=np.uint8)).realize()
|
||||||
|
tfm = Tensor(np.tile(np.eye(3, dtype=np.float32), (2, 1, 1))).realize()
|
||||||
|
path = os.path.join(self.tmp.name, "native_tinygrad.pkl")
|
||||||
|
part = compile_jit(native, model_inputs(warp(input_frame=frames, M_inv=tfm).realize()), "output_buffers")
|
||||||
|
extras = {"input_specs": {"new_img": ((2, 6, MODEL_H // 2, MODEL_W // 2), "|u1", "CPU"), "state_feat_q": (STATE, "<f4", "CPU")}
|
||||||
|
| {name: (shape, "<f4", "CPU") for name, shape in HOST.items()},
|
||||||
|
"output_specs": {"outputs": (OUT, "<f4", "CPU"), "next_state_feat_q": (STATE, "<f4", "CPU")},
|
||||||
|
"metadata": {"output_shapes": {"outputs": OUT, "next_state_feat_q": STATE}}}
|
||||||
|
write_bundle(path, {"run": part}, extras)
|
||||||
|
|
||||||
|
fused = load_model(path, cache=False, extend=functools.partial(frame_part, warp_path))
|
||||||
|
assert fused["run"].input_names == ["host", "state_feat_q"]
|
||||||
|
offsets, packed = pack_layout(HOST, FRAME_SIZE)
|
||||||
|
host = np.zeros(packed, np.uint8)
|
||||||
|
inputs = model_inputs(None)
|
||||||
|
for name, shape in ({"tfm": (2, 3, 3)} | HOST).items():
|
||||||
|
np.ndarray(shape, np.float32, host, offsets[name])[...] = tfm.numpy() if name == "tfm" else inputs[name].numpy()
|
||||||
|
host[offsets["frames"]:] = frames.numpy().ravel()
|
||||||
|
fused["run"](host=Tensor(host, device="NPY"), state_feat_q=inputs["state_feat_q"], output_buffers=inputs["output_buffers"])
|
||||||
|
|
||||||
|
expected = model_inputs(warp(input_frame=frames, M_inv=tfm).realize()) | {k: inputs[k] for k in ("state_feat_q",)}
|
||||||
|
native(**expected)
|
||||||
|
for name in ("outputs", "next_state_feat_q"):
|
||||||
|
assert np.array_equal(inputs["output_buffers"][name].numpy(), expected["output_buffers"][name].numpy()), name
|
||||||
|
|
||||||
|
def test_tinygrad_api_the_loader_reads(self):
|
||||||
|
import tinygrad.engine.jit as jit
|
||||||
|
from tinygrad.device import Buffer, TinyELF
|
||||||
|
from tinygrad.helpers import Target
|
||||||
|
from tinygrad.uop.ops import ParamArg, ProgramInfo, UOp
|
||||||
|
fields = {cls: {f.name for f in dataclasses.fields(cls)} for cls in (ProgramInfo, ParamArg, Target)}
|
||||||
|
assert fields[ProgramInfo] >= {"global_size", "local_size", "globals", "outs", "ins", "target"}, "ProgramInfo changed: update helpers._program_uop"
|
||||||
|
assert fields[ParamArg] >= {"slot", "dtype", "name", "image"}, "ParamArg changed: update helpers._program_uop"
|
||||||
|
assert fields[Target] >= {"device", "renderer", "arch"}, "Target changed: update the 'target' key in plain_artifact_compiler.serialize_jit"
|
||||||
|
assert "signature" in getattr(TinyELF, "__annotations__", {}) or hasattr(UOp, "kernel_params"), \
|
||||||
|
"kernel signatures moved: update plain_artifact_compiler.serialize_jit"
|
||||||
|
assert callable(getattr(UOp, "param", None)) and callable(getattr(UOp, "from_buffer", None)), "UOp.param/from_buffer: update helpers.PlainJit"
|
||||||
|
assert callable(getattr(UOp, "store_call", None)) and callable(getattr(UOp, "call", None)), "UOp.call/store_call: update helpers.PlainJit"
|
||||||
|
assert callable(getattr(Buffer, "view", None)) and callable(getattr(Buffer, "ensure_allocated", None)), "Buffer.view: update helpers._upload"
|
||||||
|
assert callable(getattr(jit, "compile_linear", None)) and callable(getattr(jit, "_prepare_jit_inputs", None)), "engine.jit: update helpers._Jit"
|
||||||
|
assert "expected_names" in inspect.signature(jit.CapturedJit).parameters, "CapturedJit: update helpers.PlainJit and plain_artifact_compiler.serialize_jit"
|
||||||
@@ -7,7 +7,10 @@ See the LICENSE.md file in the root directory for more details.
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
@@ -22,12 +25,101 @@ from openpilot.sunnypilot.models.helpers import (ACTIVE_BUNDLE_KEYS, get_active_
|
|||||||
|
|
||||||
# (connect, read) seconds. read is per-request inactivity, not a total cap
|
# (connect, read) seconds. read is per-request inactivity, not a total cap
|
||||||
DOWNLOAD_TIMEOUT = (30, 30)
|
DOWNLOAD_TIMEOUT = (30, 30)
|
||||||
|
# HuggingFace throttles each connection to ~1-2 MB/s but not the connection count; 12 saturates the device link
|
||||||
|
MAX_CONCURRENT_PIECES = 12
|
||||||
|
PIECE_SIZE = 8 * 1024 * 1024
|
||||||
|
PIECE_ATTEMPTS = 3
|
||||||
|
PIECE_BACKOFF = 1.0 # seconds, growing linearly: waits 1 s then 2 s between a piece's three attempts
|
||||||
|
TRANSIENT_HTTP_STATUS = frozenset({408, 429, 500, 502, 503, 504})
|
||||||
|
REPORT_INTERVAL = 0.5 # seconds between progress publications
|
||||||
|
# byte ranges count encoded bytes but iter_content yields decoded ones, so ask for an unencoded body
|
||||||
|
NO_COMPRESSION = {"Accept-Encoding": "identity"}
|
||||||
|
SPEED_SMOOTHING = 0.7 # weight of the previous speed sample in the published speed
|
||||||
|
SPEED_FLOOR = 0.05 * 1024 ** 2 # bytes/s; anything slower would only display as 0.0 MB/s, so it reads as no rate
|
||||||
|
|
||||||
|
|
||||||
class DownloadCancelled(Exception):
|
class DownloadCancelled(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_transient(e: BaseException) -> bool:
|
||||||
|
if isinstance(e, requests.HTTPError):
|
||||||
|
return e.response is not None and e.response.status_code in TRANSIENT_HTTP_STATUS
|
||||||
|
return isinstance(e, (requests.ConnectionError, requests.Timeout, requests.exceptions.ChunkedEncodingError))
|
||||||
|
|
||||||
|
|
||||||
|
class _Progress:
|
||||||
|
def __init__(self):
|
||||||
|
self._bytes = 0
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
def add_bytes(self, n: int) -> None:
|
||||||
|
with self._lock:
|
||||||
|
self._bytes += n
|
||||||
|
|
||||||
|
def snapshot(self) -> int:
|
||||||
|
with self._lock:
|
||||||
|
return self._bytes
|
||||||
|
|
||||||
|
|
||||||
|
def _piece_ranges(total: int, piece_size: int) -> list[tuple[int, int]]:
|
||||||
|
return [(start, min(start + piece_size, total)) for start in range(0, total, piece_size)]
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_target(path: str, total: int) -> None:
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.truncate(total)
|
||||||
|
|
||||||
|
|
||||||
|
def _content_range_total(response: requests.Response) -> int | None:
|
||||||
|
if response.status_code != 206:
|
||||||
|
return None
|
||||||
|
match = re.fullmatch(r"bytes \d+-\d+/(\d+)", response.headers.get("Content-Range", ""))
|
||||||
|
return int(match.group(1)) if match else None
|
||||||
|
|
||||||
|
|
||||||
|
def _probe_size(url: str) -> tuple[int, bool]:
|
||||||
|
with requests.get(url, headers={**NO_COMPRESSION, "Range": "bytes=0-0"}, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
total = _content_range_total(response)
|
||||||
|
if total is not None:
|
||||||
|
return total, True
|
||||||
|
return int(response.headers.get("Content-Length", 0)), False
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_piece(url: str, path: str, index: int, start: int, end: int | None, progress: _Progress,
|
||||||
|
cancel: threading.Event, block_size: int) -> None:
|
||||||
|
headers = {**NO_COMPRESSION, "Range": f"bytes={start}-{end - 1}"} if end is not None else NO_COMPRESSION
|
||||||
|
label = f"{os.path.basename(path)} piece {index}"
|
||||||
|
for attempt in range(PIECE_ATTEMPTS):
|
||||||
|
written = 0
|
||||||
|
try:
|
||||||
|
with requests.get(url, headers=headers, stream=True, timeout=DOWNLOAD_TIMEOUT) as response:
|
||||||
|
response.raise_for_status()
|
||||||
|
if end is not None and response.status_code != 206:
|
||||||
|
raise ValueError(f"server ignored byte range for {label}")
|
||||||
|
with open(path, "r+b") as f:
|
||||||
|
f.seek(start)
|
||||||
|
for block in response.iter_content(chunk_size=block_size):
|
||||||
|
if cancel.is_set():
|
||||||
|
raise DownloadCancelled("Download cancelled")
|
||||||
|
f.write(block)
|
||||||
|
written += len(block)
|
||||||
|
progress.add_bytes(len(block))
|
||||||
|
if end is None:
|
||||||
|
f.truncate() # unknown length: the body defines the file size
|
||||||
|
if end is not None and written != end - start:
|
||||||
|
raise requests.exceptions.ConnectionError(f"short read: {written} of {end - start} bytes")
|
||||||
|
return
|
||||||
|
except requests.RequestException as e:
|
||||||
|
progress.add_bytes(-written) # the piece restarts from scratch
|
||||||
|
if not _is_transient(e) or attempt == PIECE_ATTEMPTS - 1:
|
||||||
|
raise
|
||||||
|
cloudlog.warning(f"retrying {label} after {type(e).__name__}: {e}")
|
||||||
|
if cancel.wait(PIECE_BACKOFF * (1 + attempt)):
|
||||||
|
raise DownloadCancelled("Download cancelled") from None
|
||||||
|
|
||||||
|
|
||||||
class ModelManagerSP:
|
class ModelManagerSP:
|
||||||
"""Manages model downloads and status reporting"""
|
"""Manages model downloads and status reporting"""
|
||||||
|
|
||||||
@@ -41,7 +133,7 @@ class ModelManagerSP:
|
|||||||
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
|
self.source_models: dict[str, list[custom.ModelManagerSP.ModelBundle]] = {}
|
||||||
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
|
self.selected_bundle: custom.ModelManagerSP.ModelBundle = None
|
||||||
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
self.active_bundle: custom.ModelManagerSP.ModelBundle = get_active_bundle(self.params, chestnut=self.chestnut_present)
|
||||||
self._chunk_size = 128 * 1000 # 128 KB chunks
|
self._block_size = 128 * 1000 # 128 KB network read blocks
|
||||||
self._download_start_times: dict[str, float] = {} # Track start time per model
|
self._download_start_times: dict[str, float] = {} # Track start time per model
|
||||||
self._download_ref: bytes | str | None = None
|
self._download_ref: bytes | str | None = None
|
||||||
|
|
||||||
@@ -56,7 +148,6 @@ class ModelManagerSP:
|
|||||||
self._download_ref = None
|
self._download_ref = None
|
||||||
|
|
||||||
def _sync_artifact_progress(self, source_artifact) -> None:
|
def _sync_artifact_progress(self, source_artifact) -> None:
|
||||||
"""Mirror download progress to all artifacts sharing the same filename in the selected bundle."""
|
|
||||||
if not self.selected_bundle:
|
if not self.selected_bundle:
|
||||||
return
|
return
|
||||||
for model in self.selected_bundle.models:
|
for model in self.selected_bundle.models:
|
||||||
@@ -65,9 +156,9 @@ class ModelManagerSP:
|
|||||||
artifact.downloadProgress.status = source_artifact.downloadProgress.status
|
artifact.downloadProgress.status = source_artifact.downloadProgress.status
|
||||||
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
|
artifact.downloadProgress.progress = source_artifact.downloadProgress.progress
|
||||||
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
|
artifact.downloadProgress.eta = source_artifact.downloadProgress.eta
|
||||||
|
artifact.downloadProgress.speed = source_artifact.downloadProgress.speed
|
||||||
|
|
||||||
def _calculate_eta(self, filename: str, progress: float) -> int:
|
def _calculate_eta(self, filename: str, progress: float) -> int:
|
||||||
"""Calculate ETA based on elapsed time and current progress"""
|
|
||||||
if filename not in self._download_start_times or progress <= 0:
|
if filename not in self._download_start_times or progress <= 0:
|
||||||
return 60 # Default ETA for new downloads
|
return 60 # Default ETA for new downloads
|
||||||
|
|
||||||
@@ -75,39 +166,74 @@ class ModelManagerSP:
|
|||||||
if elapsed_time <= 0:
|
if elapsed_time <= 0:
|
||||||
return 60
|
return 60
|
||||||
|
|
||||||
# If we're at X% after Y seconds, we can estimate total time as (Y / X) * 100
|
|
||||||
total_estimated_time = (elapsed_time / progress) * 100
|
total_estimated_time = (elapsed_time / progress) * 100
|
||||||
eta = total_estimated_time - elapsed_time
|
eta = total_estimated_time - elapsed_time
|
||||||
|
|
||||||
return max(1, int(eta)) # Return at least 1 second if download is ongoing
|
return max(1, int(eta)) # Return at least 1 second if download is ongoing
|
||||||
|
|
||||||
async def _download_file(self, url: str, path: str, model) -> None:
|
def _set_progress(self, artifact, status, progress: float, eta: int = 0, speed: float = 0.0) -> None:
|
||||||
"""Downloads a file with progress tracking"""
|
artifact.downloadProgress.status = status
|
||||||
self._download_start_times[model.fileName] = time.monotonic()
|
artifact.downloadProgress.progress = progress
|
||||||
|
artifact.downloadProgress.eta = eta
|
||||||
|
artifact.downloadProgress.speed = speed
|
||||||
|
self._sync_artifact_progress(artifact)
|
||||||
|
self._report_status()
|
||||||
|
|
||||||
with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as response: # noqa: ASYNC210
|
def _publish_progress(self, artifact, done_bytes: int, total: int, speed: float) -> None:
|
||||||
response.raise_for_status()
|
# held at 99 until the file passes its hash check
|
||||||
total_size = int(response.headers.get("content-length", 0))
|
progress = min(99.0, done_bytes / total * 100) if total > 0 else 0.0
|
||||||
bytes_downloaded = 0
|
if speed > 0 and total > 0:
|
||||||
|
eta = max(1, int((total - done_bytes) / speed))
|
||||||
|
else:
|
||||||
|
eta = self._calculate_eta(artifact.fileName, progress)
|
||||||
|
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, progress, eta, speed)
|
||||||
|
|
||||||
with open(path, 'wb') as f: # noqa: ASYNC230
|
async def _report_until_done(self, tasks: list[asyncio.Future], artifact, progress: _Progress, total: int,
|
||||||
for chunk in response.iter_content(chunk_size=self._chunk_size): # type: bytes
|
started: tuple[int, float]) -> None:
|
||||||
f.write(chunk)
|
pending = set(tasks)
|
||||||
bytes_downloaded += len(chunk)
|
last_bytes, last_time = started
|
||||||
|
speed = 0.0
|
||||||
|
while pending:
|
||||||
|
done, pending = await asyncio.wait(pending, timeout=REPORT_INTERVAL)
|
||||||
|
for task in done:
|
||||||
|
task.result() # re-raises a worker failure
|
||||||
|
if self._download_interrupted():
|
||||||
|
raise DownloadCancelled("Download cancelled")
|
||||||
|
now = time.monotonic()
|
||||||
|
done_bytes = progress.snapshot()
|
||||||
|
if (dt := now - last_time) > 0:
|
||||||
|
instant = max(0.0, (done_bytes - last_bytes) / dt) # bytes/s this tick; clamped since retried pieces can lower the byte count
|
||||||
|
speed = instant if speed == 0 else SPEED_SMOOTHING * speed + (1 - SPEED_SMOOTHING) * instant
|
||||||
|
if speed < SPEED_FLOOR: # a stall decays toward 0 without reaching it, and a near-0 speed overflows the eta
|
||||||
|
speed = 0.0
|
||||||
|
last_time, last_bytes = now, done_bytes
|
||||||
|
self._publish_progress(artifact, done_bytes, total, speed)
|
||||||
|
|
||||||
if self._download_interrupted():
|
async def _download_file(self, url: str, path: str, artifact) -> None:
|
||||||
raise DownloadCancelled("Download cancelled")
|
self._download_start_times[artifact.fileName] = time.monotonic()
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
|
||||||
if total_size > 0:
|
total, ranged = await loop.run_in_executor(None, _probe_size, url)
|
||||||
progress = (bytes_downloaded / total_size) * 100
|
pieces: list[tuple[int, int | None]] = list(_piece_ranges(total, PIECE_SIZE)) if ranged else [(0, None)]
|
||||||
model.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloading
|
await loop.run_in_executor(None, _prepare_target, path, total)
|
||||||
model.downloadProgress.progress = progress
|
progress = _Progress()
|
||||||
model.downloadProgress.eta = self._calculate_eta(model.fileName, progress)
|
cancel = threading.Event()
|
||||||
self._sync_artifact_progress(model)
|
started = progress.snapshot(), time.monotonic() # before any worker can land bytes
|
||||||
self._report_status()
|
|
||||||
|
|
||||||
# Clean up start time after download completes
|
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_PIECES) as pool:
|
||||||
del self._download_start_times[model.fileName]
|
tasks = [loop.run_in_executor(pool, _fetch_piece, url, path, i, start, end, progress, cancel, self._block_size)
|
||||||
|
for i, (start, end) in enumerate(pieces)]
|
||||||
|
try:
|
||||||
|
await self._report_until_done(tasks, artifact, progress, total, started)
|
||||||
|
except BaseException as e:
|
||||||
|
cancel.set()
|
||||||
|
if isinstance(e, DownloadCancelled):
|
||||||
|
artifact.downloadProgress.canceling = True
|
||||||
|
self._set_progress(artifact, custom.ModelManagerSP.DownloadStatus.downloading, artifact.downloadProgress.progress)
|
||||||
|
for task in tasks:
|
||||||
|
task.cancel() # drops queued pieces; running ones see the event at their next block
|
||||||
|
await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
raise
|
||||||
|
del self._download_start_times[artifact.fileName]
|
||||||
|
|
||||||
async def _process_artifact(self, artifact, destination_path: str) -> None:
|
async def _process_artifact(self, artifact, destination_path: str) -> None:
|
||||||
if not artifact.downloadUri.uri:
|
if not artifact.downloadUri.uri:
|
||||||
@@ -119,33 +245,31 @@ class ModelManagerSP:
|
|||||||
expected_hash = artifact.downloadUri.sha256
|
expected_hash = artifact.downloadUri.sha256
|
||||||
filename = artifact.fileName
|
filename = artifact.fileName
|
||||||
full_path = os.path.join(destination_path, filename)
|
full_path = os.path.join(destination_path, filename)
|
||||||
|
status = custom.ModelManagerSP.DownloadStatus
|
||||||
|
artifact.downloadProgress.canceling = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
self._set_progress(artifact, status.verifying, 0)
|
||||||
if await verify_file(full_path, expected_hash):
|
if await verify_file(full_path, expected_hash):
|
||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
self._set_progress(artifact, status.cached, 100)
|
||||||
artifact.downloadProgress.progress = 100
|
|
||||||
artifact.downloadProgress.eta = 0
|
|
||||||
self._sync_artifact_progress(artifact)
|
|
||||||
self._report_status()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._set_progress(artifact, status.downloading, 0)
|
||||||
await self._download_file(url, full_path, artifact)
|
await self._download_file(url, full_path, artifact)
|
||||||
|
self._set_progress(artifact, status.verifying, 99)
|
||||||
if not await verify_file(full_path, expected_hash):
|
if not await verify_file(full_path, expected_hash):
|
||||||
raise ValueError(f"Hash validation failed for {filename}")
|
raise ValueError(f"Hash validation failed for {filename}")
|
||||||
|
|
||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.downloaded
|
self._set_progress(artifact, status.downloaded, 100)
|
||||||
artifact.downloadProgress.progress = 100
|
|
||||||
artifact.downloadProgress.eta = 0
|
|
||||||
self._sync_artifact_progress(artifact)
|
|
||||||
self._report_status()
|
|
||||||
|
|
||||||
except DownloadCancelled:
|
except DownloadCancelled:
|
||||||
self._download_start_times.pop(artifact.fileName, None)
|
self._download_start_times.pop(artifact.fileName, None)
|
||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
artifact.downloadProgress.status = status.failed
|
||||||
artifact.downloadProgress.eta = 0
|
artifact.downloadProgress.eta = 0
|
||||||
|
artifact.downloadProgress.speed = 0
|
||||||
self._sync_artifact_progress(artifact)
|
self._sync_artifact_progress(artifact)
|
||||||
if self.selected_bundle:
|
if self.selected_bundle:
|
||||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
self.selected_bundle.status = status.failed
|
||||||
self._report_status()
|
self._report_status()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -153,11 +277,12 @@ class ModelManagerSP:
|
|||||||
cloudlog.error(f"Error downloading {filename}: {str(e)}")
|
cloudlog.error(f"Error downloading {filename}: {str(e)}")
|
||||||
if os.path.isfile(full_path): # noqa: ASYNC240
|
if os.path.isfile(full_path): # noqa: ASYNC240
|
||||||
os.remove(full_path)
|
os.remove(full_path)
|
||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.failed
|
artifact.downloadProgress.status = status.failed
|
||||||
artifact.downloadProgress.eta = 0
|
artifact.downloadProgress.eta = 0
|
||||||
|
artifact.downloadProgress.speed = 0
|
||||||
self._sync_artifact_progress(artifact)
|
self._sync_artifact_progress(artifact)
|
||||||
if self.selected_bundle:
|
if self.selected_bundle:
|
||||||
self.selected_bundle.status = custom.ModelManagerSP.DownloadStatus.failed
|
self.selected_bundle.status = status.failed
|
||||||
self._report_status()
|
self._report_status()
|
||||||
self._download_start_times.pop(artifact.fileName, None)
|
self._download_start_times.pop(artifact.fileName, None)
|
||||||
raise
|
raise
|
||||||
@@ -197,6 +322,7 @@ class ModelManagerSP:
|
|||||||
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
artifact.downloadProgress.status = custom.ModelManagerSP.DownloadStatus.cached
|
||||||
artifact.downloadProgress.progress = 100
|
artifact.downloadProgress.progress = 100
|
||||||
artifact.downloadProgress.eta = 0
|
artifact.downloadProgress.eta = 0
|
||||||
|
artifact.downloadProgress.speed = 0
|
||||||
else:
|
else:
|
||||||
seen_artifacts.add(artifact.fileName)
|
seen_artifacts.add(artifact.fileName)
|
||||||
await self._process_artifact(artifact, destination_path)
|
await self._process_artifact(artifact, destination_path)
|
||||||
@@ -272,12 +398,7 @@ class ModelManagerSP:
|
|||||||
rk.keep_time()
|
rk.keep_time()
|
||||||
|
|
||||||
def clear_model_cache(self) -> None:
|
def clear_model_cache(self) -> None:
|
||||||
"""
|
""" Clears the model cache directory of all files except those in the active model bundle."""
|
||||||
Clears the model cache directory of all files except those in the active model bundle.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Get list of files used by both slots' selected bundles (either may become
|
|
||||||
# the truly active bundle depending on hardware availability)
|
|
||||||
active_files = []
|
active_files = []
|
||||||
for source in ACTIVE_BUNDLE_KEYS:
|
for source in ACTIVE_BUNDLE_KEYS:
|
||||||
if selected_bundle := get_selected_bundle(self.params, source):
|
if selected_bundle := get_selected_bundle(self.params, source):
|
||||||
@@ -285,7 +406,6 @@ class ModelManagerSP:
|
|||||||
if model.artifact.fileName:
|
if model.artifact.fileName:
|
||||||
active_files.append(model.artifact.fileName)
|
active_files.append(model.artifact.fileName)
|
||||||
|
|
||||||
# Remove all files except active ones
|
|
||||||
model_dir = Paths.model_root()
|
model_dir = Paths.model_root()
|
||||||
try:
|
try:
|
||||||
for filename in os.listdir(model_dir):
|
for filename in os.listdir(model_dir):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -126,6 +126,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
|
|||||||
keys.extend([
|
keys.extend([
|
||||||
"ToyotaEnforceStockLongitudinal",
|
"ToyotaEnforceStockLongitudinal",
|
||||||
"ToyotaStopAndGoHack",
|
"ToyotaStopAndGoHack",
|
||||||
|
"ToyotaTSS2Smooth",
|
||||||
])
|
])
|
||||||
|
|
||||||
return [{k: params.get(k, return_default=True)} for k in keys]
|
return [{k: params.get(k, return_default=True)} for k in keys]
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, 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.cereal import log
|
||||||
|
from openpilot.common.params import Params
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX
|
||||||
|
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||||
|
|
||||||
|
SCALE_RELAXED = 0.3
|
||||||
|
SCALE_STANDARD = 0.5
|
||||||
|
SCALE_AGGRESSIVE = 0.7
|
||||||
|
|
||||||
|
|
||||||
|
def get_accel_boost_scale(personality) -> float:
|
||||||
|
if personality == log.LongitudinalPersonality.aggressive:
|
||||||
|
return SCALE_AGGRESSIVE
|
||||||
|
if personality == log.LongitudinalPersonality.relaxed:
|
||||||
|
return SCALE_RELAXED
|
||||||
|
return SCALE_STANDARD
|
||||||
|
|
||||||
|
|
||||||
|
def get_accel_boost_max(personality) -> float:
|
||||||
|
return ACCEL_BOOST_MAX * (1.0 + get_accel_boost_scale(personality))
|
||||||
|
|
||||||
|
|
||||||
|
class AccelBoostExt:
|
||||||
|
def __init__(self, stock: AccelBoost | None = None):
|
||||||
|
self._stock = stock if stock is not None else AccelBoost()
|
||||||
|
self._extra = AccelBoost()
|
||||||
|
self.params = Params()
|
||||||
|
self.frame = -1
|
||||||
|
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
|
||||||
|
|
||||||
|
self.extra_boost = 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_boost(self) -> float:
|
||||||
|
return self._stock.total_boost + self.extra_boost
|
||||||
|
|
||||||
|
def _update_params(self) -> None:
|
||||||
|
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||||
|
self.enabled = self.params.get_bool("FollowDistanceAccelBoost")
|
||||||
|
|
||||||
|
def update(self, sm, output_a_target_e2e, output_a_target_mpc, a_cruise):
|
||||||
|
self.frame += 1
|
||||||
|
self._update_params()
|
||||||
|
|
||||||
|
if self.enabled and sm['selfdriveState'].enabled:
|
||||||
|
scale = get_accel_boost_scale(sm['selfdriveState'].personality)
|
||||||
|
extra_output = self._extra.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
|
||||||
|
self.extra_boost = (extra_output - output_a_target_e2e) * scale
|
||||||
|
else:
|
||||||
|
self.extra_boost = 0.0
|
||||||
|
self._extra.total_boost = 0.0
|
||||||
|
self._extra.boost_this_override = 0.0
|
||||||
|
self._extra.boost_eligible = False
|
||||||
|
|
||||||
|
return self._stock.update(sm, output_a_target_e2e, output_a_target_mpc, a_cruise)
|
||||||
|
|
||||||
|
target = output_a_target_e2e + self.extra_boost
|
||||||
|
return self._stock.update(sm, target, output_a_target_mpc, a_cruise)
|
||||||
@@ -10,6 +10,8 @@ from openpilot.common.params import Params
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class AutoLaneChangeMode:
|
class AutoLaneChangeMode:
|
||||||
OFF = -1
|
OFF = -1
|
||||||
NUDGE = 0 # default
|
NUDGE = 0 # default
|
||||||
@@ -31,6 +33,7 @@ AUTO_LANE_CHANGE_TIMER = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ONE_SECOND_DELAY = -1
|
ONE_SECOND_DELAY = -1
|
||||||
|
EDGE_DELAY = -2
|
||||||
|
|
||||||
|
|
||||||
class AutoLaneChangeController:
|
class AutoLaneChangeController:
|
||||||
@@ -48,6 +51,7 @@ class AutoLaneChangeController:
|
|||||||
self.prev_brake_pressed = False
|
self.prev_brake_pressed = False
|
||||||
self.auto_lane_change_allowed = False
|
self.auto_lane_change_allowed = False
|
||||||
self.prev_lane_change = False
|
self.prev_lane_change = False
|
||||||
|
self.lane_change_blocked = False
|
||||||
|
|
||||||
self.read_params()
|
self.read_params()
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ class AutoLaneChangeController:
|
|||||||
self.read_params()
|
self.read_params()
|
||||||
self.param_read_counter += 1
|
self.param_read_counter += 1
|
||||||
|
|
||||||
def update_lane_change_timers(self, blindspot_detected: bool) -> None:
|
def update_lane_change_timers(self, blindspot_detected: bool, edge_detected: bool) -> None:
|
||||||
self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer,
|
self.lane_change_delay = AUTO_LANE_CHANGE_TIMER.get(self.lane_change_set_timer,
|
||||||
AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE])
|
AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGE])
|
||||||
|
|
||||||
@@ -80,6 +84,12 @@ class AutoLaneChangeController:
|
|||||||
else:
|
else:
|
||||||
self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY
|
self.lane_change_wait_timer = self.lane_change_delay + ONE_SECOND_DELAY
|
||||||
|
|
||||||
|
if edge_detected and self.lane_change_delay > 0:
|
||||||
|
if self.lane_change_delay == AUTO_LANE_CHANGE_TIMER[AutoLaneChangeMode.NUDGELESS]:
|
||||||
|
self.lane_change_wait_timer = min(self.lane_change_wait_timer, EDGE_DELAY)
|
||||||
|
else:
|
||||||
|
self.lane_change_wait_timer = min(self.lane_change_wait_timer, self.lane_change_delay + EDGE_DELAY)
|
||||||
|
|
||||||
def update_allowed(self) -> bool:
|
def update_allowed(self) -> bool:
|
||||||
# Auto lane change allowed if:
|
# Auto lane change allowed if:
|
||||||
# 1. A valid delay is set (non-zero)
|
# 1. A valid delay is set (non-zero)
|
||||||
@@ -97,14 +107,23 @@ class AutoLaneChangeController:
|
|||||||
|
|
||||||
return bool(self.lane_change_wait_timer > self.lane_change_delay)
|
return bool(self.lane_change_wait_timer > self.lane_change_delay)
|
||||||
|
|
||||||
def update_lane_change(self, blindspot_detected: bool, brake_pressed: bool) -> None:
|
def update_lane_change(self, blindspot_detected: bool, edge_detected: bool, brake_pressed: bool,
|
||||||
|
torque_applied: bool = False) -> None:
|
||||||
if brake_pressed and not self.prev_brake_pressed:
|
if brake_pressed and not self.prev_brake_pressed:
|
||||||
self.prev_brake_pressed = brake_pressed
|
self.prev_brake_pressed = brake_pressed
|
||||||
|
|
||||||
self.update_lane_change_timers(blindspot_detected)
|
self.update_lane_change_timers(blindspot_detected, edge_detected)
|
||||||
|
|
||||||
self.auto_lane_change_allowed = self.update_allowed()
|
self.auto_lane_change_allowed = self.update_allowed()
|
||||||
|
|
||||||
|
# BSM always blocks. Edge blocks auto mode but nudge can override.
|
||||||
|
if blindspot_detected:
|
||||||
|
self.lane_change_blocked = True
|
||||||
|
elif edge_detected:
|
||||||
|
self.lane_change_blocked = not torque_applied
|
||||||
|
else:
|
||||||
|
self.lane_change_blocked = False
|
||||||
|
|
||||||
def update_state(self):
|
def update_state(self):
|
||||||
if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting:
|
if self.DH.lane_change_state == log.LaneChangeState.laneChangeStarting:
|
||||||
self.prev_lane_change = True
|
self.prev_lane_change = True
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from openpilot.cereal import messaging, custom
|
|||||||
from opendbc.car import structs
|
from opendbc.car import structs
|
||||||
from openpilot.common.constants import CV
|
from openpilot.common.constants import CV
|
||||||
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
from openpilot.selfdrive.car.cruise import V_CRUISE_MAX
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
from openpilot.sunnypilot.selfdrive.controls.lib.e2e_alerts_helper import E2EAlertsHelper
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
|
||||||
@@ -32,6 +33,16 @@ class LongitudinalPlannerSP:
|
|||||||
|
|
||||||
self.output_v_target = 0.
|
self.output_v_target = 0.
|
||||||
self.output_a_target = 0.
|
self.output_a_target = 0.
|
||||||
|
self._accel_boost: AccelBoostExt | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accel_boost(self) -> AccelBoostExt:
|
||||||
|
assert self._accel_boost is not None
|
||||||
|
return self._accel_boost
|
||||||
|
|
||||||
|
@accel_boost.setter
|
||||||
|
def accel_boost(self, controller) -> None:
|
||||||
|
self._accel_boost = AccelBoostExt(controller)
|
||||||
|
|
||||||
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]:
|
||||||
CS = sm['carState']
|
CS = sm['carState']
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
"""
|
||||||
|
Copyright (c) 2021-, 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 types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
from openpilot.cereal import log
|
||||||
|
from openpilot.common.realtime import DT_MDL
|
||||||
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
from openpilot.selfdrive.controls.lib.accel_boost import AccelBoost, ACCEL_BOOST_MAX, ACCEL_BOOST_PER_OVERRIDE, ACCEL_BOOST_RATE
|
||||||
|
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext import AccelBoostExt, get_accel_boost_max
|
||||||
|
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
|
||||||
|
|
||||||
|
E2E_ACCEL = 0.5
|
||||||
|
PLANNER_ACCEL = 2.0
|
||||||
|
STEPS_PER_OVERRIDE = round(ACCEL_BOOST_PER_OVERRIDE / (ACCEL_BOOST_RATE * DT_MDL))
|
||||||
|
EXPECTED_SCALE = {
|
||||||
|
log.LongitudinalPersonality.relaxed: 0.3,
|
||||||
|
log.LongitudinalPersonality.standard: 0.5,
|
||||||
|
log.LongitudinalPersonality.aggressive: 0.7,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeParams:
|
||||||
|
def __init__(self, enabled=True):
|
||||||
|
self.enabled = enabled
|
||||||
|
|
||||||
|
def get_bool(self, key):
|
||||||
|
return self.enabled
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSubMaster:
|
||||||
|
def __init__(self, enabled=True, gas_pressed=False, v_ego=15.0,
|
||||||
|
personality=log.LongitudinalPersonality.standard):
|
||||||
|
self._messages = {
|
||||||
|
'selfdriveState': SimpleNamespace(enabled=enabled, personality=personality),
|
||||||
|
'carState': SimpleNamespace(gasPressed=gas_pressed, vEgo=v_ego),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self._messages[key]
|
||||||
|
|
||||||
|
|
||||||
|
def update(boost, sm, e2e=E2E_ACCEL):
|
||||||
|
return boost.update(sm, e2e, PLANNER_ACCEL, PLANNER_ACCEL)
|
||||||
|
|
||||||
|
|
||||||
|
class PlannerStub(LongitudinalPlannerSP):
|
||||||
|
def __init__(self):
|
||||||
|
self._accel_boost = None
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccelBoostExt(OpenpilotTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.params = FakeParams()
|
||||||
|
patcher = mock.patch("openpilot.sunnypilot.selfdrive.controls.lib.accel_boost_ext.Params", return_value=self.params)
|
||||||
|
patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
|
def _boost_to_max(self, personality):
|
||||||
|
boost = AccelBoostExt()
|
||||||
|
sm = FakeSubMaster(personality=personality)
|
||||||
|
# Eligibility is latched on a frame without gas, then applied on the next override.
|
||||||
|
update(boost, sm)
|
||||||
|
for _ in range(10):
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
for _ in range(STEPS_PER_OVERRIDE):
|
||||||
|
update(boost, sm)
|
||||||
|
sm['carState'].gasPressed = False
|
||||||
|
update(boost, sm)
|
||||||
|
return boost, sm
|
||||||
|
|
||||||
|
def _applied_extra(self, boost, sm):
|
||||||
|
update(boost, sm)
|
||||||
|
return boost.extra_boost
|
||||||
|
|
||||||
|
def test_off_matches_stock_boost_exactly(self):
|
||||||
|
self.params.enabled = False
|
||||||
|
sm = FakeSubMaster()
|
||||||
|
stock = AccelBoost()
|
||||||
|
ext = AccelBoostExt()
|
||||||
|
|
||||||
|
stock_out = update(stock, sm)
|
||||||
|
ext_out = update(ext, sm)
|
||||||
|
self.assertAlmostEqual(ext_out, stock_out)
|
||||||
|
|
||||||
|
for _ in range(10):
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
for _ in range(STEPS_PER_OVERRIDE):
|
||||||
|
stock_out = update(stock, sm)
|
||||||
|
ext_out = update(ext, sm)
|
||||||
|
self.assertAlmostEqual(ext_out, stock_out)
|
||||||
|
sm['carState'].gasPressed = False
|
||||||
|
stock_out = update(stock, sm)
|
||||||
|
ext_out = update(ext, sm)
|
||||||
|
self.assertAlmostEqual(ext_out, stock_out)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
|
||||||
|
self.assertAlmostEqual(ext._stock.total_boost, stock_out - E2E_ACCEL)
|
||||||
|
self.assertEqual(ext.extra_boost, 0.0)
|
||||||
|
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||||
|
|
||||||
|
def test_no_boost_without_gas(self):
|
||||||
|
ext = AccelBoostExt()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
out = update(ext, sm)
|
||||||
|
self.assertEqual(ext.total_boost, 0.0)
|
||||||
|
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||||
|
self.assertAlmostEqual(out, E2E_ACCEL)
|
||||||
|
|
||||||
|
def test_boost_requires_e2e_to_be_limiting(self):
|
||||||
|
ext = AccelBoostExt()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
for _ in range(10):
|
||||||
|
ext.update(sm, E2E_ACCEL, E2E_ACCEL, E2E_ACCEL)
|
||||||
|
self.assertEqual(ext.total_boost, 0.0)
|
||||||
|
self.assertEqual(ext._stock.total_boost, 0.0)
|
||||||
|
self.assertEqual(ext.extra_boost, 0.0)
|
||||||
|
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||||
|
|
||||||
|
def test_extra_layer_per_override_cap(self):
|
||||||
|
ext = AccelBoostExt()
|
||||||
|
sm = FakeSubMaster(gas_pressed=False)
|
||||||
|
update(ext, sm)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
for _ in range(1000):
|
||||||
|
update(ext, sm)
|
||||||
|
|
||||||
|
self.assertAlmostEqual(ext._extra.total_boost, ACCEL_BOOST_PER_OVERRIDE)
|
||||||
|
applied = self._applied_extra(ext, sm)
|
||||||
|
expected = ACCEL_BOOST_PER_OVERRIDE * EXPECTED_SCALE[log.LongitudinalPersonality.standard]
|
||||||
|
self.assertAlmostEqual(applied, expected)
|
||||||
|
|
||||||
|
def test_extra_scales_with_following_distance(self):
|
||||||
|
applied = {}
|
||||||
|
for personality in (log.LongitudinalPersonality.relaxed,
|
||||||
|
log.LongitudinalPersonality.standard,
|
||||||
|
log.LongitudinalPersonality.aggressive):
|
||||||
|
ext, sm = self._boost_to_max(personality)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
applied[personality] = self._applied_extra(ext, sm)
|
||||||
|
|
||||||
|
for personality, value in applied.items():
|
||||||
|
self.assertAlmostEqual(value, ACCEL_BOOST_MAX * EXPECTED_SCALE[personality])
|
||||||
|
|
||||||
|
self.assertLess(applied[log.LongitudinalPersonality.relaxed], applied[log.LongitudinalPersonality.standard])
|
||||||
|
self.assertLess(applied[log.LongitudinalPersonality.standard], applied[log.LongitudinalPersonality.aggressive])
|
||||||
|
|
||||||
|
def test_capnp_dynamic_enum_personality_scales(self):
|
||||||
|
# Real SubMaster messages expose personality as a capnp DynamicEnum; equality still matches the enum value.
|
||||||
|
msg = log.SelfdriveState.new_message()
|
||||||
|
msg.personality = log.LongitudinalPersonality.aggressive
|
||||||
|
ext, sm = self._boost_to_max(msg.personality)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
applied = self._applied_extra(ext, sm)
|
||||||
|
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
|
||||||
|
|
||||||
|
def test_extra_feeds_into_stock_last(self):
|
||||||
|
ext, sm = self._boost_to_max(log.LongitudinalPersonality.standard)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
inputs = []
|
||||||
|
original_update = AccelBoost.update
|
||||||
|
|
||||||
|
def spy(boost, sm_, e2e, mpc, cruise):
|
||||||
|
inputs.append(e2e)
|
||||||
|
return original_update(boost, sm_, e2e, mpc, cruise)
|
||||||
|
|
||||||
|
with mock.patch.object(AccelBoost, "update", spy):
|
||||||
|
out = update(ext, sm)
|
||||||
|
|
||||||
|
# First call is our extra layer on the raw target, last call is stock on the boosted target.
|
||||||
|
self.assertAlmostEqual(inputs[0], E2E_ACCEL)
|
||||||
|
self.assertAlmostEqual(inputs[-1], E2E_ACCEL + ext.extra_boost)
|
||||||
|
self.assertAlmostEqual(out, inputs[-1] + ext._stock.total_boost)
|
||||||
|
|
||||||
|
def test_extra_is_added_on_top_of_stock_boost(self):
|
||||||
|
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
out = update(ext, sm)
|
||||||
|
extra = out - E2E_ACCEL - ext._stock.total_boost
|
||||||
|
self.assertAlmostEqual(ext._stock.total_boost, ACCEL_BOOST_MAX)
|
||||||
|
self.assertAlmostEqual(ext.total_boost, ACCEL_BOOST_MAX + ext.extra_boost)
|
||||||
|
self.assertAlmostEqual(ext._stock.total_boost, ACCEL_BOOST_MAX)
|
||||||
|
self.assertAlmostEqual(ext.extra_boost, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.aggressive])
|
||||||
|
self.assertAlmostEqual(extra, ext.extra_boost)
|
||||||
|
|
||||||
|
def test_sp_planner_wraps_assigned_stock_controller(self):
|
||||||
|
planner = PlannerStub()
|
||||||
|
stock = AccelBoost()
|
||||||
|
planner.accel_boost = stock
|
||||||
|
|
||||||
|
self.assertIsInstance(planner.accel_boost, AccelBoostExt)
|
||||||
|
self.assertIs(planner.accel_boost._stock, stock)
|
||||||
|
|
||||||
|
def test_wraps_existing_stock_controller(self):
|
||||||
|
stock = AccelBoost()
|
||||||
|
sm = FakeSubMaster()
|
||||||
|
update(stock, sm)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
update(stock, sm)
|
||||||
|
|
||||||
|
ext = AccelBoostExt(stock)
|
||||||
|
self.assertIs(ext._stock, stock)
|
||||||
|
self.assertAlmostEqual(ext.total_boost, stock.total_boost)
|
||||||
|
|
||||||
|
def test_boost_bar_max_matches_profile_cap(self):
|
||||||
|
for personality, scale in EXPECTED_SCALE.items():
|
||||||
|
self.assertAlmostEqual(get_accel_boost_max(personality), ACCEL_BOOST_MAX * (1.0 + scale))
|
||||||
|
|
||||||
|
def test_total_boost_is_stock_accumulated_plus_extra_applied(self):
|
||||||
|
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
update(ext, sm)
|
||||||
|
self.assertAlmostEqual(ext.total_boost, ext._stock.total_boost + ext.extra_boost)
|
||||||
|
|
||||||
|
def test_turning_toggle_off_restores_stock_only(self):
|
||||||
|
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||||
|
self.assertGreater(ext._extra.total_boost, 0.0)
|
||||||
|
|
||||||
|
self.params.enabled = False
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
# Params are re-read every PARAMS_UPDATE_PERIOD; the extra layer must clear on the next poll.
|
||||||
|
for _ in range(int(PARAMS_UPDATE_PERIOD / DT_MDL) + 1):
|
||||||
|
out = update(ext, sm)
|
||||||
|
|
||||||
|
self.assertEqual(ext._extra.total_boost, 0.0)
|
||||||
|
self.assertEqual(ext.extra_boost, 0.0)
|
||||||
|
self.assertAlmostEqual(out, E2E_ACCEL + ext._stock.total_boost)
|
||||||
|
|
||||||
|
def test_relaxing_following_distance_applies_immediately(self):
|
||||||
|
ext, sm = self._boost_to_max(log.LongitudinalPersonality.aggressive)
|
||||||
|
sm['selfdriveState'].personality = log.LongitudinalPersonality.relaxed
|
||||||
|
sm['carState'].gasPressed = True
|
||||||
|
applied = self._applied_extra(ext, sm)
|
||||||
|
self.assertAlmostEqual(applied, ACCEL_BOOST_MAX * EXPECTED_SCALE[log.LongitudinalPersonality.relaxed])
|
||||||
@@ -8,7 +8,7 @@ from openpilot.common.parameterized import parameterized
|
|||||||
from openpilot.common.realtime import DT_MDL
|
from openpilot.common.realtime import DT_MDL
|
||||||
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
|
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState, LaneChangeDirection
|
||||||
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \
|
from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode, \
|
||||||
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY
|
AUTO_LANE_CHANGE_TIMER, ONE_SECOND_DELAY, EDGE_DELAY
|
||||||
from openpilot.common.test import OpenpilotTestCase
|
from openpilot.common.test import OpenpilotTestCase
|
||||||
|
|
||||||
AUTO_LANE_CHANGE_TIMER_COMBOS = [
|
AUTO_LANE_CHANGE_TIMER_COMBOS = [
|
||||||
@@ -61,7 +61,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
# Update controller
|
# Update controller
|
||||||
num_updates = int(5.0 / DT_MDL)
|
num_updates = int(5.0 / DT_MDL)
|
||||||
for _ in range(num_updates): # Run for 5 seconds
|
for _ in range(num_updates): # Run for 5 seconds
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Mode should not allow lane change immediately
|
# Mode should not allow lane change immediately
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
@@ -74,11 +74,11 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
self.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
||||||
|
|
||||||
# Update controller once to read params
|
# Update controller once to read params
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Update multiple times to exceed the timer threshold
|
# Update multiple times to exceed the timer threshold
|
||||||
for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates
|
for _ in range(1): # Should exceed 0.1s with multiple DT_MDL updates
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Now lane change should be allowed
|
# Now lane change should be allowed
|
||||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||||
@@ -91,7 +91,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
self.alc.lane_change_set_timer = timer_state
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
# Update controller once
|
# Update controller once
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# The timer should still be below the threshold after one update
|
# The timer should still be below the threshold after one update
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
@@ -99,7 +99,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Now lane change should be allowed
|
# Now lane change should be allowed
|
||||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||||
@@ -116,7 +116,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
|
|
||||||
# Update with brake pressed for 1 second
|
# Update with brake pressed for 1 second
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=True)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=True)
|
||||||
|
|
||||||
# Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change
|
# Even though it is an auto lane change mode, lane change should be disallowed due to brake pressed prior initiating lane change
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
@@ -126,7 +126,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
|
|
||||||
# Even releasing brake shouldn't allow auto lane change
|
# Even releasing brake shouldn't allow auto lane change
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
@@ -139,13 +139,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
self.alc.lane_change_set_timer = timer_state
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
# Update with blindspot detected - this should prevent auto lane change
|
# Update with blindspot detected - this should prevent auto lane change
|
||||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
# Keep updating with blindspot detected - should still prevent auto lane change
|
# Keep updating with blindspot detected - should still prevent auto lane change
|
||||||
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
num_updates = int(timer_delay / DT_MDL) + 1 # Add one extra updates to ensure we exceed the threshold
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
@@ -157,13 +157,13 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
self.alc.lane_change_set_timer = timer_state
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
# First update with blindspot detected to set the negative timer
|
# First update with blindspot detected to set the negative timer
|
||||||
self.alc.update_lane_change(blindspot_detected=True, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False)
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
# Now update with blindspot cleared - should start incrementing timer from negative value
|
# Now update with blindspot cleared - should start incrementing timer from negative value
|
||||||
num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1
|
num_updates = int((timer_delay + abs(ONE_SECOND_DELAY)) / DT_MDL) + 1
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# After sufficient updates with no blindspot, auto lane change should be allowed
|
# After sufficient updates with no blindspot, auto lane change should be allowed
|
||||||
assert self.alc.auto_lane_change_allowed
|
assert self.alc.auto_lane_change_allowed
|
||||||
@@ -177,7 +177,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
|
|
||||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Now lane change should be allowed
|
# Now lane change should be allowed
|
||||||
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
assert self.alc.lane_change_wait_timer > self.alc.lane_change_delay
|
||||||
@@ -193,7 +193,7 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
|
|
||||||
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
# Update enough times to exceed the threshold (seconds / DT_MDL)
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
@@ -206,7 +206,86 @@ class TestAutoLaneChangeController(OpenpilotTestCase):
|
|||||||
# Simulate updates for a long period of time (e.g., 10 seconds)
|
# Simulate updates for a long period of time (e.g., 10 seconds)
|
||||||
num_updates = int(10.0 / DT_MDL)
|
num_updates = int(10.0 / DT_MDL)
|
||||||
for _ in range(num_updates):
|
for _ in range(num_updates):
|
||||||
self.alc.update_lane_change(blindspot_detected=False, brake_pressed=False)
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
# Lane change should never be allowed
|
# Lane change should never be allowed
|
||||||
assert not self.alc.auto_lane_change_allowed
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
|
def test_edge_detected_delays_auto_lane_change(self, timer_state, timer_delay):
|
||||||
|
"""Test that road edge detection adds delay to auto lane change."""
|
||||||
|
self._reset_states()
|
||||||
|
self.alc.lane_change_bsm_delay = False
|
||||||
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
|
# Update with edge detected - should prevent auto lane change
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
|
||||||
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
# Keep updating with edge detected - timer keeps resetting, should stay disallowed
|
||||||
|
num_updates = int(timer_delay / DT_MDL) + 1
|
||||||
|
for _ in range(num_updates):
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
|
||||||
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
|
def test_edge_detected_then_cleared_allows_after_delay(self, timer_state, timer_delay):
|
||||||
|
"""Test that auto lane change is allowed after edge clears plus extra delay."""
|
||||||
|
self._reset_states()
|
||||||
|
self.alc.lane_change_bsm_delay = False
|
||||||
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
|
# First update with edge detected to set the negative timer
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False)
|
||||||
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
# Now update with edge cleared - should start incrementing timer from negative value
|
||||||
|
num_updates = int((timer_delay + abs(EDGE_DELAY)) / DT_MDL) + 1
|
||||||
|
for _ in range(num_updates):
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
|
# After sufficient updates with no edge, auto lane change should be allowed
|
||||||
|
assert self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
|
def test_edge_and_bsm_combined_uses_worst_delay(self, timer_state, timer_delay):
|
||||||
|
"""Test that when both BSM and edge are detected, the worst (longest) delay wins."""
|
||||||
|
self._reset_states()
|
||||||
|
self.alc.lane_change_bsm_delay = True
|
||||||
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
|
# Update with both blindspot and edge detected
|
||||||
|
self.alc.update_lane_change(blindspot_detected=True, edge_detected=True, brake_pressed=False)
|
||||||
|
assert not self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
# Clear both - need to wait out the worst delay
|
||||||
|
worst_delay = max(abs(ONE_SECOND_DELAY), abs(EDGE_DELAY))
|
||||||
|
num_updates = int((timer_delay + worst_delay) / DT_MDL) + 1
|
||||||
|
for _ in range(num_updates):
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=False, brake_pressed=False)
|
||||||
|
|
||||||
|
assert self.alc.auto_lane_change_allowed
|
||||||
|
|
||||||
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
|
def test_nudge_overrides_edge_in_lane_change_blocked(self, timer_state, timer_delay):
|
||||||
|
"""Test that lane_change_blocked is False when nudge applied with edge-only."""
|
||||||
|
self._reset_states()
|
||||||
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
|
# Edge detected, no nudge - should be blocked
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=False)
|
||||||
|
assert self.alc.lane_change_blocked
|
||||||
|
|
||||||
|
# Edge detected, nudge applied - should NOT be blocked
|
||||||
|
self.alc.update_lane_change(blindspot_detected=False, edge_detected=True, brake_pressed=False, torque_applied=True)
|
||||||
|
assert not self.alc.lane_change_blocked
|
||||||
|
|
||||||
|
@parameterized.expand(AUTO_LANE_CHANGE_TIMER_COMBOS)
|
||||||
|
def test_nudge_does_not_override_bsm_in_lane_change_blocked(self, timer_state, timer_delay):
|
||||||
|
"""Test that lane_change_blocked stays True when BSM detected even with nudge."""
|
||||||
|
self._reset_states()
|
||||||
|
self.alc.lane_change_set_timer = timer_state
|
||||||
|
|
||||||
|
# BSM detected with nudge - should still be blocked
|
||||||
|
self.alc.update_lane_change(blindspot_detected=True, edge_detected=False, brake_pressed=False, torque_applied=True)
|
||||||
|
assert self.alc.lane_change_blocked
|
||||||
|
|||||||
@@ -113,12 +113,56 @@ class TestDesireHelperIntegration(OpenpilotTestCase):
|
|||||||
left_edge_detected=False, right_edge_detected=False)
|
left_edge_detected=False, right_edge_detected=False)
|
||||||
assert dh.desire == expected_desire
|
assert dh.desire == expected_desire
|
||||||
|
|
||||||
def test_edge_blocks_lane_change(self, set_lane_turn_params):
|
def test_nudge_overrides_edge_detection(self, set_lane_turn_params):
|
||||||
|
"""Manual steering nudge should override road edge detection and allow lane change."""
|
||||||
dh = DesireHelper()
|
dh = DesireHelper()
|
||||||
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||||
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
|
carstate = DummyCarState(vEgo=15, leftBlinker=True, steeringPressed=True, steeringTorque=1)
|
||||||
for _ in range(10):
|
for _ in range(10):
|
||||||
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
|
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
|
||||||
assert dh.lane_change_state == LaneChangeState.preLaneChange
|
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
|
||||||
assert dh.lane_change_direction == LaneChangeDirection.left
|
assert dh.lane_change_direction == LaneChangeDirection.left
|
||||||
|
assert dh.desire == log.Desire.laneChangeLeft
|
||||||
|
|
||||||
|
def test_nudge_does_not_override_bsm(self, set_lane_turn_params):
|
||||||
|
"""Manual steering nudge must NOT override BSM blindspot detection."""
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE
|
||||||
|
carstate = DummyCarState(vEgo=15, leftBlinker=True, leftBlindspot=True, steeringPressed=True, steeringTorque=1)
|
||||||
|
for _ in range(10):
|
||||||
|
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
|
||||||
|
assert dh.lane_change_state == LaneChangeState.preLaneChange
|
||||||
assert dh.desire == log.Desire.none
|
assert dh.desire == log.Desire.none
|
||||||
|
|
||||||
|
def test_edge_blocks_auto_lane_change(self, set_lane_turn_params):
|
||||||
|
"""Road edge detection should still block auto (nudgeless) lane changes."""
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
||||||
|
dh.alc.param_read_counter = 1
|
||||||
|
carstate = DummyCarState(vEgo=15, leftBlinker=True)
|
||||||
|
for _ in range(10):
|
||||||
|
dh.update(carstate, True, 1.0, left_edge_detected=True, right_edge_detected=False)
|
||||||
|
assert dh.lane_change_state == LaneChangeState.preLaneChange
|
||||||
|
assert dh.desire == log.Desire.none
|
||||||
|
|
||||||
|
def test_auto_lane_change_allowed_without_edge(self, set_lane_turn_params):
|
||||||
|
"""Auto lane change should proceed when no edge and no BSM detected."""
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
||||||
|
dh.alc.param_read_counter = 1
|
||||||
|
carstate = DummyCarState(vEgo=15, leftBlinker=True)
|
||||||
|
for _ in range(10):
|
||||||
|
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=False)
|
||||||
|
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
|
||||||
|
assert dh.desire == log.Desire.laneChangeLeft
|
||||||
|
|
||||||
|
def test_right_edge_does_not_block_left_lane_change(self, set_lane_turn_params):
|
||||||
|
"""Edge on opposite side should not affect lane change direction."""
|
||||||
|
dh = DesireHelper()
|
||||||
|
dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGELESS
|
||||||
|
dh.alc.param_read_counter = 1
|
||||||
|
carstate = DummyCarState(vEgo=15, leftBlinker=True)
|
||||||
|
for _ in range(10):
|
||||||
|
dh.update(carstate, True, 1.0, left_edge_detected=False, right_edge_detected=True)
|
||||||
|
assert dh.lane_change_state == LaneChangeState.laneChangeStarting
|
||||||
|
assert dh.desire == log.Desire.laneChangeLeft
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL, DEFAULT_BIG_MO
|
|||||||
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
|
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params import update_car_list_param
|
||||||
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
||||||
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
||||||
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
|
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS, CAR_CAPABILITY_FIELDS
|
||||||
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
|
from openpilot.sunnypilot.sunnylink.tools.generate_settings_schema import generate_schema
|
||||||
|
|
||||||
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
|
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
|
||||||
@@ -181,6 +181,7 @@ def getParamsMetadata() -> str:
|
|||||||
schema = generate_schema()
|
schema = generate_schema()
|
||||||
schema["capabilities"] = generate_capabilities()
|
schema["capabilities"] = generate_capabilities()
|
||||||
schema["capability_labels"] = CAPABILITY_LABELS
|
schema["capability_labels"] = CAPABILITY_LABELS
|
||||||
|
schema["car_capability_fields"] = CAR_CAPABILITY_FIELDS
|
||||||
schema["default_model"] = DEFAULT_MODEL
|
schema["default_model"] = DEFAULT_MODEL
|
||||||
schema["default_big_model"] = DEFAULT_BIG_MODEL
|
schema["default_big_model"] = DEFAULT_BIG_MODEL
|
||||||
schema["chestnut_active"] = params.get_bool("ChestnutActive")
|
schema["chestnut_active"] = params.get_bool("ChestnutActive")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from openpilot.cereal import custom, messaging
|
|||||||
from opendbc.car.structs import car
|
from opendbc.car.structs import car
|
||||||
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
from opendbc.car.hyundai.values import CAR as HYUNDAI_CAR, UNSUPPORTED_LONGITUDINAL_CAR
|
||||||
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
|
from opendbc.car.subaru.values import CAR as SUBARU_CAR, SubaruFlags
|
||||||
|
from opendbc.car.toyota.values import CAR as TOYOTA_CAR, ToyotaFlags
|
||||||
from opendbc.sunnypilot.car.interfaces import has_bsm
|
from opendbc.sunnypilot.car.interfaces import has_bsm
|
||||||
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
from opendbc.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||||
from openpilot.common.params import Params
|
from openpilot.common.params import Params
|
||||||
@@ -43,6 +44,8 @@ CAPABILITY_FIELDS = (
|
|||||||
"device_type",
|
"device_type",
|
||||||
"subaru_has_sng",
|
"subaru_has_sng",
|
||||||
"hyundai_alpha_long_available",
|
"hyundai_alpha_long_available",
|
||||||
|
"toyota_tss2",
|
||||||
|
"custom_model_active",
|
||||||
)
|
)
|
||||||
|
|
||||||
CAPABILITY_LABELS: dict[str, str] = {
|
CAPABILITY_LABELS: dict[str, str] = {
|
||||||
@@ -65,8 +68,29 @@ CAPABILITY_LABELS: dict[str, str] = {
|
|||||||
"device_type": "Device type",
|
"device_type": "Device type",
|
||||||
"subaru_has_sng": "Subaru Stop-and-Go available",
|
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||||
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||||
|
"toyota_tss2": "Toyota TSS2 platform",
|
||||||
|
"custom_model_active": "Non-Default model selected",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Fields read from the car (CarParams*, CarPlatformBundle). Until a car is seen these stay at
|
||||||
|
# their defaults, so a rule failing on one of them means "start the vehicle", not a real reason.
|
||||||
|
CAR_CAPABILITY_FIELDS = (
|
||||||
|
"has_longitudinal_control",
|
||||||
|
"has_icbm",
|
||||||
|
"icbm_available",
|
||||||
|
"torque_allowed",
|
||||||
|
"brand",
|
||||||
|
"pcm_cruise",
|
||||||
|
"alpha_long_available",
|
||||||
|
"steer_control_type",
|
||||||
|
"has_bsm",
|
||||||
|
"tesla_has_vehicle_bus",
|
||||||
|
"has_stop_and_go",
|
||||||
|
"subaru_has_sng",
|
||||||
|
"hyundai_alpha_long_available",
|
||||||
|
"toyota_tss2",
|
||||||
|
)
|
||||||
|
|
||||||
# Explicit defaults for non-boolean capability fields
|
# Explicit defaults for non-boolean capability fields
|
||||||
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
|
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
|
||||||
"brand": "",
|
"brand": "",
|
||||||
@@ -111,6 +135,15 @@ def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
|||||||
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||||
|
|
||||||
|
elif brand == "toyota":
|
||||||
|
if bundle_platform:
|
||||||
|
try:
|
||||||
|
caps["toyota_tss2"] = bool(TOYOTA_CAR[bundle_platform].config.flags & ToyotaFlags.TSS2)
|
||||||
|
except KeyError:
|
||||||
|
cloudlog.exception(f"capabilities: unknown toyota platform {bundle_platform!r}")
|
||||||
|
elif CP is not None:
|
||||||
|
caps["toyota_tss2"] = bool(CP.flags & ToyotaFlags.TSS2)
|
||||||
|
|
||||||
|
|
||||||
def generate_capabilities(params: Params | None = None) -> dict:
|
def generate_capabilities(params: Params | None = None) -> dict:
|
||||||
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
||||||
@@ -133,6 +166,9 @@ def generate_capabilities(params: Params | None = None) -> dict:
|
|||||||
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
||||||
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
||||||
|
|
||||||
|
# runner the manager launches (process_config.is_tinygrad_model); with a chestnut the big slot decides
|
||||||
|
caps["custom_model_active"] = params.get("ModelRunnerTypeCache") == custom.ModelManagerSP.Runner.tinygrad
|
||||||
|
|
||||||
bundle = params.get("CarPlatformBundle")
|
bundle = params.get("CarPlatformBundle")
|
||||||
bundle_brand = _bundle_field(bundle, "brand")
|
bundle_brand = _bundle_field(bundle, "brand")
|
||||||
bundle_platform = _bundle_field(bundle, "platform")
|
bundle_platform = _bundle_field(bundle, "platform")
|
||||||
|
|||||||
@@ -620,6 +620,19 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "FollowDistanceAccelBoost",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Extra Acceleration Boost by Following Distance",
|
||||||
|
"description": "Adds extra acceleration on top of the stock gas-override boost. The extra boost follows the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost remains active when this is off.",
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "IntelligentCruiseButtonManagement",
|
"key": "IntelligentCruiseButtonManagement",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -2021,8 +2034,21 @@
|
|||||||
{
|
{
|
||||||
"id": "model_behavior",
|
"id": "model_behavior",
|
||||||
"title": "Model Behavior",
|
"title": "Model Behavior",
|
||||||
"description": "Lane desire and lead-vehicle awareness tuning",
|
"description": "Lane desire, Blendful lane centering, and lead-vehicle awareness tuning",
|
||||||
"items": [
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "Blendful",
|
||||||
|
"widget": "toggle",
|
||||||
|
"title": "Blendful (Alpha)",
|
||||||
|
"description": "Adds a gentle nudge toward lane center on top of the model's own steering, stronger when lane lines are clear. Yields around obstacles. Not available with the Default model.",
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "custom_model_active",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "LaneTurnDesire",
|
"key": "LaneTurnDesire",
|
||||||
"widget": "toggle",
|
"widget": "toggle",
|
||||||
@@ -2324,6 +2350,33 @@
|
|||||||
"equals": false
|
"equals": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "ToyotaTSS2Smooth",
|
||||||
|
"widget": "toggle",
|
||||||
|
"needs_onroad_cycle": true,
|
||||||
|
"title": "TSS2 Smooth Longitudinal (Alpha)",
|
||||||
|
"description": "Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve for smoother traffic driving.",
|
||||||
|
"enablement": [
|
||||||
|
{
|
||||||
|
"type": "not_engaged"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "has_longitudinal_control",
|
||||||
|
"equals": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "param",
|
||||||
|
"key": "ToyotaEnforceStockLongitudinal",
|
||||||
|
"equals": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "capability",
|
||||||
|
"field": "toyota_tss2",
|
||||||
|
"equals": true
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,3 +63,7 @@ macros:
|
|||||||
release_branches_hide:
|
release_branches_hide:
|
||||||
- type: not
|
- type: not
|
||||||
condition: {type: capability, field: is_sp_release, equals: true}
|
condition: {type: capability, field: is_sp_release, equals: true}
|
||||||
|
|
||||||
|
# Gate for custom model features based on active source.
|
||||||
|
custom_model:
|
||||||
|
- { type: capability, field: custom_model_active, equals: true }
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ sections:
|
|||||||
label: Relaxed
|
label: Relaxed
|
||||||
enablement:
|
enablement:
|
||||||
- $ref: '#/macros/longitudinal'
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- key: FollowDistanceAccelBoost
|
||||||
|
widget: toggle
|
||||||
|
title: Extra Acceleration Boost by Following Distance
|
||||||
|
description: Adds extra acceleration on top of the stock gas-override boost. The extra boost follows
|
||||||
|
the Following Distance setting, strongest when Aggressive and weakest when Relaxed. The stock boost
|
||||||
|
remains active when this is off.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
- key: IntelligentCruiseButtonManagement
|
- key: IntelligentCruiseButtonManagement
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
title: Intelligent Cruise Button Management (ICBM) (Alpha)
|
||||||
|
|||||||
@@ -9,8 +9,14 @@ description: Driving model behavior and camera calibration
|
|||||||
sections:
|
sections:
|
||||||
- id: model_behavior
|
- id: model_behavior
|
||||||
title: Model Behavior
|
title: Model Behavior
|
||||||
description: Lane desire and lead-vehicle awareness tuning
|
description: Lane desire, Blendful lane centering, and lead-vehicle awareness tuning
|
||||||
items:
|
items:
|
||||||
|
- key: Blendful
|
||||||
|
widget: toggle
|
||||||
|
title: Blendful (Alpha)
|
||||||
|
description: Adds a gentle nudge toward lane center on top of the model's own steering, stronger when lane lines are clear. Yields around obstacles. Not available with the Default model.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/custom_model'
|
||||||
- key: LaneTurnDesire
|
- key: LaneTurnDesire
|
||||||
widget: toggle
|
widget: toggle
|
||||||
title: Use Lane Turn Desires
|
title: Use Lane Turn Desires
|
||||||
|
|||||||
@@ -101,3 +101,18 @@ sections:
|
|||||||
- type: param
|
- type: param
|
||||||
key: ToyotaEnforceStockLongitudinal
|
key: ToyotaEnforceStockLongitudinal
|
||||||
equals: false
|
equals: false
|
||||||
|
- key: ToyotaTSS2Smooth
|
||||||
|
widget: toggle
|
||||||
|
needs_onroad_cycle: true
|
||||||
|
title: TSS2 Smooth Longitudinal (Alpha)
|
||||||
|
description: Enable smoother longitudinal control for TSS2 vehicles. Uses a custom speed-dependent integral gain curve
|
||||||
|
for smoother traffic driving.
|
||||||
|
enablement:
|
||||||
|
- $ref: '#/macros/not_engaged'
|
||||||
|
- $ref: '#/macros/longitudinal'
|
||||||
|
- type: param
|
||||||
|
key: ToyotaEnforceStockLongitudinal
|
||||||
|
equals: false
|
||||||
|
- type: capability
|
||||||
|
field: toyota_tss2
|
||||||
|
equals: true
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from openpilot.sunnypilot.sunnylink.capabilities import (
|
|||||||
CAPABILITY_DEFAULTS,
|
CAPABILITY_DEFAULTS,
|
||||||
CAPABILITY_FIELDS,
|
CAPABILITY_FIELDS,
|
||||||
CAPABILITY_LABELS,
|
CAPABILITY_LABELS,
|
||||||
|
CAR_CAPABILITY_FIELDS,
|
||||||
PROTOCOL_VERSION,
|
PROTOCOL_VERSION,
|
||||||
generate_capabilities,
|
generate_capabilities,
|
||||||
)
|
)
|
||||||
@@ -75,6 +76,12 @@ class TestOpaquePerBrandFlags(OpenpilotTestCase):
|
|||||||
def test_hyundai_alpha_long_available_default_false(self, caps):
|
def test_hyundai_alpha_long_available_default_false(self, caps):
|
||||||
assert caps["hyundai_alpha_long_available"] is False
|
assert caps["hyundai_alpha_long_available"] is False
|
||||||
|
|
||||||
|
def test_toyota_tss2_field_present(self):
|
||||||
|
assert "toyota_tss2" in CAPABILITY_FIELDS
|
||||||
|
|
||||||
|
def test_toyota_tss2_default_false(self, caps):
|
||||||
|
assert caps["toyota_tss2"] is False
|
||||||
|
|
||||||
|
|
||||||
class TestCapabilitiesShape(OpenpilotTestCase):
|
class TestCapabilitiesShape(OpenpilotTestCase):
|
||||||
def test_all_fields_present(self, caps):
|
def test_all_fields_present(self, caps):
|
||||||
@@ -85,6 +92,10 @@ class TestCapabilitiesShape(OpenpilotTestCase):
|
|||||||
for field in CAPABILITY_FIELDS:
|
for field in CAPABILITY_FIELDS:
|
||||||
assert field in CAPABILITY_LABELS, f"CAPABILITY_LABELS missing {field}"
|
assert field in CAPABILITY_LABELS, f"CAPABILITY_LABELS missing {field}"
|
||||||
|
|
||||||
|
def test_car_fields_are_capabilities(self):
|
||||||
|
assert set(CAR_CAPABILITY_FIELDS) <= set(CAPABILITY_FIELDS)
|
||||||
|
assert "custom_model_active" not in CAR_CAPABILITY_FIELDS
|
||||||
|
|
||||||
def test_string_defaults_are_strings(self, caps):
|
def test_string_defaults_are_strings(self, caps):
|
||||||
assert isinstance(caps["brand"], str)
|
assert isinstance(caps["brand"], str)
|
||||||
assert isinstance(caps["steer_control_type"], str)
|
assert isinstance(caps["steer_control_type"], str)
|
||||||
|
|||||||
@@ -207,6 +207,7 @@ class TestNotEngagedReplacement(OpenpilotTestCase):
|
|||||||
"AlphaLongitudinalEnabled",
|
"AlphaLongitudinalEnabled",
|
||||||
"ToyotaEnforceStockLongitudinal",
|
"ToyotaEnforceStockLongitudinal",
|
||||||
"ToyotaStopAndGoHack",
|
"ToyotaStopAndGoHack",
|
||||||
|
"ToyotaTSS2Smooth",
|
||||||
], names=["key"])
|
], names=["key"])
|
||||||
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
def test_offroad_only_replaced_with_not_engaged(self, schema, key):
|
||||||
"""These items should use not_engaged, not offroad_only."""
|
"""These items should use not_engaged, not offroad_only."""
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ class TestKnownVehicleSettings(OpenpilotTestCase):
|
|||||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("toyota"))}
|
||||||
assert "ToyotaEnforceStockLongitudinal" in keys
|
assert "ToyotaEnforceStockLongitudinal" in keys
|
||||||
assert "ToyotaStopAndGoHack" in keys
|
assert "ToyotaStopAndGoHack" in keys
|
||||||
|
assert "ToyotaTSS2Smooth" in keys
|
||||||
|
|
||||||
def test_tesla_has_coop_steering(self, schema):
|
def test_tesla_has_coop_steering(self, schema):
|
||||||
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}
|
keys = {i["key"] for i in _brand_items(schema["vehicle_settings"].get("tesla"))}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class DownloadStatusAction(ItemAction):
|
|||||||
self.icon: str | None = None
|
self.icon: str | None = None
|
||||||
self.icon_color: rl.Color | None = None
|
self.icon_color: rl.Color | None = None
|
||||||
self._font = gui_app.font(FontWeight.NORMAL)
|
self._font = gui_app.font(FontWeight.NORMAL)
|
||||||
# raw progress arrives in steps, one per 128KB chunk the manager publishes
|
# raw progress arrives in steps, one per progress report from the manager
|
||||||
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
self._progress = FirstOrderFilter(0.0, 0.5, 1 / gui_app.target_fps)
|
||||||
# integrated per frame; (t * speed) % span jumps whenever the fill width changes
|
# integrated per frame; (t * speed) % span jumps whenever the fill width changes
|
||||||
self._sweep = 0.0
|
self._sweep = 0.0
|
||||||
|
|||||||
+1
-1
Submodule tinygrad_repo updated: 69fd4d34d4...92b742f711
Reference in New Issue
Block a user