mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-06-23 21:02:08 +08:00
Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| acb4bb848b | |||
| 0cd913d7ac | |||
| 7fd1fea3b0 | |||
| e79c3243b2 | |||
| a936c311d7 | |||
| 8e53fe176d | |||
| 22a12aec35 | |||
| 426c2abd61 | |||
| 1a84559cec | |||
| 65b61fd965 | |||
| a01449822f | |||
| f52db76444 | |||
| b61f095ef2 | |||
| 59e5fd7fe6 | |||
| 607811b6ad | |||
| 2414fe287d | |||
| f7a4dbc47f | |||
| fd0a5f806c | |||
| de62240e97 | |||
| adba29da1b | |||
| 6d6a91c318 | |||
| 7259687578 | |||
| 83a07e4bfa | |||
| cb65777186 | |||
| 282b867fcb | |||
| 3fd3c31966 | |||
| f3c3581472 | |||
| 465daf323c | |||
| 3fe8e155b6 | |||
| c0bffa2a8c | |||
| c9edcaa1a6 | |||
| 76101207cb | |||
| 62b01d3799 | |||
| a3be1edde7 | |||
| 7869542683 | |||
| 981aab0500 | |||
| 9c82592906 | |||
| 5d3f95d420 | |||
| f8d19fe9dd | |||
| 9d711350c2 | |||
| 45c853c87a | |||
| e8ab9d812d | |||
| ed775185f2 | |||
| 7bbbc6588e | |||
| e68c65d15d | |||
| 0db8722221 | |||
| a33497ed19 | |||
| 91f2bf3459 | |||
| 7fad2fc189 | |||
| e74252bdf5 | |||
| 6b795ab513 | |||
| 3b1b4ef90f | |||
| 48d33e98e7 | |||
| 2717d97350 | |||
| 64232397ed | |||
| 0613442ac9 | |||
| 93ab6ee06b | |||
| 4baa170cfa | |||
| 2be95225b1 | |||
| f8e8c59dd5 | |||
| dd9d4bedf4 | |||
| ada449989b | |||
| 68c7f69439 | |||
| 1db71aff57 | |||
| 942ccb90dd | |||
| e6f5aae246 | |||
| 7032e4a972 | |||
| ffa78eabaa | |||
| 5b03369a8f | |||
| 1e0564b484 | |||
| eb94abaa14 | |||
| 4da32cc009 | |||
| 4820265268 | |||
| 01aa6c4204 | |||
| 21beea51ec | |||
| 98c479830c | |||
| 5369b6880f | |||
| a9e57f0a76 | |||
| 712a358c94 | |||
| 423a7d2ed0 | |||
| e4e10d4b87 | |||
| 632e9d13b2 | |||
| 362e9ce04b | |||
| 51d0666c85 | |||
| deda1329a2 | |||
| 4110749cb0 | |||
| 3946e643f6 | |||
| 0c37a38596 | |||
| 9c5acf61c0 | |||
| 121b304fe0 | |||
| 47d848293b |
@@ -23,43 +23,56 @@ env:
|
||||
CI: 1
|
||||
|
||||
jobs:
|
||||
generate_cereal_artifact:
|
||||
name: Generate cereal validation artifacts
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: true
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
run: scons -j$(nproc) cereal
|
||||
- name: Generate the log file
|
||||
run: |
|
||||
export PYTHONPATH=${{ github.workspace }}
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py -g -f schema_instances.bin
|
||||
- name: 'Prepare artifact'
|
||||
run: |
|
||||
mkdir -p "cereal/messaging/tests/cereal_validations"
|
||||
cp cereal/messaging/tests/validate_sp_cereal_upstream.py "cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py"
|
||||
cp schema_instances.bin "cereal/messaging/tests/cereal_validations/schema_instances.bin"
|
||||
- name: 'Upload Artifact'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: cereal/messaging/tests/cereal_validations
|
||||
|
||||
validate_cereal_with_upstream:
|
||||
name: Validate cereal with Upstream
|
||||
runs-on: ubuntu-24.04
|
||||
needs: generate_cereal_artifact
|
||||
steps:
|
||||
- name: Checkout sunnypilot cereal
|
||||
- name: Checkout sunnypilot
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
sparse-checkout: cereal
|
||||
|
||||
- name: Init sunnypilot opendbc submodule
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Checkout upstream openpilot cereal
|
||||
- name: Checkout upstream openpilot
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: 'commaai/openpilot'
|
||||
path: upstream_openpilot
|
||||
sparse-checkout: cereal
|
||||
path: openpilot
|
||||
submodules: true
|
||||
ref: "refs/heads/master"
|
||||
|
||||
- name: Init upstream opendbc submodule
|
||||
working-directory: upstream_openpilot
|
||||
run: git submodule update --init --depth 1 opendbc_repo
|
||||
|
||||
- name: Install uv
|
||||
run: pip install uv
|
||||
|
||||
- name: Generate sunnypilot schema
|
||||
- run: ./tools/op.sh setup
|
||||
- name: Build openpilot
|
||||
working-directory: openpilot
|
||||
run: scons -j$(nproc) cereal
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cereal_validations
|
||||
path: openpilot/cereal/messaging/tests/cereal_validations
|
||||
- name: 'Run the validation'
|
||||
run: |
|
||||
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||
-g -f /tmp/sp_schema.json --cereal-dir cereal
|
||||
|
||||
- name: Validate against upstream
|
||||
run: |
|
||||
PYCAPNP_VER=$(python3 -c "import re; m=re.search(r'name = \"pycapnp\"\nversion = \"([^\"]+)\"', open('uv.lock').read()); print(m.group(1))")
|
||||
uv run --isolated --with "pycapnp==${PYCAPNP_VER}" \
|
||||
python3 cereal/messaging/tests/validate_sp_cereal_upstream.py \
|
||||
-r -f /tmp/sp_schema.json --cereal-dir upstream_openpilot/cereal
|
||||
export PYTHONPATH=${{ github.workspace }}/openpilot
|
||||
chmod +x openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py
|
||||
python3 openpilot/cereal/messaging/tests/cereal_validations/validate_sp_cereal_upstream.py -r -f openpilot/cereal/messaging/tests/cereal_validations/schema_instances.bin
|
||||
|
||||
@@ -29,9 +29,9 @@ jobs:
|
||||
# Build
|
||||
- name: Build docs
|
||||
run: |
|
||||
git lfs pull
|
||||
pip install zensical
|
||||
python scripts/docs.py build
|
||||
# TODO: can we install just the "docs" dependency group without the normal deps?
|
||||
pip install mkdocs
|
||||
mkdocs build
|
||||
|
||||
# Push to docs.comma.ai
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
@@ -44,10 +44,8 @@ bin/
|
||||
config.json
|
||||
compile_commands.json
|
||||
compare_runtime*.html
|
||||
selfdrive/modeld/models/tg_compiled_flags.json
|
||||
|
||||
# build artifacts
|
||||
docs_site/
|
||||
selfdrive/pandad/pandad
|
||||
cereal/services.h
|
||||
cereal/gen
|
||||
|
||||
+1
-122
@@ -1,40 +1,4 @@
|
||||
sunnypilot Version 2026.001.007 (2026-05-27)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* Revert "DM: Lancia Delta HF Integrale model" by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.006 (2026-05-17)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink SDUI: tweak DisableUpdate param for clarity by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.005 (2026-05-13)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink: add CarParams fallback for brand-specific capabilities by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.004 (2026-05-10)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* sunnylink: switch athena domain by @DevTekVE
|
||||
* sunnylink: fix max time offroad values by @nayan8teen
|
||||
|
||||
sunnypilot Version 2026.001.003 (2026-05-08)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* manager: disable DEVELOPMENT_ONLY reset by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.002 (2026-05-07)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* release: ignore upstream IsReleaseBranch by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.001 (2026-05-06)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* ui: update gates for certain toggles by @sunnyhaibin
|
||||
|
||||
sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
sunnypilot Version 2026.001.000 (2026-03-xx)
|
||||
========================
|
||||
* What's Changed (sunnypilot/sunnypilot)
|
||||
* Complete rewrite of the user interface from Qt C++ to Raylib Python
|
||||
@@ -102,64 +66,6 @@ sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
* Pause Lateral Control with Blinker: Post-Blinker Delay by @CHaucke89
|
||||
* SCC-V: Use p97 for predicted lateral accel by @yasu-oh
|
||||
* Controls: Support for Torque Lateral Control v0 Tune by @sunnyhaibin
|
||||
* [TIZI/TICI] ui: ensure null checks for `CarParams` and `CarParamsSP` by @sunnyhaibin
|
||||
* [TIZI/TICI] ui: use `vCruiseCluster` and `vEgoCluster` for SLA `preActive` by @sunnyhaibin
|
||||
* Fix display of values when using use_float_scaling by @CHaucke89
|
||||
* models: fix default & index "0" by @nayan8teen
|
||||
* [TIZI/TICI] visuals: Improved speed limit by @angaz
|
||||
* ICBM: ensure button timers update on disable to clear stale presses by @jamesmikesell
|
||||
* [TIZI/TICI] ui: simplify Smart Cruise Control text rendering by @sunnyhaibin
|
||||
* controlsd: fix steer_limited_by_safety not updating under MADS by @zephleggett
|
||||
* soundd: trigger timeout warning during MADS lateral-only by @zephleggett
|
||||
* pandad: flasher for Rivian long upgrade module by @lukasloetkolben
|
||||
* modeld_v2: tinygrad transformation warp by @Discountchubbs
|
||||
* tools: block `manage_sunnylinkd` in sim startup script by @sunnyhaibin
|
||||
* [MICI] ui: need superclass `_render` in `HudRendererSP` by @sunnyhaibin
|
||||
* [TIZI/TICI] ui: Speed Limit Assist active status by @sunnyhaibin
|
||||
* ui: reimplement "Screen Off" option to Onroad Brightness by @sunnyhaibin
|
||||
* ui: don't hide steering wheel when blindspot disabled by @royjr
|
||||
* ui: Speed Limit Assist `preActive` improvements by @sunnyhaibin
|
||||
* ui: consolidate Speed Limit Assist `preActive` status rendering by @sunnyhaibin
|
||||
* [MICI] ui: Speed Limit Assist `preActive` status by @sunnyhaibin
|
||||
* sunnypilot modeld: remove thneed modeld by @Discountchubbs
|
||||
* modeld_v2: decouple planplus scaling from accel by @Discountchubbs
|
||||
* sunnylink: Handle exceptions in `getParamsAllKeysV1` to log crashes by @devtekve
|
||||
* [TIZI/TICI] ui: Developer UI cleanup by @sunnyhaibin
|
||||
* [TIZI/TICI] ui: dynamic alert size by @nayan8teen
|
||||
* i18n(fr): Add French translations by @didlawowo
|
||||
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
|
||||
* ui: `AlertFadeAnimator` for longitudinal-related statuses by @sunnyhaibin
|
||||
* pandad: gate unsupported pandas before flashing by @sunnyhaibin
|
||||
* Rivian: Flash xnor's Longitudinal Upgrade Kit prior supported panda check by @lukasloetkolben
|
||||
* [TIZI/TICI] ui: add back gate steering arc behind toggle by @sunnyhaibin
|
||||
* ui: gate Onroad Brightness Delay on readiness by @sunnyhaibin
|
||||
* ui: add new timer options for Onroad Brightness Delay by @sunnyhaibin
|
||||
* [TIZI/TICI] ui: branch switcher is always available by @sunnyhaibin
|
||||
* pandad: always prioritize internal panda by @sunnyhaibin
|
||||
* sunnylinkd: fetch compressed params schema by @sunnyhaibin
|
||||
* sunnypilot locationd: remove unused car_ekf filter by @sunnyhaibin
|
||||
* modeld_v2: update deprecated temporalPose ref by @sunnyhaibin
|
||||
* NNLC: restore pre-v1 PID gains in torque extension by @mmmorks
|
||||
* MADS safety: enable heartbeat and lateral controls mismatch checks by @sunnyhaibin
|
||||
* [MICI] ui: models panel enhancements by @nayan8teen
|
||||
* [TIZI/TICI] ui: fix unintended selection while scrolling in TreeOptionDialog by @TheSecurityDev
|
||||
* tools: script for video concatenation by @Discountchubbs
|
||||
* tools: profile memory usage by @Discountchubbs
|
||||
* [TIZI/TICI] ui: remove per-frame param sync by @sunnyhaibin
|
||||
* [MICI] ui: always offroad by @nayan8teen
|
||||
* controls: always default Torque Lateral Control to v0 Tune by @sunnyhaibin
|
||||
* Revert "controls: always default Torque Lateral Control to v0 Tune" by @sunnyhaibin
|
||||
* Reapply "controls: always default Torque Lateral Control to v0 Tune" (#1806) by @sunnyhaibin
|
||||
* [MICI] ui: add sunnylink info & connectivity check by @nayan8teen
|
||||
* sunnylink: Remove unused API endpoint by @devtekve
|
||||
* DM: wheel touch enforcement in MADS by @sunnyhaibin
|
||||
* torque: show static override values in Dev UI & gate `useParams` on custom torque tune by @sunnyhaibin
|
||||
* MADS: suppress espActive event when long is not engaged by @sunnyhaibin
|
||||
* sunnylink: SDUI by @sunnyhaibin
|
||||
* [MICI] ui: align upstream changes with sunnypilot settings buttons by @nayan8teen
|
||||
* ui: fix cellular toggles by @AmyJeanes
|
||||
* sunnylink: switch athena domain by @devtekve
|
||||
* Platform List: dynamically migrate CarPlatformBundle by @sunnyhaibin
|
||||
* What's Changed (sunnypilot/opendbc)
|
||||
* Honda: DBC for Accord 9th Generation by @mvl-boston
|
||||
* FCA: update tire stiffness values for `RAM_HD` by @dparring
|
||||
@@ -178,25 +84,12 @@ sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
* Honda: add missing `GasInterceptor` messages to Taiwan Odyssey DBC by @mvl-boston
|
||||
* GM: remove `CHEVROLET_EQUINOX_NON_ACC_3RD_GEN` from `dashcamOnly` by @sunnyhaibin
|
||||
* GM: remove `CHEVROLET_BOLT_NON_ACC_2ND_GEN` from `dashcamOnly` by @sunnyhaibin
|
||||
* Hyundai Longitudinal: deprecate ramp update for dynamic tune by @Discountchubbs
|
||||
* Rivian: long upgrade messages on bus 1 by @lukasloetkolben
|
||||
* Toyota: Stop and Go Hack (Alpha) by @sunnyhaibin
|
||||
* Toyota: gate Smart DSU behind Alpha Longitudinal by @sunnyhaibin
|
||||
* Toyota: Gas Interceptor always set `standstill_req` by @sunnyhaibin
|
||||
* MADS safety: dedicated `controls_allowed_lateral` by @sunnyhaibin
|
||||
* Platform List: include community supported platforms by @sunnyhaibin
|
||||
* New Contributors (sunnypilot/sunnypilot)
|
||||
* @TheSecurityDev made their first contribution in "ui: fix sidebar scroll in UI screenshots"
|
||||
* @zikeji made their first contribution in "sunnylink: block remote modification of SSH key parameters"
|
||||
* @Candy0707 made their first contribution in "[TIZI/TICI] ui: Fix misaligned turn signals and blindspot indicators with sidebar"
|
||||
* @CHaucke89 made their first contribution in "Pause Lateral Control with Blinker: Post-Blinker Delay"
|
||||
* @yasu-oh made their first contribution in "SCC-V: Use p97 for predicted lateral accel"
|
||||
* @angaz made their first contribution in "[TIZI/TICI] visuals: Improved speed limit"
|
||||
* @jamesmikesell made their first contribution in "ICBM: ensure button timers update on disable to clear stale presses"
|
||||
* @zephleggett made their first contribution in "controlsd: fix steer_limited_by_safety not updating under MADS"
|
||||
* @lukasloetkolben made their first contribution in "pandad: flasher for Rivian long upgrade module"
|
||||
* @didlawowo made their first contribution in "i18n(fr): Add French translations"
|
||||
* @mmmorks made their first contribution in "NNLC: restore pre-v1 PID gains in torque extension"
|
||||
* New Contributors (sunnypilot/opendbc)
|
||||
* @AmyJeanes made their first contribution in "Tesla: Fix stock LKAS being blocked when MADS is enabled"
|
||||
* @mvl-boston made their first contribution in "Honda: Update Clarity brake to renamed DBC message name"
|
||||
@@ -206,20 +99,6 @@ sunnypilot Version 2026.001.000 (2026-05-06)
|
||||
* @royjr made their first contribution in "HKG: add KIA_FORTE_2019_NON_SCC fingerprint"
|
||||
* @ssysm made their first contribution in "Tesla: remove `TESLA_MODEL_X` from `dashcamOnly`"
|
||||
* Full Changelog: https://github.com/sunnypilot/sunnypilot/compare/v2025.002.000...v2026.001.000
|
||||
************************
|
||||
* Synced with commaai's openpilot (v0.11.1)
|
||||
* master commit c001f3c9b490a80e69539f0af6022f6e07ceb721 (April 16, 2026)
|
||||
* New driver monitoring model
|
||||
* Improved image processing pipeline for driver camera
|
||||
* Rivian R1S and R1T 2025 support thanks to lukasloetkolben!
|
||||
* New driving model #36798
|
||||
* Fully trained using a learned simulator
|
||||
* Improved longitudinal performance in Experimental mode
|
||||
* Reduce comma four standby power usage by 77% to 52 mW
|
||||
* Kia K7 2017 support thanks to royjr!
|
||||
* Lexus LS 2018 support thanks to Hacheoy!
|
||||
* Improved inter-process communication memory efficiency
|
||||
* comma four support
|
||||
|
||||
sunnypilot Version 2025.002.000 (2025-11-06)
|
||||
========================
|
||||
|
||||
+9
-7
@@ -558,8 +558,8 @@ struct PandaState @0xa7649e2575e4591e {
|
||||
|
||||
# these fields are not used by openpilot, but they're
|
||||
# reserved for forks building alternate experiences.
|
||||
controlsAllowedLateral @38 :Bool;
|
||||
controlsAllowedLongitudinal @39 :Bool;
|
||||
controlsAllowedRESERVED1 @38 :Bool;
|
||||
controlsAllowedRESERVED2 @39 :Bool;
|
||||
|
||||
enum FaultStatus {
|
||||
none @0;
|
||||
@@ -2054,14 +2054,16 @@ struct DriverStateV2 {
|
||||
facePosition @2 :List(Float32);
|
||||
facePositionStd @3 :List(Float32);
|
||||
faceProb @4 :Float32;
|
||||
leftEyeProb @5 :Float32;
|
||||
rightEyeProb @6 :Float32;
|
||||
leftBlinkProb @7 :Float32;
|
||||
rightBlinkProb @8 :Float32;
|
||||
sunglassesProb @9 :Float32;
|
||||
eyesVisibleProb @14 :Float32;
|
||||
eyesClosedProb @15 :Float32;
|
||||
phoneProb @13 :Float32;
|
||||
|
||||
deprecated :group {
|
||||
leftEyeProb @5 :Float32;
|
||||
rightEyeProb @6 :Float32;
|
||||
leftBlinkProb @7 :Float32;
|
||||
rightBlinkProb @8 :Float32;
|
||||
sunglassesProb @9 :Float32;
|
||||
notReadyProb @12 :List(Float32);
|
||||
occludedProb @10 :Float32;
|
||||
readyProb @11 :List(Float32);
|
||||
|
||||
@@ -1,256 +1,222 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Schema-level cereal compat check between sunnypilot and upstream openpilot.
|
||||
|
||||
Rules (per struct matched across sides by typeId):
|
||||
R1 shared ordinal must reference the same type.
|
||||
R2 sunnypilot-only ordinal in a union -> FAIL (unknown discriminant upstream).
|
||||
R3 sunnypilot-only ordinal on a regular field -> OK (additive struct evolution).
|
||||
R4 upstream-only ordinal -> OK.
|
||||
R5 sunnypilot-only struct referenced via an upstream-shared field -> FAIL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
NO_DISCRIMINANT = 0xFFFF
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def hex_id(value: int) -> str:
|
||||
return f"0x{value:016x}"
|
||||
def print_debug(string: str) -> None:
|
||||
if DEBUG:
|
||||
print(string)
|
||||
|
||||
|
||||
def encode_type(type_node: Any) -> dict:
|
||||
which = type_node.which()
|
||||
if which == "struct":
|
||||
return {"kind": "struct", "typeId": hex_id(type_node.struct.typeId)}
|
||||
if which == "enum":
|
||||
return {"kind": "enum", "typeId": hex_id(type_node.enum.typeId)}
|
||||
if which == "interface":
|
||||
return {"kind": "interface", "typeId": hex_id(type_node.interface.typeId)}
|
||||
if which == "list":
|
||||
return {"kind": "list", "element": encode_type(type_node.list.elementType)}
|
||||
if which == "anyPointer":
|
||||
return {"kind": "anyPointer"}
|
||||
return {"kind": which}
|
||||
def create_schema_instance(struct: Any, prop: Tuple[str, Any]) -> Any:
|
||||
"""
|
||||
Create a new instance of a schema type, handling different field types.
|
||||
|
||||
Args:
|
||||
struct: The Cap'n Proto schema structure
|
||||
prop: A tuple containing the field name and field metadata
|
||||
|
||||
Returns:
|
||||
A new initialized schema instance
|
||||
"""
|
||||
struct_instance = struct.new_message()
|
||||
field_name, field_metadata = prop
|
||||
|
||||
try:
|
||||
field_type = field_metadata.proto.slot.type.which()
|
||||
|
||||
# Initialize different types of fields
|
||||
if field_type in ('list', 'text', 'data'):
|
||||
struct_instance.init(field_name, 1)
|
||||
print_debug(f"Initialized list/text/data field: {field_name}")
|
||||
elif field_type in ('struct', 'object'):
|
||||
struct_instance.init(field_name)
|
||||
print_debug(f"Initialized struct/object field: {field_name}")
|
||||
|
||||
return struct_instance
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error creating instance for {field_name}: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def encode_field(name: str, field: Any) -> dict:
|
||||
proto = field.proto
|
||||
ordinal = proto.ordinal.explicit if proto.ordinal.which() == "explicit" else None
|
||||
discriminant = proto.discriminantValue if proto.discriminantValue != NO_DISCRIMINANT else None
|
||||
def get_schema_fields(schema_struct: Any) -> List[Tuple[str, Any]]:
|
||||
"""
|
||||
Retrieve all fields from a given schema structure.
|
||||
|
||||
if proto.which() == "group":
|
||||
type_desc = {"kind": "group", "typeId": hex_id(proto.group.typeId)}
|
||||
else:
|
||||
type_desc = encode_type(proto.slot.type)
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"ordinal": ordinal,
|
||||
"discriminant": discriminant,
|
||||
"type": type_desc,
|
||||
}
|
||||
Returns:
|
||||
A list of field names and their metadata
|
||||
"""
|
||||
try:
|
||||
# Get all fields from the schema
|
||||
schema_fields = list(schema_struct.schema.fields.items())
|
||||
|
||||
print_debug("Discovered schema fields:")
|
||||
for field_name, field_metadata in schema_fields:
|
||||
print_debug(f"- {field_name}")
|
||||
|
||||
return schema_fields
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving schema fields: {e}")
|
||||
return []
|
||||
|
||||
|
||||
def encode_struct(schema: Any) -> dict:
|
||||
node = schema.node
|
||||
return {
|
||||
"typeId": hex_id(node.id),
|
||||
"displayName": node.displayName,
|
||||
"hasUnion": node.struct.discriminantCount > 0,
|
||||
"fields": [encode_field(name, field) for name, field in schema.fields.items()],
|
||||
}
|
||||
def generate_schema_instances(schema_struct: Any) -> List[Any]:
|
||||
"""
|
||||
Generate instances for all fields in a given schema.
|
||||
|
||||
Args:
|
||||
schema_struct: The Cap'n Proto schema structure
|
||||
|
||||
Returns:
|
||||
A list of schema instances
|
||||
"""
|
||||
schema_fields = get_schema_fields(schema_struct)
|
||||
instances = []
|
||||
|
||||
for field_prop in schema_fields:
|
||||
try:
|
||||
instance = create_schema_instance(schema_struct, field_prop)
|
||||
if instance is not None:
|
||||
instances.append(instance)
|
||||
except Exception as e:
|
||||
print(f"Skipping field due to error: {e}")
|
||||
|
||||
print(f"Generated {len(instances)} schema instances")
|
||||
return instances
|
||||
|
||||
|
||||
def _child_struct_schema(field: Any) -> Any:
|
||||
proto = field.proto
|
||||
if proto.which() == "group":
|
||||
return field.schema
|
||||
type_node = proto.slot.type
|
||||
which = type_node.which()
|
||||
if which == "struct":
|
||||
return field.schema
|
||||
if which == "list":
|
||||
container = field.schema
|
||||
element_type = type_node.list.elementType
|
||||
while element_type.which() == "list":
|
||||
container = container.elementType
|
||||
element_type = element_type.list.elementType
|
||||
if element_type.which() == "struct":
|
||||
return container.elementType
|
||||
return None
|
||||
def persist_instances(instances: List[Any], filename: str) -> None:
|
||||
"""
|
||||
Write schema instances to a binary file.
|
||||
|
||||
Args:
|
||||
instances: List of schema instances
|
||||
filename: Output file path
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'wb') as f:
|
||||
for instance in instances:
|
||||
f.write(instance.to_bytes())
|
||||
|
||||
print(f"Successfully wrote {len(instances)} instances to {filename}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error persisting instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def collect_schema(root: Any) -> dict[str, dict]:
|
||||
structs: dict[str, dict] = {}
|
||||
stack = [root]
|
||||
while stack:
|
||||
schema = stack.pop()
|
||||
type_id = hex_id(schema.node.id)
|
||||
if type_id in structs:
|
||||
continue
|
||||
structs[type_id] = encode_struct(schema)
|
||||
for _name, field in schema.fields.items():
|
||||
try:
|
||||
child = _child_struct_schema(field)
|
||||
except Exception:
|
||||
child = None
|
||||
if child is not None:
|
||||
stack.append(child)
|
||||
return structs
|
||||
def read_instances(filename: str, schema_type: Any) -> List[Any]:
|
||||
"""
|
||||
Read schema instances from a binary file.
|
||||
|
||||
Args:
|
||||
filename: Input file path
|
||||
schema_type: The schema type to use for reading
|
||||
|
||||
Returns:
|
||||
A list of read schema instances
|
||||
"""
|
||||
try:
|
||||
with open(filename, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
instances = list(schema_type.read_multiple_bytes(data))
|
||||
|
||||
print(f"Read {len(instances)} instances from {filename}")
|
||||
return instances
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error reading instances: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def load_log(cereal_dir: str) -> Any:
|
||||
import capnp
|
||||
cereal_dir = os.path.abspath(cereal_dir)
|
||||
capnp.remove_import_hook()
|
||||
return capnp.load(os.path.join(cereal_dir, "log.capnp"), imports=[cereal_dir])
|
||||
def compare_schemas(original_instances: List[Any], read_instances: List[Any]) -> bool:
|
||||
"""
|
||||
Compare original and read-back instances to detect potential breaking changes.
|
||||
|
||||
Args:
|
||||
original_instances: List of originally generated instances
|
||||
read_instances: List of instances read back from file
|
||||
|
||||
def dump_schema(cereal_dir: str, path: str) -> None:
|
||||
log = load_log(cereal_dir)
|
||||
payload = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
}
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
json.dump(payload, handle, indent=2, sort_keys=True)
|
||||
print(f"wrote schema dump with {len(payload['structs'])} structs to {path}")
|
||||
|
||||
|
||||
def types_equal(a: dict, b: dict) -> bool:
|
||||
if a.get("kind") != b.get("kind"):
|
||||
Returns:
|
||||
Boolean indicating whether schemas appear compatible
|
||||
"""
|
||||
if len(original_instances) != len(read_instances):
|
||||
print("❌ Schema Compatibility Warning: Instance count mismatch")
|
||||
return False
|
||||
kind = a["kind"]
|
||||
if kind in ("struct", "enum", "interface", "group"):
|
||||
return a.get("typeId") == b.get("typeId")
|
||||
if kind == "list":
|
||||
return types_equal(a["element"], b["element"])
|
||||
return True
|
||||
|
||||
compatible = True
|
||||
for struct in read_instances:
|
||||
try:
|
||||
getattr(struct, struct.which()) # Attempting to access the field to validate readability
|
||||
except Exception as e:
|
||||
print(f"❌ Structural change detected: {struct.which()} is not readable.\nFull error: {e}")
|
||||
compatible = False
|
||||
|
||||
return compatible
|
||||
|
||||
|
||||
def type_repr(t: dict) -> str:
|
||||
kind = t.get("kind", "?")
|
||||
if kind in ("struct", "enum", "interface", "group"):
|
||||
return f"{kind}({t.get('typeId')})"
|
||||
if kind == "list":
|
||||
return f"list<{type_repr(t['element'])}>"
|
||||
return kind
|
||||
|
||||
|
||||
def field_is_union_variant(field: dict) -> bool:
|
||||
return field.get("discriminant") is not None
|
||||
|
||||
|
||||
def index_fields_by_ordinal(struct: dict) -> dict[int, dict]:
|
||||
indexed: dict[int, dict] = {}
|
||||
for field in struct["fields"]:
|
||||
ordinal = field.get("ordinal")
|
||||
if ordinal is None:
|
||||
continue
|
||||
indexed[ordinal] = field
|
||||
return indexed
|
||||
|
||||
|
||||
def compare(sunnypilot_dump: dict, upstream_dump: dict) -> list[str]:
|
||||
violations: list[str] = []
|
||||
sunnypilot_structs: dict[str, dict] = sunnypilot_dump["structs"]
|
||||
upstream_structs: dict[str, dict] = upstream_dump["structs"]
|
||||
|
||||
sunnypilot_struct_referenced_from_shared: set[str] = set()
|
||||
|
||||
for type_id, sunnypilot_struct in sunnypilot_structs.items():
|
||||
upstream_struct = upstream_structs.get(type_id)
|
||||
if upstream_struct is None:
|
||||
continue
|
||||
|
||||
sunnypilot_fields = index_fields_by_ordinal(sunnypilot_struct)
|
||||
upstream_fields = index_fields_by_ordinal(upstream_struct)
|
||||
display = sunnypilot_struct["displayName"]
|
||||
|
||||
for ordinal, sunnypilot_field in sunnypilot_fields.items():
|
||||
upstream_field = upstream_fields.get(ordinal)
|
||||
if upstream_field is None:
|
||||
if field_is_union_variant(sunnypilot_field):
|
||||
violations.append(
|
||||
f"[R2] {display} @{ordinal} ('{sunnypilot_field['name']}', {type_repr(sunnypilot_field['type'])}): "
|
||||
f"union variant not present upstream. upstream cannot parse this discriminant."
|
||||
)
|
||||
continue
|
||||
|
||||
if not types_equal(sunnypilot_field["type"], upstream_field["type"]):
|
||||
violations.append(
|
||||
f"[R1] {display} @{ordinal}: type mismatch. "
|
||||
f"sunnypilot='{sunnypilot_field['name']}' {type_repr(sunnypilot_field['type'])} vs "
|
||||
f"upstream='{upstream_field['name']}' {type_repr(upstream_field['type'])}."
|
||||
)
|
||||
continue
|
||||
|
||||
cursor = sunnypilot_field["type"]
|
||||
while cursor.get("kind") == "list":
|
||||
cursor = cursor["element"]
|
||||
if cursor.get("kind") in ("struct", "group", "interface") and cursor.get("typeId"):
|
||||
sunnypilot_struct_referenced_from_shared.add(cursor["typeId"])
|
||||
|
||||
for type_id, sunnypilot_struct in sunnypilot_structs.items():
|
||||
if type_id in upstream_structs:
|
||||
continue
|
||||
if type_id in sunnypilot_struct_referenced_from_shared:
|
||||
violations.append(
|
||||
f"[R5] struct {sunnypilot_struct['displayName']} ({type_id}) exists only on sunnypilot "
|
||||
f"but is referenced from an upstream-shared field. upstream cannot resolve this type."
|
||||
)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def load_peer(path: str) -> dict:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
|
||||
|
||||
def run_read(cereal_dir: str, peer_path: str) -> int:
|
||||
log = load_log(cereal_dir)
|
||||
peer_dump = load_peer(peer_path)
|
||||
local_dump = {
|
||||
"root": hex_id(log.Event.schema.node.id),
|
||||
"structs": collect_schema(log.Event.schema),
|
||||
}
|
||||
violations = compare(sunnypilot_dump=peer_dump, upstream_dump=local_dump)
|
||||
|
||||
if not violations:
|
||||
print("cereal compat OK: upstream openpilot can parse sunnypilot routes "
|
||||
"(no leaked structs, no ordinal collisions).")
|
||||
return 0
|
||||
|
||||
print(f"cereal compat FAIL: upstream openpilot would misparse sunnypilot routes "
|
||||
f"({len(violations)} violation(s)):")
|
||||
for v in violations:
|
||||
print(f" {v}")
|
||||
return 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
def main():
|
||||
"""
|
||||
CLI entry point for schema compatibility testing.
|
||||
"""
|
||||
# Setup argument parser
|
||||
parser = argparse.ArgumentParser(
|
||||
description="sunnypilot <-> upstream cereal compatibility validator (schema-level)."
|
||||
description='Cap\'n Proto Schema Compatibility Testing Tool',
|
||||
epilog='Test schema compatibility by generating and reading back instances.'
|
||||
)
|
||||
mode = parser.add_mutually_exclusive_group(required=True)
|
||||
mode.add_argument("-g", "--generate", action="store_true", help="dump local schema to JSON")
|
||||
mode.add_argument("-r", "--read", action="store_true", help="load peer JSON and diff against local")
|
||||
parser.add_argument("-f", "--file", default="schema.json", help="JSON file path (default: schema.json)")
|
||||
parser.add_argument("--cereal-dir", required=True, help="path to cereal directory containing log.capnp")
|
||||
|
||||
# Add mutually exclusive group for generation or reading mode
|
||||
mode_group = parser.add_mutually_exclusive_group(required=True)
|
||||
mode_group.add_argument('-g', '--generate', action='store_true',
|
||||
help='Generate schema instances')
|
||||
mode_group.add_argument('-r', '--read', action='store_true',
|
||||
help='Read and validate schema instances')
|
||||
|
||||
# Common arguments
|
||||
parser.add_argument('-f', '--file',
|
||||
default='schema_instances.bin',
|
||||
help='Output/input binary file (default: schema_instances.bin)')
|
||||
|
||||
# Parse arguments
|
||||
args = parser.parse_args()
|
||||
|
||||
# Import the schema dynamically
|
||||
try:
|
||||
from cereal import log
|
||||
schema_type = log.Event
|
||||
except ImportError:
|
||||
print("Error: Unable to import schema. Ensure 'cereal' is installed.")
|
||||
sys.exit(1)
|
||||
|
||||
# Execute based on mode
|
||||
if args.generate:
|
||||
dump_schema(args.cereal_dir, args.file)
|
||||
return 0
|
||||
return run_read(args.cereal_dir, args.file)
|
||||
print("🔧 Generating Schema Instances")
|
||||
instances = generate_schema_instances(schema_type)
|
||||
persist_instances(instances, args.file)
|
||||
print("✅ Instance generation complete")
|
||||
|
||||
elif args.read:
|
||||
print("🔍 Reading and Validating Schema Instances")
|
||||
generated_instances = generate_schema_instances(schema_type)
|
||||
read_back_instances = read_instances(args.file, schema_type)
|
||||
|
||||
# Compare schemas
|
||||
if compare_schemas(generated_instances, read_back_instances):
|
||||
print("✅ Schema Compatibility: No breaking changes detected")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("❌ Potential Schema Breaking Changes Detected")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
main()
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
#define DEFAULT_MODEL "CD210 (Default)"
|
||||
#define DEFAULT_MODEL "OP Model 7 (Default)"
|
||||
|
||||
@@ -179,6 +179,8 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverEnabled", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ScreenSaverTimeout", {PERSISTENT | BACKUP, INT, "300"}},
|
||||
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"ShowTurnSignals", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
{"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}},
|
||||
@@ -204,7 +206,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
// sunnylink params
|
||||
{"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}},
|
||||
{"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}},
|
||||
{"ParamsVersion", {PERSISTENT, INT}},
|
||||
{"SunnylinkCache_Roles", {PERSISTENT, STRING}},
|
||||
{"SunnylinkCache_Users", {PERSISTENT, STRING}},
|
||||
{"SunnylinkDongleId", {PERSISTENT, STRING}},
|
||||
|
||||
+67
-67
@@ -22,7 +22,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Audi[<sup>11</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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"></img></a>||
|
||||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EV 2022-23">Buy Here</a></sub></details>|||
|
||||
|Chevrolet|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|Silverado 1500 2020-21|Safety Package II|openpilot available[<sup>1</sup>](#footnotes)|0 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 Silverado 1500 2020-21">Buy Here</a></sub></details>|||
|
||||
@@ -32,34 +32,34 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Chrysler|Pacifica 2021-23|All|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 2021-23">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>|||
|
||||
|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"></img></a>||
|
||||
|CUPRA[<sup>11</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=CUPRA Ateca 2018-23">Buy Here</a></sub></details>|||
|
||||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 FCA connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Dodge Durango 2020-21">Buy Here</a></sub></details>|||
|
||||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Bronco Sport 2021-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Escape Hybrid 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 Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Escape Plug-in Hybrid 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 Plug-in Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Escape Plug-in Hybrid 2023-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Expedition 2022-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Explorer Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford F-150 Hybrid 2021-23">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=MewJc9LYp9M" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Focus 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Focus Hybrid 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Focus Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Kuga Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Kuga Plug-in Hybrid 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Maverick 2022|LARIAT Luxury|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 Maverick 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick 2023-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 Maverick 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|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 Maverick Hybrid 2022">Buy Here</a></sub></details>|||
|
||||
|Ford|Maverick Hybrid 2023-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 Maverick Hybrid 2023-24">Buy Here</a></sub></details>|||
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Mustang Mach-E 2021-24">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q4 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ford Ranger 2024">Buy Here</a></sub></details>||<a href="https://www.youtube.com/watch?v=uUGkH6C_EQU" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|Genesis|G70 2018|All|Stock|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=Genesis G70 2018">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2019-21|All|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=Genesis G70 2019-21">Buy Here</a></sub></details>|||
|
||||
|Genesis|G70 2022-23|All|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=Genesis G70 2022-23">Buy Here</a></sub></details>|||
|
||||
@@ -74,18 +74,18 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Genesis|GV70 Electrified (Australia Only) 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q 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=Genesis GV70 Electrified (Australia Only) 2022">Buy Here</a></sub></details>|||
|
||||
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q 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=Genesis GV70 Electrified (with HDA II) 2023-24">Buy Here</a></sub></details>|||
|
||||
|Genesis|GV80 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai M 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=Genesis GV80 2023">Buy Here</a></sub></details>|||
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 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=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 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=GMC Sierra 1500 2020-21">Buy Here</a></sub></details>|<a href="https://youtu.be/5HbNoBLzRwE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2018-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=mrUwlj3Mi58" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2018-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2018-22">Buy Here</a></sub></details>|||
|
||||
|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Accord Hybrid 2023-25">Buy Here</a></sub></details>|||
|
||||
|Honda|City (Brazil only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|14 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda City (Brazil only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2016-18">Buy Here</a></sub></details>|<a href="https://youtu.be/-IkImTe1NYE" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|2 mph[<sup>4</sup>](#footnotes)|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=4Iz1Mz5LGF8" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2017-18">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2019-21">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic 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-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback 2022-24">Buy Here</a></sub></details>|<a href="https://youtu.be/ytiOT5lcp6Q" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hatchback Hybrid (Europe only) 2023">Buy Here</a></sub></details>|||
|
||||
|Honda|Civic Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Honda Bosch B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Honda Civic Hybrid 2025-26">Buy Here</a></sub></details>|||
|
||||
@@ -117,9 +117,9 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Custin 2023|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 Custin 2023">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 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=Hyundai Elantra 2017-18">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 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=Hyundai Elantra 2019">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra 2021-23|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=Hyundai Elantra 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Elantra 2021-23|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=Hyundai Elantra 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 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=Hyundai Elantra GT 2017-20">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Elantra Hybrid 2021-23|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=Hyundai Elantra Hybrid 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Elantra Hybrid 2021-23|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=Hyundai Elantra Hybrid 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/_EdYQtV52-c" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai J 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 Genesis 2015-16">Buy Here</a></sub></details>|||
|
||||
|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 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=Hyundai i30 2017-19">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q 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 Ioniq 5 (Southeast Asia and Europe only) 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -136,17 +136,17 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O 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 Kona 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|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=Hyundai Kona Electric 2018-21">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai O 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 Kona Electric 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R 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 Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R 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 Kona Electric (with HDA II, Korea only) 2023">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=U2fOCmcQ8hw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai I 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 Kona Hybrid 2020">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Nexo 2021|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=Hyundai Nexo 2021">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Palisade 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=Hyundai Palisade 2020-22">Buy Here</a></sub></details>|<a href="https://youtu.be/TAnDqjF4fDY?t=456" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Palisade 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=Hyundai Palisade 2020-22">Buy Here</a></sub></details>|<a href="https://youtu.be/TAnDqjF4fDY?t=456" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Cruz 2022-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=Hyundai Santa Cruz 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Santa Fe 2019-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D 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 Santa Fe 2019-20">Buy Here</a></sub></details>|<a href="https://youtu.be/bjDR0YjM__s" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Santa Fe 2021-23|All|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=Hyundai Santa Fe 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/VnHzSTygTS4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Santa Fe 2019-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai D 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 Santa Fe 2019-20">Buy Here</a></sub></details>|<a href="https://youtu.be/bjDR0YjM__s" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Fe 2021-23|All|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=Hyundai Santa Fe 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/VnHzSTygTS4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Santa Fe Hybrid 2022-23|All|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=Hyundai Santa Fe Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|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=Hyundai Santa Fe Plug-in Hybrid 2022-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Sonata 2018-19|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=Hyundai Sonata 2018-19">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Sonata 2020-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=Hyundai Sonata 2020-23">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=ix63r9kE3Fw" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Hyundai|Sonata 2020-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=Hyundai Sonata 2020-23">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=ix63r9kE3Fw" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Hyundai|Sonata Hybrid 2020-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=Hyundai Sonata Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Staria 2023|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 Staria 2023">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|19 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=Hyundai Tucson 2021">Buy Here</a></sub></details>|||
|
||||
@@ -156,8 +156,8 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Hyundai|Tucson Hybrid 2022-24|All|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=Hyundai Tucson Hybrid 2022-24">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Tucson Plug-in Hybrid 2024|All|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=Hyundai Tucson Plug-in Hybrid 2024">Buy Here</a></sub></details>|||
|
||||
|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 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=Hyundai Veloster 2019-20">Buy Here</a></sub></details>|||
|
||||
|Jeep|Grand Cherokee 2016-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=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Jeep|Grand Cherokee 2019-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=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Jeep|Grand Cherokee 2016-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=Jeep Grand Cherokee 2016-18">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=eLR9o2JkuRk" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Jeep|Grand Cherokee 2019-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=Jeep Grand Cherokee 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=jBe4lWnRSu4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|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|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>|||
|
||||
@@ -170,10 +170,10 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Kia|K5 Hybrid 2020-22|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 K5 Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Kia|K7 2017|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 K7 2017">Buy Here</a></sub></details>|||
|
||||
|Kia|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai Q 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 K8 Hybrid (with HDA II) 2023">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro EV 2019|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 Niro EV 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2020|All|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 EV 2020">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2021|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=Kia Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2022|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 Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Kia|Niro EV 2019|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 Niro EV 2019">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2020|All|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 EV 2020">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2021|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=Kia Niro EV 2021">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV 2022|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 Niro EV 2022">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=lT7zcG6ZpGo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Kia|Niro EV (with HDA II) 2024-25|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai R 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 EV (with HDA II) 2024-25">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro EV (without HDA II) 2023-25|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 Niro EV (without HDA II) 2023-25">Buy Here</a></sub></details>|||
|
||||
|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 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 Niro Hybrid 2018">Buy Here</a></sub></details>|||
|
||||
@@ -188,21 +188,21 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|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)|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|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 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"></img></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"></img></a>||
|
||||
|Kia|Sorento 2021-23|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 Sorento 2021-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Sorento Hybrid 2021-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 Hybrid 2021-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 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|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"></img></a>||
|
||||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai K connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2022-23">Buy Here</a></sub></details>|||
|
||||
|Kia|Telluride 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Hyundai H connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Telluride 2020-22">Buy Here</a></sub></details>|||
|
||||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus CT Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES 2019-25">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2017-18">Buy Here</a></sub></details>|||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus ES Hybrid 2019-25">Buy Here</a></sub></details>|<a href="https://youtu.be/BZ29osRVJeg?t=12" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus GS F 2016">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2017-19">Buy Here</a></sub></details>|||
|
||||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lexus IS 2022-24">Buy Here</a></sub></details>|||
|
||||
@@ -223,25 +223,25 @@ 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>|||
|
||||
|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator 2020-24">Buy Here</a></sub></details>|||
|
||||
|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Ford Q3 connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Lincoln Aviator Plug-in Hybrid 2020-24">Buy Here</a></sub></details>|||
|
||||
|MAN[<sup>11</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN eTGE 2020-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|MAN[<sup>11</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-5 2022-25">Buy Here</a></sub></details>|||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan B connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Altima 2019-20, 2024">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 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>5</sup>](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Leaf 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/vaMbtAh_0cY" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan Rogue 2018-20">Buy Here</a></sub></details>|||
|
||||
|Nissan[<sup>5</sup>](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 Nissan A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Nissan X-Trail 2017">Buy Here</a></sub></details>|||
|
||||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 1500 2019-24">Buy Here</a></sub></details>|||
|
||||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 2500 2020-24">Buy Here</a></sub></details>|||
|
||||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Ram connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Ram 3500 2019-22">Buy Here</a></sub></details>|||
|
||||
|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1S 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|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/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></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|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/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>|
|
||||
|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Rivian A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Rivian R1T 2022-24">Buy Here</a></sub></details>||<a href="https://youtu.be/uaISd1j7Z4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>|
|
||||
|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>|||
|
||||
|SEAT[<sup>11</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=SEAT Leon 2014-20">Buy Here</a></sub></details>|||
|
||||
|Subaru|Ascent 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Ascent 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2020-23">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2017-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Forester 2019-21|All[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Forester 2019-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
@@ -252,7 +252,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2015-17">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|Outback 2020-22|All[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru B connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Outback 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Subaru|XV 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2020-21">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|
||||
|Škoda|Fabia 2022-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Fabia 2022-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
|Škoda|Kamiq 2021-23[<sup>12,14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Škoda Kamiq 2021-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|
||||
@@ -279,50 +279,50 @@ 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 Hybrid 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2017-20">Buy Here</a></sub></details>|||
|
||||
|Toyota|C-HR Hybrid 2021-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota C-HR Hybrid 2021-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Camry 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>10</sup>](#footnotes)|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></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"></img></a>||
|
||||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2021-24">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla 2020-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross (Non-US only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Cross Hybrid (Non-US only) 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hatchback 2019-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=_66pXk0CBYA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid 2020-22">Buy Here</a></sub></details>|||
|
||||
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Corolla Hybrid (South America only) 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2017-19">Buy Here</a></sub></details>|||
|
||||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander Hybrid 2020-23">Buy Here</a></sub></details>|||
|
||||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Mirai 2021">Buy Here</a></sub></details>|||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2016">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2017-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=8zopPJI8XQ0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius Prime 2021-22">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=J58TvCpUd4U" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Prius v 2017">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2016">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2017-18">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2019-21">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=wJxjDd42gGA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2022">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 2023-25">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|
||||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2016">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2019-21">Buy Here</a></sub></details>|||
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></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>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>||
|
||||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2022">Buy Here</a></sub></details>|<a href="https://youtu.be/U0nH9cnrFB0" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2023-25">Buy Here</a></sub></details>|<a href="https://youtu.be/4eIsEq4L4Ng" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Sienna 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=q1UPOo4Sh68" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon 2018-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon eHybrid 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon R 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen CC 2018-22">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Crafter 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen e-Crafter 2018-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>|||
|
||||
@@ -331,7 +331,7 @@ A supported vehicle is one that just works when you install a comma device. All
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Grand California 2019-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|
||||
|Volkswagen[<sup>11</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>11</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Jetta GLI 2021-23">Buy Here</a></sub></details>|||
|
||||
|Volkswagen|Passat 2015-22[<sup>13</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[](##)|[](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 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>|||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# Docs development
|
||||
|
||||
The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
docs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
docs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://zensical.org/docs/
|
||||
@@ -0,0 +1,26 @@
|
||||
# openpilot docs
|
||||
|
||||
This is the source for [docs.comma.ai](https://docs.comma.ai).
|
||||
The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
|
||||
|
||||
## Development
|
||||
NOTE: Those commands must be run in the root directory of openpilot, **not /docs**
|
||||
|
||||
**1. Install the docs dependencies**
|
||||
``` bash
|
||||
uv pip install .[docs]
|
||||
```
|
||||
|
||||
**2. Build the new site**
|
||||
``` bash
|
||||
mkdocs build
|
||||
```
|
||||
|
||||
**3. Run the new site locally**
|
||||
``` bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
References:
|
||||
* https://www.mkdocs.org/getting-started/
|
||||
* https://github.com/ntno/mkdocs-terminal
|
||||
@@ -1 +0,0 @@
|
||||
../../selfdrive/assets/icons_mici/settings/comma_icon.png
|
||||
@@ -0,0 +1,5 @@
|
||||
# Developing a car brand port
|
||||
|
||||
A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
|
||||
@@ -0,0 +1,65 @@
|
||||
# CarState signals
|
||||
|
||||
## Required for basic lateral control
|
||||
|
||||
* `brakePressed`
|
||||
* `cruiseState`
|
||||
* `doorOpen`
|
||||
* `espDisabled`
|
||||
* `gasPressed`
|
||||
* `gearShifter`
|
||||
* `leftBlinker` / `rightBlinker`
|
||||
* `seatbeltUnlatched`
|
||||
* `standstill`
|
||||
* `steeringAngleDeg`
|
||||
* `steeringPressed`
|
||||
* `steeringTorque`
|
||||
* `steerFaultPermanent`
|
||||
* `steerFaultTemporary`
|
||||
* `vCruise`
|
||||
* `wheelSpeeds.[fl|fr|rl|rr]`: Speed of each of the car's four wheels, in m/s. The car's CAN bus often broadcasts the
|
||||
speed in kph, so the helper function `parse_wheel_speeds` performs this conversion by default.
|
||||
|
||||
## Recommended / Required for openpilot longitudinal control
|
||||
|
||||
* `accFaulted`
|
||||
* `espActive`
|
||||
* `parkingBrake`
|
||||
|
||||
## Application Dependent
|
||||
|
||||
* `blockPcmEnable`
|
||||
* `buttonEnable`
|
||||
* `brakeHoldActive`
|
||||
* `carFaultedNonCritical`
|
||||
* `invalidLkasSetting`
|
||||
* `lowSpeedAlert`
|
||||
* `regenBraking`
|
||||
* `steeringAngleOffsetDeg`
|
||||
* `steeringDisengage`
|
||||
* `steeringTorqueEps`
|
||||
* `stockLkas`
|
||||
* `vCruiseCluster`
|
||||
* `vEgoCluster`
|
||||
* `vehicleSensorsInvalid`
|
||||
|
||||
## Automatically populated
|
||||
|
||||
* `buttonEvents`
|
||||
|
||||
These values are populated automatically by `parse_wheel_speeds`:
|
||||
|
||||
* `aEgo`: Acceleration of the ego vehicle, Kalman filtered derivative of `vEgo`.
|
||||
* `vEgo`: Speed of the ego vehicle, Kalman filtered from `vEgoRaw`.
|
||||
* `vEgoRaw`: Speed of the ego vehicle, based on the average of all four wheel speeds, unfiltered.
|
||||
|
||||
## Optional
|
||||
|
||||
* `brake`
|
||||
* `charging`
|
||||
* `fuelGauge`
|
||||
* `leftBlindspot` / `rightBlindspot`
|
||||
* `steeringRateDeg`
|
||||
* `stockAeb`
|
||||
* `stockFcw`
|
||||
* `yawRate`
|
||||
@@ -0,0 +1,5 @@
|
||||
# Developing a car model port
|
||||
|
||||
A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
|
||||
@@ -0,0 +1,85 @@
|
||||
# Stimulus-Response Tests
|
||||
|
||||
These are example test drives that can help identify the CAN bus messaging necessary for ADAS control. Each scripted
|
||||
test should be done in a separate route (ignition cycle). These tests are a guide, not necessarily exhaustive.
|
||||
|
||||
While testing, constant power to the comma device is highly recommended, using [comma power](https://comma.ai/shop/comma-power) if
|
||||
necessary to make sure all test activity is fully captured and for ease of uploading. If constant power isn't
|
||||
available, keep the ignition on for at least one minute after your test to make sure power loss doesn't result
|
||||
in loss of the last minute of testing data.
|
||||
|
||||
## Stationary ignition-only tests, part 1
|
||||
|
||||
1. Ignition on, but don't start engine, remain in Park
|
||||
2. Open and close each door in a defined order: driver, passenger, rear left, rear right
|
||||
3. Re-enter the vehicle, close the driver's door, and fasten the driver's seatbelt
|
||||
4. Slowly press and release the accelerator pedal 3 times
|
||||
5. Slowly press and release the brake pedal 3 times
|
||||
6. Hold the brake and move the gearshift to reverse, then neutral, then drive, then sport/eco/etc if applicable
|
||||
7. Return to Park, ignition off
|
||||
|
||||
Brake-pressed information may show up in several messages and signals, both as on/off states and as a percentage or
|
||||
pressure. It may reflect a switch on the driver's brake pedal, or a pressure-threshold state, or signals to turn on
|
||||
the rear brake lights. Start by identifying all the potential signals, and confirm while driving with ACC later.
|
||||
|
||||
Locate signals for all four door states if possible, but some cars only expose the driver's door state on the ADAS bus.
|
||||
Driver/passenger door signals may or may not change positions for LHD vs RHD cars. For cars where only the driver's
|
||||
door signal is available, the same signal may follow the driver.
|
||||
|
||||
## Stationary ignition-only tests, part 2
|
||||
|
||||
1. Ignition on, but don't start engine, remain in Park
|
||||
2. Press each ACC button in a defined order: main switch on/off, set, resume, cancel, accel, decel, gap adjust
|
||||
3. Set the left turn signal for about five seconds
|
||||
4. Operate the left turn signal one time in its touch-to-pass mode
|
||||
5. Set the right turn signal for about five seconds
|
||||
6. Operate the right turn signal one time in its touch-to-pass mode
|
||||
7. Set the hazard / emergency indicator switch for about five seconds
|
||||
8. Ignition off
|
||||
|
||||
Your vehicle may have a momentary-press main ACC switch or a physical toggle that remains set. Actual ACC engagement
|
||||
isn't necessary for purposes of detecting the ACC button presses.
|
||||
|
||||
## Steering angle and steering torque tests
|
||||
|
||||
Power steering should be available. On ICE cars, engine RPM may be present.
|
||||
|
||||
1. Ignition on, start engine if applicable, remain in Park
|
||||
2. Rotate the steering wheel as follows, with a few seconds pause between each step
|
||||
* Start as close to exact center as possible
|
||||
* Turn to 45 degrees right and hold
|
||||
* Turn to 90 degrees right and hold
|
||||
* Turn to 180 degrees right and hold
|
||||
* Turn to full lock right and hold, with firm pressure against lock
|
||||
* Release the wheel and allow it to bounce back slightly from lock
|
||||
* Turn to 180 degrees left and hold
|
||||
* Return to center and release
|
||||
3. Ignition off
|
||||
|
||||
Performing the full test to the right, followed by an abbreviated test to the left, helps give additional confirmation
|
||||
of signal scale, and sign/direction for both the steering wheel angle and driver input torque signals.
|
||||
|
||||
## Low speed / parking lot driving tests
|
||||
|
||||
Before this test, drive to a place like an empty parking lot where you are free to drive in a series of curves.
|
||||
|
||||
1. Ignition on, start engine if applicable, prepare to drive
|
||||
2. Slowly (10-20mph at most) drive a figure-8 if possible, or at least one sharp left and one sharp right.
|
||||
3. Come to a complete stop
|
||||
4. When and where safe, drive in reverse for a short distance (10-15 feet)
|
||||
5. Park the car in a safe place, ignition off
|
||||
|
||||
## High speed / highway driving tests
|
||||
|
||||
Select a place and time where you can safely set cruise control at normal travel speeds with little interference from
|
||||
traffic ahead, and safely test the response of your factory lane guidance system.
|
||||
|
||||
1. Ignition on, start engine if applicable, prepare to drive
|
||||
2. When safely able, engage adaptive cruise control below 50 mph
|
||||
3. When safely able, use the ACC buttons to accelerate to 50mph, then 55mph, then 60mph
|
||||
4. Disengage adaptive cruise
|
||||
5. When safely able, allow your factory lane guidance to prevent lane departures, 2-3 times on both the left and right
|
||||
|
||||
The series of setpoints can be adjusted to local traffic regulations, and of course metric units. The specific cruise
|
||||
setpoints are useful for locating the ACC HUD signals later, and confirming their precise scaling. When the car reaches
|
||||
and holds the setpoint, that can also provide additional confirmation of wheel speed scaling.
|
||||
@@ -8,7 +8,7 @@ A car port enables openpilot support on a particular car. Each car model openpil
|
||||
|
||||
# Structure of a car port
|
||||
|
||||
All car-specific code is contained in the [opendbc](https://github.com/commaai/opendbc) project.
|
||||
Virtually all car-specific code is contained in two other repositories: [opendbc](https://github.com/commaai/opendbc) and [panda](https://github.com/commaai/panda).
|
||||
|
||||
## opendbc
|
||||
|
||||
@@ -23,8 +23,8 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b
|
||||
|
||||
## safety
|
||||
|
||||
* `opendbc/safety/modes/[brand].h`: Brand-specific safety logic
|
||||
* `opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
|
||||
* `opendbc_repo/opendbc/safety/modes/[brand].h`: Brand-specific safety logic
|
||||
* `opendbc_repo/opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
|
||||
|
||||
## openpilot
|
||||
|
||||
@@ -32,20 +32,8 @@ For historical reasons, openpilot still contains a small amount of car-specific
|
||||
|
||||
* `selfdrive/car/car_specific.py`: Brand-specific event logic
|
||||
|
||||
# How do I port car?
|
||||
# Overview
|
||||
|
||||
[Jason Young](https://github.com/jyoung8607) gave a talk at COMMA_CON with an overview of the car porting process. The talk is available on YouTube:
|
||||
|
||||
https://www.youtube.com/watch?v=XxPS5TpTUnI
|
||||
|
||||
## Brand Port
|
||||
|
||||
A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
|
||||
|
||||
## Model Port
|
||||
|
||||
A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
|
||||
|
||||
Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
|
||||
@@ -1,3 +1,9 @@
|
||||
# openpilot glossary
|
||||
|
||||
{{GLOSSARY_DEFINITIONS}}
|
||||
* **onroad**: openpilot's system state while ignition is on
|
||||
* **offroad**: openpilot's system state while ignition is off
|
||||
* **route**: a route is a recording of an onroad session
|
||||
* **segment**: routes are split into one minute chunks called segments.
|
||||
* **comma connect**: the web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).
|
||||
* **panda**: this is the secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).
|
||||
* **comma four**: the latest hardware by comma.ai for running openpilot. more info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
|
||||
|
||||
@@ -6,9 +6,9 @@ Check out our [Python library](https://github.com/commaai/openpilot/blob/master/
|
||||
|
||||
For each segment, openpilot records the following log types:
|
||||
|
||||
## rlog.zst
|
||||
## rlog.bz2
|
||||
|
||||
rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/openpilot/blob/master/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages.
|
||||
rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for a list of all the logged services. They're a bzip2 archive of the serialized capnproto messages.
|
||||
|
||||
## {f,e,d}camera.hevc
|
||||
|
||||
@@ -18,10 +18,12 @@ Each camera stream is H.265 encoded and written to its respective file.
|
||||
* `ecamera.hevc` is the wide road camera
|
||||
* `dcamera.hevc` is the driver camera
|
||||
|
||||
## qlog.zst & qcamera.ts
|
||||
## qlog.bz2 & qcamera.ts
|
||||
|
||||
qlogs are a decimated subset of the rlogs. Check out [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for the decimation.
|
||||
|
||||
|
||||
qcameras are H.264 encoded, lower res versions of the fcamera.hevc. The video shown in [comma connect](https://connect.comma.ai/) is from the qcameras.
|
||||
|
||||
qlogs and qcameras are designed to be small enough to upload instantly on slow internet, yet useful enough for most analysis and debugging.
|
||||
|
||||
qlogs and qcameras are designed to be small enough to upload instantly on slow internet and store forever, yet useful enough for most analysis and debugging.
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# How to Give Feedback
|
||||
|
||||
Feedback is one of the highest leverage ways to contribute to openpilot as a user.
|
||||
|
||||
## Driving
|
||||
|
||||
Got feedback about how your car drives?
|
||||
Join the community Discord, then use the form in `#submit-feedback`.
|
||||
|
||||
Before posting feedback, please ensure:
|
||||
|
||||
- **openpilot is up to date** you should be on the latest openpilot release or nightly
|
||||
- **both road-facing cameras have a clear view** your windshield is clean, lenses are clean, etc.
|
||||
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
|
||||
|
||||
## Driver Monitoring
|
||||
|
||||
If you find DM annoying while being perfectly attentive, these are likely false positives and we want to fix them!
|
||||
In general, driver monitoring feedback is very actionable, and we can fix your complaint within a release cycle.
|
||||
|
||||
To post your feedback:
|
||||
|
||||
1. Join the [community Discord](https://discord.comma.ai).
|
||||
2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue.
|
||||
3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera.
|
||||
4. Post the segment in the `#openpilot-experience` channel on Discord with a good description.
|
||||
|
||||
Before posting feedback, please ensure:
|
||||
|
||||
- **openpilot is up to date** you should be on the latest openpilot release or nightly
|
||||
- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
|
||||
- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
|
||||
|
||||
## Other bugs
|
||||
|
||||
Got an issue with something else? Open an issue on our [GitHub issue tracker](https://github.com/commaai/openpilot/issues/new/choose).
|
||||
@@ -7,11 +7,25 @@ This is the roadmap for the next major openpilot releases. Also check out
|
||||
* [Bounties](https://comma.ai/bounties) for paid individual issues
|
||||
* [#current-projects](https://discord.com/channels/469524606043160576/1249579909739708446) in Discord for discussion on work-in-progress projects
|
||||
|
||||
## openpilot 0.10
|
||||
|
||||
openpilot 0.10 will be the first release with a driving policy trained in
|
||||
a [learned simulator](https://youtu.be/EqQNZXqzFSI).
|
||||
|
||||
* Driving model trained in a learned simulator
|
||||
* Always-on driver monitoring (behind a toggle)
|
||||
* GPS removed from the driving stack
|
||||
* 100KB qlogs
|
||||
* `nightly` pushed after 1000 hours of hardware-in-the-loop testing
|
||||
* Car interface code moved into [opendbc](https://github.com/commaai/opendbc)
|
||||
* openpilot on PC for Linux x86, Linux arm64, and Mac (Apple Silicon)
|
||||
|
||||
## openpilot 1.0
|
||||
|
||||
openpilot 1.0 will feature a fully end-to-end driving policy.
|
||||
|
||||
* End-to-end longitudinal control in Chill mode
|
||||
* Automatic Emergency Braking (AEB)
|
||||
* Driver monitoring with sleep detection
|
||||
* Rolling updates/releases pushed out by CI
|
||||
* [panda safety 1.0](https://github.com/orgs/commaai/projects/27)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
[data-tooltip] {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-bottom: 1px dotted black;
|
||||
}
|
||||
|
||||
[data-tooltip] .tooltip-content {
|
||||
width: max-content;
|
||||
max-width: 25em;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background-color: white;
|
||||
color: #404040;
|
||||
box-shadow: 0 4px 14px 0 rgba(0,0,0,.2), 0 0 0 1px rgba(0,0,0,.05);
|
||||
padding: 10px;
|
||||
font: 14px/1.5 Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
|
||||
text-decoration: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.1s, visibility 0s;
|
||||
z-index: 1000;
|
||||
pointer-events: none; /* Prevent accidental interaction */
|
||||
}
|
||||
|
||||
[data-tooltip]:hover .tooltip-content {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
pointer-events: auto; /* Allow interaction when visible */
|
||||
}
|
||||
|
||||
.tooltip-content .tooltip-glossary-link {
|
||||
display: inline-block;
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.tooltip-content .tooltip-glossary-link:hover {
|
||||
color: #0056b3;
|
||||
text-decoration: underline;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
import posixpath
|
||||
import re
|
||||
import tomllib
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
|
||||
from markdown.extensions import Extension
|
||||
from markdown.preprocessors import Preprocessor
|
||||
from markdown.treeprocessors import Treeprocessor
|
||||
|
||||
from zensical.extensions.links import LinksProcessor
|
||||
|
||||
GlossaryTerm = tuple[str, re.Pattern[str], str]
|
||||
|
||||
GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
|
||||
GLOSSARY_PAGE = "concepts/glossary.md"
|
||||
GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
|
||||
|
||||
SKIP_TAGS = {
|
||||
"a",
|
||||
"code",
|
||||
"h1",
|
||||
"h2",
|
||||
"h3",
|
||||
"h4",
|
||||
"h5",
|
||||
"h6",
|
||||
"kbd",
|
||||
"pre",
|
||||
"script",
|
||||
"style",
|
||||
}
|
||||
|
||||
def clean_tooltip(description: str) -> str:
|
||||
text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
|
||||
text = re.sub(r"`([^`]+)`", r"\1", text)
|
||||
text = re.sub(r"[*_~]", "", text)
|
||||
return re.sub(r"\s+", " ", text).strip()
|
||||
|
||||
|
||||
def load_glossary() -> tuple[list[GlossaryTerm], str]:
|
||||
with GLOSSARY_FILE.open("rb") as f:
|
||||
glossary_data = tomllib.load(f).get("glossary", {})
|
||||
|
||||
glossary: list[GlossaryTerm] = []
|
||||
rendered = []
|
||||
for key, value in glossary_data.items():
|
||||
label = str(key).strip().replace("_", " ")
|
||||
description = str(value).strip()
|
||||
if not description:
|
||||
continue
|
||||
|
||||
slug = label.replace(" ", "-").replace("_", "-").lower()
|
||||
glossary.append((slug, re.compile(rf"(?<!\w){re.escape(label)}(?!\w)", re.IGNORECASE), clean_tooltip(description)))
|
||||
rendered.append(f'* <span id="{slug}"></span>**{label}**: {description}')
|
||||
|
||||
return glossary, "\n".join(rendered)
|
||||
|
||||
|
||||
class GlossaryPreprocessor(Preprocessor):
|
||||
def __init__(self, md, glossary: str):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
markdown = "\n".join(lines)
|
||||
if GLOSSARY_PLACEHOLDER not in markdown:
|
||||
return lines
|
||||
return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
|
||||
|
||||
|
||||
class GlossaryTreeprocessor(Treeprocessor):
|
||||
def __init__(self, md, glossary: list[GlossaryTerm]):
|
||||
super().__init__(md)
|
||||
self.glossary = glossary
|
||||
self.seen: set[str] = set()
|
||||
|
||||
def run(self, root: ET.Element) -> None:
|
||||
at = self.md.treeprocessors.get_index_for_name("zrelpath")
|
||||
processor = self.md.treeprocessors[at]
|
||||
if not isinstance(processor, LinksProcessor):
|
||||
raise TypeError("Links processor not registered")
|
||||
if processor.path == GLOSSARY_PAGE:
|
||||
return
|
||||
|
||||
self.seen.clear()
|
||||
glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
|
||||
self._walk(root, glossary_href)
|
||||
|
||||
def _walk(self, element: ET.Element, glossary_href: str) -> None:
|
||||
if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
|
||||
return
|
||||
|
||||
self._replace(element, glossary_href)
|
||||
|
||||
idx = 0
|
||||
while idx < len(element):
|
||||
child = element[idx]
|
||||
self._walk(child, glossary_href)
|
||||
idx = self._replace(element, glossary_href, idx) + 1
|
||||
|
||||
def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
|
||||
child = None if index is None else parent[index]
|
||||
text = parent.text if child is None else child.tail
|
||||
pieces = self._pieces(text or "", glossary_href)
|
||||
if not pieces:
|
||||
return -1 if index is None else index
|
||||
|
||||
if child is None:
|
||||
parent.text = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
# Insert replacements for parent.text before the first existing child.
|
||||
insert_at = -1
|
||||
else:
|
||||
assert index is not None
|
||||
child.tail = pieces[0] if isinstance(pieces[0], str) else ""
|
||||
insert_at = index
|
||||
|
||||
start = 1 if isinstance(pieces[0], str) else 0
|
||||
previous = child
|
||||
|
||||
for piece in pieces[start:]:
|
||||
if isinstance(piece, str):
|
||||
previous.tail = (previous.tail or "") + piece
|
||||
continue
|
||||
|
||||
insert_at += 1
|
||||
parent.insert(insert_at, piece)
|
||||
previous = piece
|
||||
|
||||
return insert_at
|
||||
|
||||
def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
|
||||
if not text.strip():
|
||||
return []
|
||||
|
||||
pieces: list[str | ET.Element] = []
|
||||
cursor = 0
|
||||
|
||||
while True:
|
||||
best = None
|
||||
for slug, pattern, tooltip in self.glossary:
|
||||
if slug in self.seen:
|
||||
continue
|
||||
|
||||
found = pattern.search(text, cursor)
|
||||
if found is None:
|
||||
continue
|
||||
|
||||
candidate = (slug, tooltip, found.start(), found.end())
|
||||
if best is None:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
_, _, best_start, best_end = best
|
||||
_, _, current_start, current_end = candidate
|
||||
if current_start < best_start:
|
||||
best = candidate
|
||||
continue
|
||||
|
||||
if current_start == best_start and current_end - current_start > best_end - best_start:
|
||||
best = candidate
|
||||
|
||||
if best is None:
|
||||
break
|
||||
|
||||
slug, tooltip, start, end = best
|
||||
if start > cursor:
|
||||
pieces.append(text[cursor:start])
|
||||
|
||||
link = ET.Element(
|
||||
"a",
|
||||
{
|
||||
"class": "glossary-term",
|
||||
"data-glossary-term": "",
|
||||
"href": f"{glossary_href}{slug}",
|
||||
},
|
||||
)
|
||||
ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
|
||||
ET.SubElement(
|
||||
link,
|
||||
"span",
|
||||
{
|
||||
"class": "glossary-term__tooltip",
|
||||
"data-search-exclude": "",
|
||||
},
|
||||
).text = tooltip
|
||||
pieces.append(link)
|
||||
self.seen.add(slug)
|
||||
cursor = end
|
||||
|
||||
if not pieces:
|
||||
return []
|
||||
if cursor < len(text):
|
||||
pieces.append(text[cursor:])
|
||||
return pieces
|
||||
|
||||
|
||||
class GlossaryExtension(Extension):
|
||||
def extendMarkdown(self, md) -> None:
|
||||
md.registerExtension(self)
|
||||
glossary, rendered = load_glossary()
|
||||
|
||||
md.preprocessors.register(
|
||||
GlossaryPreprocessor(md, rendered),
|
||||
"docs-ext-glossary-preprocessor",
|
||||
27,
|
||||
)
|
||||
md.treeprocessors.register(
|
||||
GlossaryTreeprocessor(md, glossary),
|
||||
"docs-ext-glossary-treeprocessor",
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
def makeExtension(**kwargs) -> GlossaryExtension:
|
||||
return GlossaryExtension(**kwargs)
|
||||
@@ -1,8 +0,0 @@
|
||||
[glossary]
|
||||
onroad = "openpilot's system state while ignition is on."
|
||||
offroad = "openpilot's system state while ignition is off."
|
||||
route = "A route is a recording of an onroad session."
|
||||
segment = "Routes are split into one minute chunks called segments."
|
||||
"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
|
||||
panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
|
||||
"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
|
||||
@@ -0,0 +1,12 @@
|
||||
# What is openpilot?
|
||||
|
||||
[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
|
||||
|
||||
|
||||
## How do I use it?
|
||||
|
||||
openpilot is designed to be used on the comma four.
|
||||
|
||||
## How does it work?
|
||||
|
||||
In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
|
||||
@@ -0,0 +1,68 @@
|
||||
import re
|
||||
import tomllib
|
||||
|
||||
def load_glossary(file_path="docs/glossary.toml"):
|
||||
with open(file_path, "rb") as f:
|
||||
glossary_data = tomllib.load(f)
|
||||
return glossary_data.get("glossary", {})
|
||||
|
||||
def generate_anchor_id(name):
|
||||
return name.replace(" ", "-").replace("_", "-").lower()
|
||||
|
||||
def format_markdown_term(name, definition):
|
||||
anchor_id = generate_anchor_id(name)
|
||||
markdown = f"* [**{name.replace('_', ' ').title()}**](#{anchor_id})"
|
||||
if definition.get("abbreviation"):
|
||||
markdown += f" *({definition['abbreviation']})*"
|
||||
if definition.get("description"):
|
||||
markdown += f": {definition['description']}\n"
|
||||
return markdown
|
||||
|
||||
def glossary_markdown(vocabulary):
|
||||
markdown = ""
|
||||
for category, terms in vocabulary.items():
|
||||
markdown += f"## {category.replace('_', ' ').title()}\n\n"
|
||||
for name, definition in terms.items():
|
||||
markdown += format_markdown_term(name, definition)
|
||||
return markdown
|
||||
|
||||
def format_tooltip_html(term_key, definition, html):
|
||||
display_term = term_key.replace("_", " ").title()
|
||||
clean_description = re.sub(r"\[(.+)]\(.+\)", r"\1", definition["description"])
|
||||
glossary_link = (
|
||||
f"<a href='/concepts/glossary#{term_key}' class='tooltip-glossary-link' title='View in glossary'>Glossary🔗</a>"
|
||||
)
|
||||
return re.sub(
|
||||
re.escape(display_term),
|
||||
lambda
|
||||
match: f"<span data-tooltip>{match.group(0)}<span class='tooltip-content'>{clean_description} {glossary_link}</span></span>",
|
||||
html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
def apply_tooltip(_term_key, _definition, pattern, html):
|
||||
return re.sub(
|
||||
pattern,
|
||||
lambda match: format_tooltip_html(_term_key, _definition, match.group(0)),
|
||||
html,
|
||||
flags=re.IGNORECASE,
|
||||
)
|
||||
|
||||
def tooltip_html(vocabulary, html):
|
||||
for _category, terms in vocabulary.items():
|
||||
for term_key, definition in terms.items():
|
||||
if definition.get("description"):
|
||||
pattern = rf"(?<!\w){re.escape(term_key.replace('_', ' ').title())}(?![^<]*<\/a>)(?!\([^)]*\))"
|
||||
html = apply_tooltip(term_key, definition, pattern, html)
|
||||
return html
|
||||
|
||||
# Page Hooks
|
||||
def on_page_markdown(markdown, **kwargs):
|
||||
glossary = load_glossary()
|
||||
return markdown.replace("{{GLOSSARY_DEFINITIONS}}", glossary_markdown(glossary))
|
||||
|
||||
def on_page_content(html, **kwargs):
|
||||
if kwargs.get("page").title == "Glossary":
|
||||
return html
|
||||
glossary = load_glossary()
|
||||
return tooltip_html(glossary, html)
|
||||
@@ -1,15 +1,15 @@
|
||||
# connect to a comma 3X or comma four
|
||||
# connect to a comma four
|
||||
|
||||
A comma device is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
|
||||
A comma four is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
|
||||
|
||||
## Serial Console
|
||||
|
||||
On the comma 3X, the serial console is accessible from the main OBD-C port, forwarded through the panda.
|
||||
Access it using `panda/scripts/som_debug.sh`.
|
||||
On both the comma three and comma four, the serial console is accessible from the main OBD-C port.
|
||||
Connect the comma four to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
|
||||
|
||||
comma four also exposes a serial console, albeit through an internal debug connector. Dedicated debug hardware coming soon to the comma shop.
|
||||
On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
|
||||
|
||||
Login to the default user with:
|
||||
On the comma four, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
|
||||
|
||||
* Username: `comma`
|
||||
* Password: `comma`
|
||||
@@ -25,7 +25,7 @@ In order to SSH into your device, you'll need a GitHub account with SSH keys. Se
|
||||
* Port: `22`
|
||||
|
||||
Here's an example command for connecting to your device using its tethered connection:<br />
|
||||
`ssh comma@192.168.43.1 -i ~/.ssh/my_github_key`
|
||||
`ssh comma@192.168.43.1`
|
||||
|
||||
For doing development work on device, it's recommended to use [SSH agent forwarding](https://docs.github.com/en/developers/overview/using-ssh-agent-forwarding).
|
||||
|
||||
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
|
||||
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
|
||||
|
||||
> [!NOTE]
|
||||
> The default port for ADB is 5555.
|
||||
> The default port for ADB is 5555 on the comma four.
|
||||
|
||||
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).
|
||||
|
||||
@@ -55,7 +55,7 @@ The public keys are only fetched from your GitHub account once. In order to upda
|
||||
|
||||
The `id_rsa` key in this directory only works while your device is in the setup state with no software installed. After installation, that default key will be removed.
|
||||
|
||||
## ssh.comma.ai proxy
|
||||
#### ssh.comma.ai proxy
|
||||
|
||||
With a [comma prime subscription](https://comma.ai/connect), you can SSH into your comma device from anywhere.
|
||||
|
||||
@@ -79,7 +79,6 @@ Host ssh.comma.ai
|
||||
```
|
||||
ssh -i ~/.ssh/my_github_key -o ProxyCommand="ssh -i ~/.ssh/my_github_key -W %h:%p -p %p %h@ssh.comma.ai" comma@ffffffffffffffff
|
||||
```
|
||||
|
||||
(Replace `ffffffffffffffff` with your dongle_id)
|
||||
|
||||
### ssh.comma.ai host key fingerprint
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
# What is openpilot?
|
||||
|
||||
[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
|
||||
|
||||
|
||||
## How do I use it?
|
||||
|
||||
openpilot is designed to be used on the comma four.
|
||||
|
||||
## How does it work?
|
||||
|
||||
In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
getting-started/what-is-openpilot.md
|
||||
@@ -1,42 +0,0 @@
|
||||
.md-logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.glossary-term {
|
||||
position: relative;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.glossary-term__label {
|
||||
border-bottom: 1px dotted currentColor;
|
||||
}
|
||||
|
||||
.glossary-term__tooltip {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.4rem);
|
||||
left: 50%;
|
||||
width: max-content;
|
||||
max-width: min(30rem, 80vw);
|
||||
padding: 0.65rem 0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
background: rgb(26 26 26 / 96%);
|
||||
color: white;
|
||||
box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%) translateY(-0.15rem);
|
||||
transition: opacity 120ms ease, transform 120ms ease;
|
||||
visibility: hidden;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.glossary-term:hover .glossary-term__tooltip,
|
||||
.glossary-term:focus-visible .glossary-term__tooltip,
|
||||
.glossary-term:focus-within .glossary-term__tooltip {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
visibility: visible;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
site_name: openpilot docs
|
||||
repo_url: https://github.com/commaai/openpilot/
|
||||
site_url: https://docs.comma.ai
|
||||
|
||||
exclude_docs: README.md
|
||||
|
||||
strict: true
|
||||
docs_dir: docs
|
||||
site_dir: docs_site/
|
||||
|
||||
hooks:
|
||||
- docs/hooks/glossary.py
|
||||
extra_css:
|
||||
- css/tooltip.css
|
||||
theme:
|
||||
name: readthedocs
|
||||
navigation_depth: 3
|
||||
|
||||
nav:
|
||||
- Getting Started:
|
||||
- What is openpilot?: getting-started/what-is-openpilot.md
|
||||
- How-to:
|
||||
- Turn the speed blue: how-to/turn-the-speed-blue.md
|
||||
- Connect to a comma 3X: how-to/connect-to-comma.md
|
||||
# - Make your first pull request: how-to/make-first-pr.md
|
||||
#- Replay a drive: how-to/replay-a-drive.md
|
||||
- Concepts:
|
||||
- Logs: concepts/logs.md
|
||||
- Safety: concepts/safety.md
|
||||
- Glossary: concepts/glossary.md
|
||||
- Car Porting:
|
||||
- What is a car port?: car-porting/what-is-a-car-port.md
|
||||
- Porting a car brand: car-porting/brand-port.md
|
||||
- Porting a car model: car-porting/model-port.md
|
||||
- Contributing:
|
||||
- Roadmap: contributing/roadmap.md
|
||||
#- Architecture: contributing/architecture.md
|
||||
- Contributing Guide →: https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md
|
||||
- Links:
|
||||
- Blog →: https://blog.comma.ai
|
||||
- Bounties →: https://comma.ai/bounties
|
||||
- GitHub →: https://github.com/commaai
|
||||
- Discord →: https://discord.comma.ai
|
||||
- X →: https://x.com/comma_ai
|
||||
+1
-1
Submodule opendbc_repo updated: 57b531acd3...427032a89a
+1
-1
Submodule panda updated: 0a9ef7ab54...6cd1972ecf
+2
-2
@@ -82,7 +82,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"Jinja2",
|
||||
"zensical",
|
||||
"mkdocs",
|
||||
]
|
||||
|
||||
testing = [
|
||||
@@ -150,7 +150,7 @@ quiet-level = 3
|
||||
# if you've got a short variable name that's getting flagged, add it here
|
||||
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite,ser"
|
||||
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, *.pem, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
||||
skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
|
||||
|
||||
# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml
|
||||
[tool.ruff]
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
"""
|
||||
wrapper that materializes symlinks in docs/ before build
|
||||
|
||||
we can delete this once zensical supports symlinks:
|
||||
https://github.com/zensical/backlog/issues/55
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import signal
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DOCS_DIR = REPO_ROOT / "docs"
|
||||
SITE_DIR = REPO_ROOT / "docs_site"
|
||||
sys.path.insert(0, str(REPO_ROOT))
|
||||
# Local docs build helpers live under docs/ so they stay near the content
|
||||
# source. The wrapper prunes them from docs_site/ after build.
|
||||
sys.path.insert(0, str(DOCS_DIR))
|
||||
|
||||
|
||||
def _materialize(docs: Path) -> dict[Path, str]:
|
||||
originals: dict[Path, str] = {}
|
||||
for link in docs.rglob("*"):
|
||||
if not link.is_symlink():
|
||||
continue
|
||||
target = link.resolve()
|
||||
if not target.is_file():
|
||||
continue
|
||||
originals[link] = os.readlink(link)
|
||||
link.unlink()
|
||||
shutil.copy2(target, link)
|
||||
return originals
|
||||
|
||||
|
||||
def _restore(originals: dict[Path, str]) -> None:
|
||||
for link, target in originals.items():
|
||||
link.unlink(missing_ok=True)
|
||||
os.symlink(target, link)
|
||||
|
||||
|
||||
def _raise_interrupt(*_):
|
||||
raise KeyboardInterrupt
|
||||
|
||||
|
||||
def _prune_site_output() -> None:
|
||||
shutil.rmtree(SITE_DIR / "ext", ignore_errors=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
signal.signal(signal.SIGTERM, _raise_interrupt)
|
||||
originals = _materialize(DOCS_DIR)
|
||||
try:
|
||||
from zensical.main import cli
|
||||
cli(standalone_mode=False)
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "build":
|
||||
_prune_site_output()
|
||||
finally:
|
||||
_restore(originals)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+11
-2
@@ -33,7 +33,16 @@ if __name__ == "__main__":
|
||||
print("|-| ----- | --------- |")
|
||||
|
||||
for f in glob.glob(BASEDIR + MODEL_PATH + "/*.onnx"):
|
||||
# TODO: add checkpoint to DM
|
||||
if "dmonitoring" in f:
|
||||
continue
|
||||
|
||||
fn = os.path.basename(f)
|
||||
master = get_checkpoint(MASTER_PATH + MODEL_PATH + fn)
|
||||
master_path = MASTER_PATH + MODEL_PATH + fn
|
||||
if os.path.exists(master_path):
|
||||
master = get_checkpoint(master_path)
|
||||
master_col = f"[{master}](https://reporter.comma.life/experiment/{master})"
|
||||
else:
|
||||
master_col = "N/A (new model)"
|
||||
pr = get_checkpoint(BASEDIR + MODEL_PATH + fn)
|
||||
print("|", fn, "|", f"[{master}](https://reporterv2.comma.life/{master})", "|", f"[{pr}](https://reporterv2.comma.life/{pr})", "|")
|
||||
print("|", fn, "|", master_col, "|", f"[{pr}](https://reporter.comma.life/experiment/{pr})", "|")
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4ce1d357acadd798939b398cce1761ceb05564b44f2a5bc6865c7842e60e79f2
|
||||
size 1474
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b3fe73cd1a24c05346a9b4a02e4f900a314c83a422beb38b0f88f91389582cd4
|
||||
size 3960
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b5aee9f6cec03f1967014cd2ea2a23982b262e7d86dadca602ecfa8875b38101
|
||||
size 5875
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:56de402482b5987ed9a0ff3f793a1c89f857304b34fbb8a3deb5b5d4a332be1c
|
||||
size 3688
|
||||
@@ -1,6 +1,6 @@
|
||||
{% set footnote_tag = '[<sup>{}</sup>](#footnotes)' %}
|
||||
{% set star_icon = '[](##)' %}
|
||||
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>' %}
|
||||
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>' %}
|
||||
{# Force hardware column wider by using a blank image with max width. #}
|
||||
{% set width_tag = '<a href="##"><img width=2000></a>%s<br> ' %}
|
||||
{% set hardware_col_name = 'Hardware Needed' %}
|
||||
|
||||
@@ -86,7 +86,7 @@ class Car:
|
||||
|
||||
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
|
||||
|
||||
is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||
is_release = self.params.get_bool("IsReleaseBranch")
|
||||
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
|
||||
|
||||
if CI is None:
|
||||
|
||||
@@ -11,7 +11,7 @@ FOOTNOTE_TAG = "<sup>{}</sup>"
|
||||
STAR_ICON = '<a href="##"><img valign="top" ' + \
|
||||
'src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-star-{}.svg" width="22" /></a>'
|
||||
VIDEO_ICON = '<a href="{}" target="_blank">' + \
|
||||
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg" /></a>'
|
||||
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg"></img></a>'
|
||||
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
|
||||
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
|
||||
ARROW_SYMBOL = "➡️"
|
||||
|
||||
+13
-38
@@ -1,9 +1,6 @@
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
from SCons.Script import Value
|
||||
import glob
|
||||
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
|
||||
from tinygrad import Device
|
||||
|
||||
Import('env', 'arch')
|
||||
chunker_file = File("#common/file_chunker.py")
|
||||
@@ -16,52 +13,31 @@ tinygrad_files = ["#"+x for x in glob.glob(env.Dir("#tinygrad_repo").relpath + "
|
||||
def estimate_pickle_max_size(onnx_size):
|
||||
return 1.2 * onnx_size + 10 * 1024 * 1024 # 20% + 10MB is plenty
|
||||
|
||||
# compile warp
|
||||
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
|
||||
# get fastest TG config
|
||||
available = set(Device.get_available_devices())
|
||||
# FIXME-SP: reset when we bump tg
|
||||
if False: # 'CUDA' in available:
|
||||
tg_backend = 'CUDA'
|
||||
tg_flags = f'DEV={tg_backend}'
|
||||
elif 'QCOM' in available:
|
||||
tg_backend = 'QCOM'
|
||||
tg_flags = f'DEV={tg_backend} FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0'
|
||||
else:
|
||||
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU CPU_LLVM=1' # FIXME-SP: reset when we bump tg
|
||||
tg_flags = f'DEV={tg_backend} THREADS=0'
|
||||
|
||||
def write_tg_compiled_flags(target, source, env):
|
||||
with open(str(target[0]), "w") as f:
|
||||
json.dump({"DEV": tg_backend}, f)
|
||||
f.write("\n")
|
||||
|
||||
compiled_flags_node = lenv.Command(
|
||||
File("models/tg_compiled_flags.json").abspath,
|
||||
tinygrad_files + [Value(tg_backend)],
|
||||
write_tg_compiled_flags,
|
||||
)
|
||||
|
||||
# tinygrad calls brew which needs a $HOME in the env
|
||||
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
|
||||
tg_flags = {
|
||||
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
|
||||
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}', # tinygrad calls brew which needs a $HOME in the env
|
||||
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
|
||||
|
||||
# Get model metadata
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
fn = File(f"models/{model_name}").abspath
|
||||
script_files = [File(Dir("#selfdrive/modeld").File("get_model_metadata.py").abspath)]
|
||||
cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files + [compiled_flags_node], cmd)
|
||||
cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
|
||||
lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd)
|
||||
|
||||
image_flag = {
|
||||
'larch64': 'IMAGE=2',
|
||||
}.get(arch, 'IMAGE=0')
|
||||
script_files = [File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
|
||||
compile_warp_cmd = f'{tg_flags} {mac_brew_string} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py '
|
||||
compile_warp_cmd = f'{tg_flags} python3 {Dir("#selfdrive/modeld").abspath}/compile_warp.py '
|
||||
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
|
||||
warp_targets = []
|
||||
for cam in [_ar_ox_fisheye, _os_fisheye]:
|
||||
w, h = cam.width, cam.height
|
||||
warp_targets += [File(f"models/warp_{w}x{h}_tinygrad.pkl").abspath, File(f"models/dm_warp_{w}x{h}_tinygrad.pkl").abspath]
|
||||
lenv.Command(warp_targets, tinygrad_files + script_files + [compiled_flags_node], compile_warp_cmd)
|
||||
lenv.Command(warp_targets, tinygrad_files + script_files, compile_warp_cmd)
|
||||
|
||||
def tg_compile(flags, model_name):
|
||||
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
|
||||
@@ -71,7 +47,7 @@ def tg_compile(flags, model_name):
|
||||
chunk_targets = get_chunk_paths(pkl, estimate_pickle_max_size(os.path.getsize(onnx_path)))
|
||||
compile_node = lenv.Command(
|
||||
pkl,
|
||||
[onnx_path] + tinygrad_files + [chunker_file, compiled_flags_node],
|
||||
[onnx_path] + tinygrad_files + [chunker_file],
|
||||
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
|
||||
)
|
||||
def do_chunk(target, source, env):
|
||||
@@ -83,6 +59,5 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile small models
|
||||
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
|
||||
for model_name in ['driving_vision', 'driving_off_policy', 'driving_on_policy', 'dmonitoring_model']:
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -97,8 +97,8 @@ def make_update_img_input(frame_prepare, model_w, model_h):
|
||||
def update_img_input_tinygrad(tensor, frame, M_inv):
|
||||
M_inv = M_inv.to(Device.DEFAULT)
|
||||
new_img = frame_prepare(frame, M_inv)
|
||||
tensor.assign(tensor[6:].cat(new_img, dim=0).contiguous())
|
||||
return Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
||||
full_buffer = tensor[6:].cat(new_img, dim=0).contiguous()
|
||||
return full_buffer, Tensor.cat(full_buffer[:6], full_buffer[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
|
||||
return update_img_input_tinygrad
|
||||
|
||||
|
||||
@@ -107,9 +107,9 @@ def make_update_both_imgs(frame_prepare, model_w, model_h):
|
||||
|
||||
def update_both_imgs_tinygrad(calib_img_buffer, new_img, M_inv,
|
||||
calib_big_img_buffer, new_big_img, M_inv_big):
|
||||
calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
||||
calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
||||
return calib_img_pair, calib_big_img_pair
|
||||
calib_img_buffer, calib_img_pair = update_img(calib_img_buffer, new_img, M_inv)
|
||||
calib_big_img_buffer, calib_big_img_pair = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
|
||||
return calib_img_buffer, calib_img_pair, calib_big_img_buffer, calib_big_img_pair
|
||||
return update_both_imgs_tinygrad
|
||||
|
||||
|
||||
@@ -136,20 +136,29 @@ def compile_modeld_warp(cam_w, cam_h):
|
||||
|
||||
full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
||||
big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
|
||||
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||
new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||
full_buffer_np = np.zeros(IMG_BUFFER_SHAPE, dtype=np.uint8)
|
||||
big_full_buffer_np = np.zeros(IMG_BUFFER_SHAPE, dtype=np.uint8)
|
||||
|
||||
for i in range(10):
|
||||
new_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
|
||||
img_inputs = [full_buffer,
|
||||
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||
new_big_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
|
||||
big_img_inputs = [big_full_buffer,
|
||||
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||
inputs = img_inputs + big_img_inputs
|
||||
Device.default.synchronize()
|
||||
|
||||
inputs_np = [x.numpy() for x in inputs]
|
||||
inputs_np[0] = full_buffer_np
|
||||
inputs_np[3] = big_full_buffer_np
|
||||
|
||||
st = time.perf_counter()
|
||||
_ = update_img_jit(*inputs)
|
||||
out = update_img_jit(*inputs)
|
||||
full_buffer = out[0].contiguous().realize().clone()
|
||||
big_full_buffer = out[2].contiguous().realize().clone()
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
@@ -173,9 +182,8 @@ def compile_dm_warp(cam_w, cam_h):
|
||||
warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h)
|
||||
warp_dm_jit = TinyJit(warp_dm, prune=True)
|
||||
|
||||
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||
for i in range(10):
|
||||
inputs = [Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||
inputs = [Tensor.from_blob((32 * Tensor.randn(yuv_size,) + 128).cast(dtype='uint8').realize().numpy().ctypes.data, (yuv_size,), dtype='uint8'),
|
||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||
Device.default.synchronize()
|
||||
st = time.perf_counter()
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
||||
set_tinygrad_backend_from_compiled_flags()
|
||||
|
||||
# FIXME-SP: remove once we bump tg
|
||||
from openpilot.system.hardware import TICI
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
|
||||
from tinygrad.tensor import Tensor
|
||||
import time
|
||||
import pickle
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
from cereal import messaging
|
||||
from cereal.messaging import PubMaster, SubMaster
|
||||
@@ -25,8 +21,9 @@ from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid, safe_exp
|
||||
|
||||
PROCESS_NAME = "selfdrive.modeld.dmonitoringmodeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
MODEL_PKL_PATH = MODELS_DIR / 'dmonitoring_model_tinygrad.pkl'
|
||||
METADATA_PATH = MODELS_DIR / 'dmonitoring_model_metadata.pkl'
|
||||
MODEL_PKL_PATH = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
|
||||
METADATA_PATH = Path(__file__).parent / 'models/dmonitoring_model_metadata.pkl'
|
||||
MODELS_DIR = Path(__file__).parent / 'models'
|
||||
|
||||
class ModelState:
|
||||
inputs: dict[str, np.ndarray]
|
||||
@@ -83,7 +80,7 @@ def parse_model_output(model_output):
|
||||
face_descs = model_output[f'face_descs_{ds_suffix}']
|
||||
parsed[f'face_descs_{ds_suffix}'] = face_descs[:, :-6]
|
||||
parsed[f'face_descs_{ds_suffix}_std'] = safe_exp(face_descs[:, -6:])
|
||||
for key in ['face_prob', 'left_eye_prob', 'right_eye_prob','left_blink_prob', 'right_blink_prob', 'sunglasses_prob', 'using_phone_prob']:
|
||||
for key in ['face_prob', 'eyes_visible_prob', 'eyes_closed_prob', 'using_phone_prob']:
|
||||
parsed[f'{key}_{ds_suffix}'] = sigmoid(model_output[f'{key}_{ds_suffix}'])
|
||||
return parsed
|
||||
|
||||
@@ -93,11 +90,8 @@ def fill_driver_data(msg, model_output, ds_suffix):
|
||||
msg.facePosition = model_output[f'face_descs_{ds_suffix}'][0, 3:5].tolist()
|
||||
msg.facePositionStd = model_output[f'face_descs_{ds_suffix}_std'][0, 3:5].tolist()
|
||||
msg.faceProb = model_output[f'face_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.leftEyeProb = model_output[f'left_eye_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.rightEyeProb = model_output[f'right_eye_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.leftBlinkProb = model_output[f'left_blink_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.rightBlinkProb = model_output[f'right_blink_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.sunglassesProb = model_output[f'sunglasses_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.eyesVisibleProb = model_output[f'eyes_visible_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.eyesClosedProb = model_output[f'eyes_closed_prob_{ds_suffix}'][0, 0].item()
|
||||
msg.phoneProb = model_output[f'using_phone_prob_{ds_suffix}'][0, 0].item()
|
||||
|
||||
def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_time: float, gpu_exec_time: float):
|
||||
|
||||
+28
-12
@@ -1,12 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
from openpilot.selfdrive.modeld.tinygrad_helpers import MODELS_DIR, set_tinygrad_backend_from_compiled_flags
|
||||
set_tinygrad_backend_from_compiled_flags()
|
||||
|
||||
# FIXME-SP: remove once we bump tg
|
||||
from openpilot.system.hardware import TICI
|
||||
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
|
||||
|
||||
USBGPU = "USBGPU" in os.environ
|
||||
if USBGPU:
|
||||
os.environ['DEV'] = 'AMD'
|
||||
@@ -17,6 +12,7 @@ import pickle
|
||||
import numpy as np
|
||||
import cereal.messaging as messaging
|
||||
from cereal import car, log
|
||||
from pathlib import Path
|
||||
from cereal.messaging import PubMaster, SubMaster
|
||||
from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf
|
||||
from opendbc.car.car_helpers import get_demo_car_params
|
||||
@@ -41,10 +37,13 @@ from openpilot.sunnypilot.modeld_v2.modeld_base import ModelStateBase
|
||||
PROCESS_NAME = "selfdrive.modeld.modeld"
|
||||
SEND_RAW_PRED = os.getenv('SEND_RAW_PRED')
|
||||
|
||||
MODELS_DIR = Path(__file__).parent / 'models'
|
||||
VISION_PKL_PATH = MODELS_DIR / 'driving_vision_tinygrad.pkl'
|
||||
VISION_METADATA_PATH = MODELS_DIR / 'driving_vision_metadata.pkl'
|
||||
POLICY_PKL_PATH = MODELS_DIR / 'driving_policy_tinygrad.pkl'
|
||||
POLICY_METADATA_PATH = MODELS_DIR / 'driving_policy_metadata.pkl'
|
||||
ON_POLICY_PKL_PATH = MODELS_DIR / 'driving_on_policy_tinygrad.pkl'
|
||||
ON_POLICY_METADATA_PATH = MODELS_DIR / 'driving_on_policy_metadata.pkl'
|
||||
OFF_POLICY_PKL_PATH = MODELS_DIR / 'driving_off_policy_tinygrad.pkl'
|
||||
OFF_POLICY_METADATA_PATH = MODELS_DIR / 'driving_off_policy_metadata.pkl'
|
||||
|
||||
LAT_SMOOTH_SECONDS = 0.0
|
||||
LONG_SMOOTH_SECONDS = 0.3
|
||||
@@ -159,7 +158,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output_slices = vision_metadata['output_slices']
|
||||
vision_output_size = vision_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(POLICY_METADATA_PATH, 'rb') as f:
|
||||
with open(OFF_POLICY_METADATA_PATH, 'rb') as f:
|
||||
off_policy_metadata = pickle.load(f)
|
||||
self.off_policy_input_shapes = off_policy_metadata['input_shapes']
|
||||
self.off_policy_output_slices = off_policy_metadata['output_slices']
|
||||
off_policy_output_size = off_policy_metadata['output_shapes']['outputs'][1]
|
||||
|
||||
with open(ON_POLICY_METADATA_PATH, 'rb') as f:
|
||||
policy_metadata = pickle.load(f)
|
||||
self.policy_input_shapes = policy_metadata['input_shapes']
|
||||
self.policy_output_slices = policy_metadata['output_slices']
|
||||
@@ -183,11 +188,13 @@ class ModelState(ModelStateBase):
|
||||
self.vision_output = np.zeros(vision_output_size, dtype=np.float32)
|
||||
self.policy_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
|
||||
self.policy_output = np.zeros(policy_output_size, dtype=np.float32)
|
||||
self.off_policy_output = np.zeros(off_policy_output_size, dtype=np.float32)
|
||||
self.parser = Parser()
|
||||
self.frame_buf_params : dict[str, tuple[int, int, int, int]] = {}
|
||||
self.update_imgs = None
|
||||
self.vision_run = pickle.loads(read_file_chunked(str(VISION_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(POLICY_PKL_PATH)))
|
||||
self.policy_run = pickle.loads(read_file_chunked(str(ON_POLICY_PKL_PATH)))
|
||||
self.off_policy_run = pickle.loads(read_file_chunked(str(OFF_POLICY_PKL_PATH)))
|
||||
|
||||
def slice_outputs(self, model_outputs: np.ndarray, output_slices: dict[str, slice]) -> dict[str, np.ndarray]:
|
||||
parsed_model_outputs = {k: model_outputs[np.newaxis, v] for k,v in output_slices.items()}
|
||||
@@ -220,7 +227,8 @@ class ModelState(ModelStateBase):
|
||||
|
||||
out = self.update_imgs(self.img_queues['img'], self.full_frames['img'], self.transforms['img'],
|
||||
self.img_queues['big_img'], self.full_frames['big_img'], self.transforms['big_img'])
|
||||
vision_inputs = {'img': out[0], 'big_img': out[1]}
|
||||
self.img_queues['img'], self.img_queues['big_img'] = out[0].realize(), out[2].realize()
|
||||
vision_inputs = {'img': out[1], 'big_img': out[3]}
|
||||
|
||||
if prepare_only:
|
||||
return None
|
||||
@@ -235,9 +243,17 @@ class ModelState(ModelStateBase):
|
||||
|
||||
self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
|
||||
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices))
|
||||
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
|
||||
|
||||
self.off_policy_output = self.off_policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy()
|
||||
off_policy_outputs_dict = self.parser.parse_off_policy_outputs(self.slice_outputs(self.off_policy_output, self.off_policy_output_slices))
|
||||
off_policy_outputs_dict.pop('plan')
|
||||
|
||||
|
||||
combined_outputs_dict = {**vision_outputs_dict, **off_policy_outputs_dict, **policy_outputs_dict}
|
||||
if 'planplus' in combined_outputs_dict and 'plan' in combined_outputs_dict:
|
||||
combined_outputs_dict['plan'] = combined_outputs_dict['plan'] + combined_outputs_dict['planplus']
|
||||
if SEND_RAW_PRED:
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
|
||||
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy(), self.off_policy_output.copy()])
|
||||
|
||||
return combined_outputs_dict
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
driving_policy.onnx
|
||||
@@ -1 +0,0 @@
|
||||
driving_vision.onnx
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7aff7ff1dc08bbaf562a8f77380ab5e5914f8557dba2f760d87e4d953f5604b0
|
||||
size 7307246
|
||||
oid sha256:2fd471febb6e973313ac0d0c6755f6410c1937ba92230b58a433761e8c883072
|
||||
size 7364290
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e53f4e0527766082ba7bde38e275def0fe3c14f6c59ae2854439e239884d3ecc
|
||||
size 13393365
|
||||
@@ -0,0 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ea89c50da3a16e710da292f97c81b083a982cfdee5c28eca0d37ed2fb99af6c5
|
||||
size 13022642
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:78477124cbf3ffe30fa951ebada8410b43c4242c6054584d656f1d329b067e15
|
||||
size 14060847
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ee29ee5bce84d1ce23e9ff381280de9b4e4d96d2934cd751740354884e112c66
|
||||
size 46877473
|
||||
oid sha256:6263aa3fbb44cde6c68a34cdb7cd8c389789dbc02b15c1911afdac4e018281ae
|
||||
size 23267151
|
||||
|
||||
@@ -96,11 +96,17 @@ class Parser:
|
||||
self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,))
|
||||
self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,))
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
return outs
|
||||
|
||||
def parse_off_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH))
|
||||
self.parse_binary_crossentropy('lane_lines_prob', outs)
|
||||
self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH))
|
||||
self.parse_binary_crossentropy('meta', outs)
|
||||
self.parse_binary_crossentropy('lead_prob', outs)
|
||||
lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH)
|
||||
lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0)
|
||||
@@ -110,7 +116,7 @@ class Parser:
|
||||
return outs
|
||||
|
||||
def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH)
|
||||
plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0)
|
||||
self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH))
|
||||
if 'planplus' in outs:
|
||||
@@ -120,5 +126,6 @@ class Parser:
|
||||
|
||||
def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
|
||||
outs = self.parse_vision_outputs(outs)
|
||||
outs = self.parse_off_policy_outputs(outs)
|
||||
outs = self.parse_policy_outputs(outs)
|
||||
return outs
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
MODELS_DIR = Path(__file__).parent / 'models'
|
||||
COMPILED_FLAGS_PATH = MODELS_DIR / 'tg_compiled_flags.json'
|
||||
|
||||
|
||||
def set_tinygrad_backend_from_compiled_flags() -> None:
|
||||
if os.path.isfile(COMPILED_FLAGS_PATH):
|
||||
with open(COMPILED_FLAGS_PATH) as f:
|
||||
os.environ['DEV'] = str(json.load(f)['DEV'])
|
||||
@@ -32,9 +32,8 @@ class DRIVER_MONITOR_SETTINGS:
|
||||
self._DISTRACTED_PROMPT_TIME_TILL_TERMINAL = 6.
|
||||
|
||||
self._FACE_THRESHOLD = 0.7
|
||||
self._EYE_THRESHOLD = 0.65
|
||||
self._SG_THRESHOLD = 0.9
|
||||
self._BLINK_THRESHOLD = 0.865
|
||||
self._EYE_THRESHOLD = 0.5
|
||||
self._BLINK_THRESHOLD = 0.5
|
||||
self._PHONE_THRESH = 0.5
|
||||
|
||||
self._POSE_PITCH_THRESHOLD = 0.3133
|
||||
@@ -111,11 +110,6 @@ class DriverProb:
|
||||
self.prob_offseter = RunningStatFilter(raw_priors=raw_priors, max_trackable=max_trackable)
|
||||
self.prob_calibrated = False
|
||||
|
||||
class DriverBlink:
|
||||
def __init__(self):
|
||||
self.left = 0.
|
||||
self.right = 0.
|
||||
|
||||
|
||||
# model output refers to center of undistorted+leveled image
|
||||
EFL = 598.0 # focal length in K
|
||||
@@ -150,7 +144,7 @@ class DriverMonitoring:
|
||||
wheelpos_filter_raw_priors = (self.settings._WHEELPOS_DATA_AVG, self.settings._WHEELPOS_DATA_VAR, 2)
|
||||
self.wheelpos = DriverProb(raw_priors=wheelpos_filter_raw_priors, max_trackable=self.settings._WHEELPOS_MAX_COUNT)
|
||||
self.pose = DriverPose(settings=self.settings)
|
||||
self.blink = DriverBlink()
|
||||
self.blink_prob = 0.
|
||||
self.phone_prob = 0.
|
||||
|
||||
self.always_on = always_on
|
||||
@@ -253,7 +247,7 @@ class DriverMonitoring:
|
||||
if pitch_error > pitch_threshold or yaw_error > yaw_threshold:
|
||||
distracted_types.append(DistractedType.DISTRACTED_POSE)
|
||||
|
||||
if (self.blink.left + self.blink.right)*0.5 > self.settings._BLINK_THRESHOLD:
|
||||
if self.blink_prob > self.settings._BLINK_THRESHOLD:
|
||||
distracted_types.append(DistractedType.DISTRACTED_BLINK)
|
||||
|
||||
if self.phone_prob > self.settings._PHONE_THRESH:
|
||||
@@ -294,10 +288,7 @@ class DriverMonitoring:
|
||||
self.pose.yaw_std = driver_data.faceOrientationStd[1]
|
||||
model_std_max = max(self.pose.pitch_std, self.pose.yaw_std)
|
||||
self.pose.low_std = model_std_max < self.settings._POSESTD_THRESHOLD
|
||||
self.blink.left = driver_data.leftBlinkProb * (driver_data.leftEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.blink.right = driver_data.rightBlinkProb * (driver_data.rightEyeProb > self.settings._EYE_THRESHOLD) \
|
||||
* (driver_data.sunglassesProb < self.settings._SG_THRESHOLD)
|
||||
self.blink_prob = driver_data.eyesClosedProb * (driver_data.eyesVisibleProb > self.settings._EYE_THRESHOLD)
|
||||
self.phone_prob = driver_data.phoneProb
|
||||
|
||||
self.distracted_types = self._get_distracted_types()
|
||||
@@ -443,7 +434,7 @@ class DriverMonitoring:
|
||||
enabled = sm['selfdriveState'].enabled or sm['carControl'].latActive
|
||||
wrong_gear = sm['carState'].gearShifter not in (car.CarState.GearShifter.drive, car.CarState.GearShifter.low)
|
||||
standstill = sm['carState'].standstill
|
||||
driver_engaged = sm['carState'].steeringPressed or (sm['selfdriveState'].enabled and sm['carState'].gasPressed)
|
||||
driver_engaged = sm['carState'].steeringPressed or sm['carState'].gasPressed
|
||||
brake_disengage_prob = sm['modelV2'].meta.disengagePredictions.brakeDisengageProbs[0] # brake disengage prob in next 2s
|
||||
rpyCalib = sm['liveCalibration'].rpyCalib
|
||||
self._set_policy(
|
||||
|
||||
@@ -20,10 +20,8 @@ def make_msg(face_detected, distracted=False, model_uncertain=False):
|
||||
ds.leftDriverData.faceOrientation = [0., 0., 0.]
|
||||
ds.leftDriverData.facePosition = [0., 0.]
|
||||
ds.leftDriverData.faceProb = 1. * face_detected
|
||||
ds.leftDriverData.leftEyeProb = 1.
|
||||
ds.leftDriverData.rightEyeProb = 1.
|
||||
ds.leftDriverData.leftBlinkProb = 1. * distracted
|
||||
ds.leftDriverData.rightBlinkProb = 1. * distracted
|
||||
ds.leftDriverData.eyesVisibleProb = 1.
|
||||
ds.leftDriverData.eyesClosedProb = 1. * distracted
|
||||
ds.leftDriverData.faceOrientationStd = [1.*model_uncertain, 1.*model_uncertain, 1.*model_uncertain]
|
||||
ds.leftDriverData.facePositionStd = [1.*model_uncertain, 1.*model_uncertain]
|
||||
# TODO: test both separately when e2e is used
|
||||
@@ -217,48 +215,64 @@ class TestMonitoring:
|
||||
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
|
||||
|
||||
|
||||
def _build_sm(selfdrive_enabled, lat_active, steering_pressed, gas_pressed):
|
||||
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
|
||||
(False, False, False), # Both Disabled
|
||||
(True, False, True), # OP Enabled, Lat Inactive
|
||||
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
|
||||
(True, True, True) # Both Active
|
||||
])
|
||||
def test_enabled_states(enabled_state, lat_active_state, expected):
|
||||
"""
|
||||
Test DriverMonitoring.run_step with all 4 combinations of:
|
||||
- selfdriveState.enabled (True/False)
|
||||
- carControl.latActive (True/False)
|
||||
"""
|
||||
cs = car.CarState.new_message()
|
||||
cs.vEgo = 30.0
|
||||
cs.gearShifter = car.CarState.GearShifter.drive
|
||||
cs.steeringPressed = steering_pressed
|
||||
cs.gasPressed = gas_pressed
|
||||
cs.standstill = False
|
||||
cs.steeringPressed = False
|
||||
cs.gasPressed = False
|
||||
|
||||
ss = log.SelfdriveState.new_message()
|
||||
ss.enabled = selfdrive_enabled
|
||||
ss.enabled = enabled_state
|
||||
|
||||
cc = car.CarControl.new_message()
|
||||
cc.latActive = lat_active
|
||||
cc.latActive = lat_active_state
|
||||
|
||||
mv2 = log.ModelDataV2.new_message()
|
||||
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
|
||||
|
||||
lc = log.LiveCalibrationData.new_message()
|
||||
lc.rpyCalib = [0.0, 0.0, 0.0]
|
||||
return {
|
||||
'carState': cs, 'selfdriveState': ss, 'carControl': cc,
|
||||
'modelV2': mv2, 'liveCalibration': lc, 'driverStateV2': make_msg(False),
|
||||
|
||||
ds = make_msg(False)
|
||||
|
||||
sm = {
|
||||
'carState': cs,
|
||||
'selfdriveState': ss,
|
||||
'carControl': cc,
|
||||
'modelV2': mv2,
|
||||
'liveCalibration': lc,
|
||||
'driverStateV2': ds
|
||||
}
|
||||
|
||||
driver_monitoring = DriverMonitoring()
|
||||
|
||||
@pytest.mark.parametrize("selfdrive_enabled, lat_active, steering, gas, expected_op_engaged, expected_driver_engaged", [
|
||||
(False, False, False, False, False, False), # disabled
|
||||
(True, False, False, False, True, False), # OP enabled
|
||||
(False, True, False, False, True, False), # MADS lat-only
|
||||
(True, True, False, False, True, False), # both active
|
||||
(False, True, False, True, True, False), # MADS lat-only + gas
|
||||
(True, True, False, True, True, True), # full op + gas: override
|
||||
(False, True, True, False, True, True), # MADS lat-only + wheel touch: override
|
||||
])
|
||||
def test_run_step_engagement(selfdrive_enabled, lat_active, steering, gas,
|
||||
expected_op_engaged, expected_driver_engaged):
|
||||
sm = _build_sm(selfdrive_enabled, lat_active, steering, gas)
|
||||
dm = DriverMonitoring()
|
||||
captured = {}
|
||||
orig = dm._update_events
|
||||
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
|
||||
captured_args = []
|
||||
original_update_events = driver_monitoring._update_events
|
||||
|
||||
def spy(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||
captured['driver_engaged'] = driver_engaged
|
||||
captured['op_engaged'] = op_engaged
|
||||
return orig(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
|
||||
def spy_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed):
|
||||
captured_args.append(op_engaged)
|
||||
return original_update_events(driver_engaged, op_engaged, standstill, wrong_gear, car_speed)
|
||||
|
||||
dm._update_events = spy
|
||||
dm.run_step(sm, demo=False)
|
||||
assert captured['op_engaged'] == expected_op_engaged
|
||||
assert captured['driver_engaged'] == expected_driver_engaged
|
||||
driver_monitoring._update_events = spy_update_events
|
||||
|
||||
driver_monitoring.run_step(sm, demo=False)
|
||||
|
||||
# Assertion
|
||||
assert len(captured_args) == 1, "Expected _update_events to be called exactly once"
|
||||
actual_enabled = captured_args[0]
|
||||
|
||||
assert actual_enabled == expected, f"Expected op_engaged={expected}, but got {actual_enabled}"
|
||||
|
||||
+13
-23
@@ -21,6 +21,8 @@
|
||||
#define CUTOFF_IL 400
|
||||
#define SATURATE_IL 1000
|
||||
|
||||
#define ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE 2048
|
||||
|
||||
ExitHandler do_exit;
|
||||
|
||||
bool check_connected(Panda *panda) {
|
||||
@@ -32,8 +34,15 @@ bool check_connected(Panda *panda) {
|
||||
}
|
||||
|
||||
bool process_mads_heartbeat(SubMaster *sm) {
|
||||
const int &alt_exp = (*sm)["carParams"].getCarParams().getAlternativeExperience();
|
||||
const bool disengage_lateral_on_brake = (alt_exp & ALT_EXP_MADS_DISENGAGE_LATERAL_ON_BRAKE) != 0;
|
||||
|
||||
const auto &mads = (*sm)["selfdriveStateSP"].getSelfdriveStateSP().getMads();
|
||||
return sm->allAliveAndValid({"selfdriveStateSP"}) && mads.getEnabled();
|
||||
const bool heartbeat_type = disengage_lateral_on_brake ? mads.getActive() : mads.getEnabled();
|
||||
|
||||
const bool engaged = sm->allAliveAndValid({"selfdriveStateSP"}) && heartbeat_type;
|
||||
|
||||
return engaged;
|
||||
}
|
||||
|
||||
Panda *connect(std::string serial) {
|
||||
@@ -143,8 +152,6 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda
|
||||
ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f);
|
||||
ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f);
|
||||
ps.setSoundOutputLevel(health.sound_output_level_pkt);
|
||||
ps.setControlsAllowedLateral(health.controls_allowed_lateral_pkt);
|
||||
ps.setControlsAllowedLongitudinal(health.controls_allowed_longitudinal_pkt);
|
||||
}
|
||||
|
||||
void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const can_health_t &can_health) {
|
||||
@@ -299,7 +306,7 @@ void process_panda_state(Panda *panda, PubMaster *pm, bool engaged, bool engaged
|
||||
panda->send_heartbeat(engaged, engaged_mads);
|
||||
}
|
||||
|
||||
void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, bool is_onroad) {
|
||||
void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) {
|
||||
static Params params;
|
||||
static SubMaster sm({"deviceState", "driverCameraState"});
|
||||
|
||||
@@ -309,8 +316,6 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control,
|
||||
static int prev_ir_pwr = 999;
|
||||
static uint32_t prev_frame_id = UINT32_MAX;
|
||||
static bool driver_view = false;
|
||||
static bool not_car = false;
|
||||
static bool not_car_checked = false;
|
||||
|
||||
// TODO: can we merge these?
|
||||
static FirstOrderFilter integ_lines_filter(0, 30.0, 0.05);
|
||||
@@ -356,21 +361,6 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control,
|
||||
ir_pwr = 0;
|
||||
}
|
||||
|
||||
// turn off IR leds if body
|
||||
if (!not_car_checked && is_onroad) {
|
||||
std::string cp_bytes = params.get("CarParams");
|
||||
if (cp_bytes.size() > 0) {
|
||||
AlignedBuffer aligned_buf;
|
||||
capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
|
||||
cereal::CarParams::Reader CP = cmsg.getRoot<cereal::CarParams>();
|
||||
not_car = CP.getNotCar();
|
||||
not_car_checked = true;
|
||||
}
|
||||
}
|
||||
if (not_car) {
|
||||
ir_pwr = 0;
|
||||
}
|
||||
|
||||
if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0) {
|
||||
int16_t ir_panda = util::map_val(ir_pwr, 0, 100, 0, MAX_IR_PANDA_VAL);
|
||||
panda->set_ir_pwr(ir_panda);
|
||||
@@ -390,7 +380,7 @@ void pandad_run(Panda *panda) {
|
||||
|
||||
Params params;
|
||||
RateKeeper rk("pandad", 100);
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP"});
|
||||
SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"});
|
||||
PubMaster pm({"can", "pandaStates", "peripheralState"});
|
||||
PandaSafety panda_safety(panda);
|
||||
bool engaged = false;
|
||||
@@ -404,7 +394,7 @@ void pandad_run(Panda *panda) {
|
||||
|
||||
// Process peripheral state at 20 Hz
|
||||
if (rk.frame() % 5 == 0) {
|
||||
process_peripheral_state(panda, &pm, no_fan_control, is_onroad);
|
||||
process_peripheral_state(panda, &pm, no_fan_control);
|
||||
}
|
||||
|
||||
// Process panda state at 10 Hz
|
||||
|
||||
@@ -230,7 +230,7 @@ class SelfdriveD(CruiseHelper):
|
||||
|
||||
if self.CP.notCar:
|
||||
# wait for everything to init first
|
||||
if self.sm.frame > int(2. / DT_CTRL) and self.initialized:
|
||||
if self.sm.frame > int(5. / DT_CTRL) and self.initialized:
|
||||
# body always wants to enable
|
||||
self.events.add(EventName.pcmEnable)
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def _diff_capnp_values(v1, v2, path, tolerance):
|
||||
for i in range(n):
|
||||
yield from _diff_capnp_values(v1[i], v2[i], path + (str(i),), tolerance)
|
||||
if n2 > n:
|
||||
yield 'add', dot, [(i, v2[i]) for i in range(n, n2)]
|
||||
yield 'add', dot, list(enumerate(v2[n:], n))
|
||||
if n1 > n:
|
||||
yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)]))
|
||||
|
||||
|
||||
@@ -49,8 +49,6 @@ def diff_format(diffs, ref, new, field) -> list[str]:
|
||||
msg_type = field.split(".")[0]
|
||||
ref_ts = [(m.logMonoTime, MsgWrap(m)) for m in ref.get(msg_type, [])]
|
||||
new_wrapped = [MsgWrap(m) for m in new.get(msg_type, [])]
|
||||
if not ref_ts or not new_wrapped:
|
||||
return format_numeric_diffs(diffs)
|
||||
return format_diff(diffs, ref_ts, new_wrapped, field)
|
||||
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ def generate_report(proposed, master, tmp, commit):
|
||||
(lambda x: get_idx_if_non_empty(x.wheelOnRightProb), "wheelOnRightProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceProb), "leftDriverData.faceProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.faceOrientation, 0), "leftDriverData.faceOrientation0"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.leftBlinkProb), "leftDriverData.leftBlinkProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.eyesClosedProb), "leftDriverData.eyesClosedProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.leftDriverData.phoneProb), "leftDriverData.phoneProb"),
|
||||
(lambda x: get_idx_if_non_empty(x.rightDriverData.faceProb), "rightDriverData.faceProb"),
|
||||
], "driverStateV2")
|
||||
|
||||
@@ -36,7 +36,7 @@ class DeveloperLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# Build items and keep references for callbacks/state updates
|
||||
self._adb_toggle = toggle_item(
|
||||
|
||||
@@ -42,7 +42,7 @@ class TogglesLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._params = Params()
|
||||
self._is_release = False # self._params.get_bool("IsReleaseBranch")
|
||||
self._is_release = self._params.get_bool("IsReleaseBranch")
|
||||
|
||||
# param, title, desc, icon, needs_restart
|
||||
self._toggle_defs = {
|
||||
|
||||
@@ -7,15 +7,13 @@ from collections.abc import Callable
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.layouts import HBoxLayout
|
||||
from openpilot.system.ui.widgets.icon_widget import IconWidget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.version import RELEASE_BRANCHES
|
||||
|
||||
HEAD_BUTTON_FONT_SIZE = 40
|
||||
HOME_PADDING = 8
|
||||
SETTINGS_ZONE_WIDTH = 280
|
||||
ALERTS_ZONE_WIDTH = 180
|
||||
|
||||
NetworkType = log.DeviceState.NetworkType
|
||||
|
||||
@@ -30,37 +28,6 @@ NETWORK_TYPES = {
|
||||
}
|
||||
|
||||
|
||||
class AlertsPill(Widget):
|
||||
ICON_OFFSET = 12
|
||||
COUNT_OFFSET = 40
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.set_rect(rl.Rectangle(0, 0, 104, 52))
|
||||
|
||||
self._pill_bg_txt = gui_app.texture("icons_mici/alerts_pill.png", 104, 52)
|
||||
self._warning_txt = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", 36, 36)
|
||||
self._alert_count_callback: Callable[[], int] | None = None
|
||||
|
||||
def set_alert_count_callback(self, callback: Callable[[], int] | None):
|
||||
self._alert_count_callback = callback
|
||||
|
||||
def _render(self, _):
|
||||
alert_count = self._alert_count_callback() if self._alert_count_callback else 0
|
||||
if alert_count > 0:
|
||||
pill_w, pill_h = self._pill_bg_txt.width, self._pill_bg_txt.height
|
||||
rl.draw_texture_ex(self._pill_bg_txt, rl.Vector2(self.rect.x, self.rect.y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
warn_x = self.rect.x + self.ICON_OFFSET
|
||||
warn_y = self.rect.y + (pill_h - self._warning_txt.height) / 2
|
||||
rl.draw_texture_ex(self._warning_txt, rl.Vector2(warn_x, warn_y), 0.0, 1.0, rl.WHITE)
|
||||
|
||||
count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
|
||||
gui_label(count_rect, str(alert_count), font_size=36,
|
||||
alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
|
||||
alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
|
||||
|
||||
|
||||
class NetworkIcon(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -117,8 +84,6 @@ class MiciHomeLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._on_settings_click: Callable | None = None
|
||||
self._on_alerts_click: Callable | None = None
|
||||
self._alert_count_callback: Callable[[], int] | None = None
|
||||
|
||||
self._last_refresh = 0
|
||||
self._mouse_down_t: None | float = None
|
||||
@@ -131,8 +96,6 @@ class MiciHomeLayout(Widget):
|
||||
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
|
||||
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
|
||||
|
||||
self._alerts_pill = AlertsPill()
|
||||
|
||||
self._status_bar_layout = HBoxLayout([
|
||||
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
|
||||
NetworkIcon(),
|
||||
@@ -178,23 +141,13 @@ class MiciHomeLayout(Widget):
|
||||
self._last_refresh = rl.get_time()
|
||||
self._update_params()
|
||||
|
||||
def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
|
||||
alert_count_callback: Callable[[], int] | None = None):
|
||||
def set_callbacks(self, on_settings: Callable | None = None):
|
||||
self._on_settings_click = on_settings
|
||||
self._on_alerts_click = on_alerts
|
||||
self._alert_count_callback = alert_count_callback
|
||||
self._alerts_pill.set_alert_count_callback(alert_count_callback)
|
||||
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
if not self._did_long_press:
|
||||
relative_x = mouse_pos.x - self.rect.x
|
||||
has_alerts = self._alert_count_callback and self._alert_count_callback() > 0
|
||||
if relative_x < SETTINGS_ZONE_WIDTH:
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
elif has_alerts and relative_x > self.rect.width - ALERTS_ZONE_WIDTH:
|
||||
if self._on_alerts_click:
|
||||
self._on_alerts_click()
|
||||
if self._on_settings_click:
|
||||
self._on_settings_click()
|
||||
self._did_long_press = False
|
||||
|
||||
def _get_version_text(self) -> tuple[str, str, str, str] | None:
|
||||
@@ -250,8 +203,3 @@ class MiciHomeLayout(Widget):
|
||||
|
||||
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
|
||||
self._status_bar_layout.render(footer_rect)
|
||||
|
||||
# TODO: add alignment to hboxlayout and add to there
|
||||
self._alerts_pill.set_position(self.rect.x + self.rect.width - self._alerts_pill.rect.width - HOME_PADDING,
|
||||
self.rect.y + self.rect.height - self._alerts_pill.rect.height)
|
||||
self._alerts_pill.render()
|
||||
|
||||
@@ -13,6 +13,7 @@ from openpilot.system.ui.lib.application import gui_app
|
||||
if gui_app.sunnypilot_ui():
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.settings import SettingsLayoutSP as SettingsLayout
|
||||
|
||||
|
||||
ONROAD_DELAY = 2.5 # seconds
|
||||
|
||||
|
||||
@@ -60,11 +61,7 @@ class MiciMainLayout(Scroller):
|
||||
gui_app.push_widget(self._onboarding_window)
|
||||
|
||||
def _setup_callbacks(self):
|
||||
self._home_layout.set_callbacks(
|
||||
on_settings=lambda: gui_app.push_widget(self._settings_layout),
|
||||
on_alerts=lambda: self._scroll_to(self._alerts_layout),
|
||||
alert_count_callback=self._alerts_layout.active_alerts,
|
||||
)
|
||||
self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
|
||||
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
|
||||
device.add_interactive_timeout_callback(self._on_interactive_timeout)
|
||||
|
||||
@@ -72,11 +69,6 @@ class MiciMainLayout(Scroller):
|
||||
layout_x = int(layout.rect.x)
|
||||
self._scroller.scroll_to(layout_x, smooth=True)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
# TODO: Hack to run alert updates while not in view. Add a nav stack tick?
|
||||
self._alerts_layout._update_state()
|
||||
|
||||
def _render(self, _):
|
||||
if not self._setup:
|
||||
if self._alerts_layout.active_alerts() > 0:
|
||||
|
||||
@@ -178,8 +178,6 @@ class AugmentedRoadView(CameraView):
|
||||
# update offroad label
|
||||
if ui_state.panda_type == log.PandaState.PandaType.unknown:
|
||||
self._offroad_label.set_text("system booting")
|
||||
elif ui_state.ignition and not ui_state.started:
|
||||
self._offroad_label.set_text("openpilot can't start\ncheck alerts")
|
||||
else:
|
||||
self._offroad_label.set_text("start the car to\nuse sunnypilot")
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ class BaseDriverCameraDialog(Widget):
|
||||
self._eye_fill_texture = None
|
||||
self._eye_orange_texture = None
|
||||
self._eye_size = 74
|
||||
self._glasses_texture = None
|
||||
self._glasses_size = 171
|
||||
|
||||
self._load_eye_textures()
|
||||
|
||||
@@ -154,8 +152,6 @@ class BaseDriverCameraDialog(Widget):
|
||||
self._eye_fill_texture = gui_app.texture("icons_mici/onroad/eye_fill.png", self._eye_size, self._eye_size)
|
||||
if self._eye_orange_texture is None:
|
||||
self._eye_orange_texture = gui_app.texture("icons_mici/onroad/eye_orange.png", self._eye_size, self._eye_size)
|
||||
if self._glasses_texture is None:
|
||||
self._glasses_texture = gui_app.texture("icons_mici/onroad/glasses.png", self._glasses_size, self._glasses_size)
|
||||
|
||||
def _draw_face_detection(self, rect: rl.Rectangle):
|
||||
dm_state = ui_state.sm["driverMonitoringState"]
|
||||
@@ -202,31 +198,21 @@ class BaseDriverCameraDialog(Widget):
|
||||
eye_offset_x = 10
|
||||
eye_offset_y = 10
|
||||
eye_spacing = self._eye_size + 15
|
||||
eyes_prob = driver_data.eyesVisibleProb
|
||||
|
||||
left_eye_x = rect.x + eye_offset_x
|
||||
left_eye_y = rect.y + eye_offset_y
|
||||
left_eye_prob = driver_data.leftEyeProb
|
||||
|
||||
right_eye_x = rect.x + eye_offset_x + eye_spacing
|
||||
right_eye_y = rect.y + eye_offset_y
|
||||
right_eye_prob = driver_data.rightEyeProb
|
||||
|
||||
# Draw eyes with opacity based on probability
|
||||
for eye_x, eye_y, eye_prob in [(left_eye_x, left_eye_y, left_eye_prob), (right_eye_x, right_eye_y, right_eye_prob)]:
|
||||
fill_opacity = eye_prob
|
||||
orange_opacity = 1.0 - eye_prob
|
||||
|
||||
fill_opacity = eyes_prob
|
||||
orange_opacity = 1.0 - eyes_prob
|
||||
for eye_x, eye_y in [(left_eye_x, left_eye_y), (right_eye_x, right_eye_y)]:
|
||||
rl.draw_texture_v(self._eye_orange_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * orange_opacity)))
|
||||
rl.draw_texture_v(self._eye_fill_texture, (eye_x, eye_y), rl.Color(255, 255, 255, int(255 * fill_opacity)))
|
||||
|
||||
# Draw sunglasses indicator based on sunglasses probability
|
||||
# Position glasses centered between the two eyes at top left
|
||||
glasses_x = rect.x + eye_offset_x - 4
|
||||
glasses_y = rect.y
|
||||
glasses_pos = rl.Vector2(glasses_x, glasses_y)
|
||||
glasses_prob = driver_data.sunglassesProb
|
||||
rl.draw_texture_v(self._glasses_texture, glasses_pos, rl.Color(70, 80, 161, int(255 * glasses_prob)))
|
||||
|
||||
|
||||
class DriverCameraDialog(NavWidget, BaseDriverCameraDialog):
|
||||
def __init__(self):
|
||||
|
||||
@@ -6,13 +6,11 @@ from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
|
||||
|
||||
AlertSize = log.SelfdriveState.AlertSize
|
||||
|
||||
DEBUG = False
|
||||
|
||||
# TODO: Only left for DM preview, remove
|
||||
LOOKING_CENTER_THRESHOLD_UPPER = math.radians(6)
|
||||
LOOKING_CENTER_THRESHOLD_LOWER = math.radians(3)
|
||||
|
||||
@@ -61,6 +59,8 @@ class DriverStateRenderer(Widget):
|
||||
|
||||
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size)
|
||||
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
|
||||
center_size = round(36 / self.BASE_SIZE * self._rect.width)
|
||||
self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
|
||||
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height))
|
||||
|
||||
def set_should_draw(self, should_draw: bool):
|
||||
@@ -113,7 +113,16 @@ class DriverStateRenderer(Widget):
|
||||
dest_rect,
|
||||
rl.Vector2(dest_rect.width / 2, dest_rect.height / 2),
|
||||
self._rotation_filter.x - 90,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)),
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x * (1 - self._looking_center_filter.x))),
|
||||
)
|
||||
|
||||
rl.draw_texture_ex(
|
||||
self._dm_center,
|
||||
(int(self._rect.x + (self._rect.width - self._dm_center.width) / 2),
|
||||
int(self._rect.y + (self._rect.height - self._dm_center.height) / 2)),
|
||||
0,
|
||||
1.0,
|
||||
rl.Color(255, 255, 255, int(255 * self._fade_filter.x * self._looking_center_filter.x)),
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -165,22 +174,11 @@ class DriverStateRenderer(Widget):
|
||||
# Get monitoring state
|
||||
driver_data = self.get_driver_data()
|
||||
driver_orient = driver_data.faceOrientation
|
||||
driver_position = driver_data.facePosition
|
||||
|
||||
if len(driver_orient) != 3:
|
||||
return
|
||||
|
||||
# Calibrate orientation so looking straight ahead at road (instead of at device) is (0, 0, 0)
|
||||
sm = ui_state.sm
|
||||
if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
|
||||
cal_rpy = sm['liveCalibration'].rpyCalib
|
||||
else:
|
||||
cal_rpy = [0.0, 0.0, 0.0]
|
||||
|
||||
_, pitch, yaw = face_orientation_from_net(driver_orient, driver_position, cal_rpy)
|
||||
pitch += math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
|
||||
yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
|
||||
|
||||
pitch, yaw, roll = driver_orient
|
||||
pitch = self._pitch_filter.update(pitch)
|
||||
yaw = self._yaw_filter.update(yaw)
|
||||
|
||||
@@ -194,6 +192,7 @@ class DriverStateRenderer(Widget):
|
||||
if DEBUG:
|
||||
pitchd = math.degrees(pitch)
|
||||
yawd = math.degrees(yaw)
|
||||
rolld = math.degrees(roll)
|
||||
|
||||
rl.draw_line_ex((0, 100), (200, 100), 3, rl.RED)
|
||||
rl.draw_line_ex((0, 120), (200, 120), 3, rl.RED)
|
||||
@@ -201,11 +200,13 @@ class DriverStateRenderer(Widget):
|
||||
|
||||
pitch_x = 100 + pitchd
|
||||
yaw_x = 100 + yawd
|
||||
roll_x = 100 + rolld
|
||||
rl.draw_circle(int(pitch_x), 100, 5, rl.GREEN)
|
||||
rl.draw_circle(int(yaw_x), 120, 5, rl.GREEN)
|
||||
rl.draw_circle(int(roll_x), 140, 5, rl.GREEN)
|
||||
|
||||
# filter head rotation, handling wrap-around
|
||||
rotation = math.degrees(math.atan2(pitch * 2, yaw)) # reduce yaw sensitivity
|
||||
rotation = math.degrees(math.atan2(pitch, yaw))
|
||||
angle_diff = rotation - self._rotation_filter.x
|
||||
angle_diff = ((angle_diff + 180) % 360) - 180
|
||||
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
|
||||
|
||||
@@ -6,10 +6,12 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.system.ui.sunnypilot.widgets.option_control import OptionControlSP
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets.scroller_tici import Scroller
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import option_item_sp
|
||||
from openpilot.system.ui.sunnypilot.widgets.list_view import toggle_item_sp, option_item_sp, ToggleActionSP
|
||||
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
|
||||
|
||||
|
||||
@@ -23,6 +25,7 @@ class DisplayLayout(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self._params = Params()
|
||||
items = self._initialize_items()
|
||||
self._scroller = Scroller(items, line_separator=True, spacing=0)
|
||||
|
||||
@@ -61,10 +64,27 @@ class DisplayLayout(Widget):
|
||||
f"{value} s" if value < 60 else f"{int(value/60)} m"),
|
||||
inline=True
|
||||
)
|
||||
self._screensaver_toggle = toggle_item_sp(
|
||||
param="ScreenSaverEnabled",
|
||||
title=lambda: tr("Enable sunnypilot Screen Saver"),
|
||||
description=lambda: tr("Enable screen saver when the device is offroad & idle. " +
|
||||
"The screen saver will kick off after the interactivity timeout expires, and will stay on for the duration configured below."),
|
||||
)
|
||||
self._screensaver_timeout = option_item_sp(
|
||||
param="ScreenSaverTimeout",
|
||||
title=lambda: tr("Screen Saver Timeout"),
|
||||
description=lambda: tr("Configure how long the screen saver should stay on after the interactivity timeout expires."),
|
||||
min_value=60,
|
||||
max_value=600,
|
||||
value_change_step=60,
|
||||
label_callback=lambda value: f"{int(value/60)} m"
|
||||
)
|
||||
items = [
|
||||
self._onroad_brightness,
|
||||
self._onroad_brightness_timer,
|
||||
self._interactivity_timeout,
|
||||
self._screensaver_toggle,
|
||||
self._screensaver_timeout,
|
||||
]
|
||||
return items
|
||||
|
||||
@@ -84,9 +104,21 @@ class DisplayLayout(Widget):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
brightness_val = self._onroad_brightness.action_item.current_value
|
||||
for _item in self._scroller._items:
|
||||
if isinstance(_item.action_item, ToggleActionSP) and _item.action_item.toggle.param_key is not None:
|
||||
_item.action_item.set_state(self._params.get_bool(_item.action_item.toggle.param_key))
|
||||
elif isinstance(_item.action_item, OptionControlSP) and _item.action_item.param_key is not None:
|
||||
raw_value = self._params.get(_item.action_item.param_key, return_default=True)
|
||||
if _item.action_item.value_map:
|
||||
reverse_map = {v: k for k, v in _item.action_item.value_map.items()}
|
||||
raw_value = reverse_map.get(raw_value, _item.action_item.current_value)
|
||||
_item.action_item.set_value(raw_value)
|
||||
|
||||
brightness_val = self._params.get("OnroadScreenOffBrightness", return_default=True)
|
||||
self._onroad_brightness_timer.action_item.set_enabled(brightness_val not in (OnroadBrightness.AUTO, OnroadBrightness.AUTO_DARK))
|
||||
|
||||
self._screensaver_timeout.set_visible(self._screensaver_toggle.action_item.get_state())
|
||||
|
||||
def _render(self, rect):
|
||||
self._scroller.render(rect)
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
self._initialize_items()
|
||||
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
for ctrl, key in [(self.lane_turn_value_control, "LaneTurnValue"), (self.delay_control, "LagdToggleDelay")]:
|
||||
ctrl.action_item.set_value(int(float(ui_state.params.get(key, return_default=True)) * 100))
|
||||
|
||||
@@ -112,7 +112,7 @@ class ModelsLayout(Widget):
|
||||
self.model_manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
|
||||
@staticmethod
|
||||
def calculate_cache_size():
|
||||
def _calculate_cache_size():
|
||||
cache_size = 0.0
|
||||
if os.path.exists(CUSTOM_MODEL_PATH):
|
||||
cache_size = sum(os.path.getsize(os.path.join(CUSTOM_MODEL_PATH, file)) for file in os.listdir(CUSTOM_MODEL_PATH)) / (1024**2)
|
||||
@@ -122,7 +122,7 @@ class ModelsLayout(Widget):
|
||||
def _callback(response):
|
||||
if response == DialogResult.CONFIRM:
|
||||
ui_state.params.put_bool("ModelManager_ClearCache", True)
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
dialog = ConfirmDialog(tr("This will delete ALL downloaded models from the cache except the currently active model. Are you sure?"),
|
||||
tr("Clear Cache"), callback=_callback)
|
||||
@@ -155,7 +155,7 @@ class ModelsLayout(Widget):
|
||||
|
||||
if (current_time := time.monotonic()) - self.last_cache_calc_time > 0.5:
|
||||
self.last_cache_calc_time = current_time
|
||||
self.clear_cache_item.action_item.set_value(f"{self.calculate_cache_size():.2f} MB")
|
||||
self.clear_cache_item.action_item.set_value(f"{self._calculate_cache_size():.2f} MB")
|
||||
|
||||
if self.download_status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
device._reset_interactive_timeout()
|
||||
|
||||
@@ -120,12 +120,20 @@ class SteeringLayout(Widget):
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
torque_allowed = ui_state.CP is not None and ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle
|
||||
torque_allowed = True
|
||||
if ui_state.CP is not None:
|
||||
mads_main_desc = self._mads_limited_desc if self._mads_settings_layout._mads_limited_settings() else self._mads_full_desc
|
||||
self._mads_toggle.set_description(f"<b>{mads_main_desc}</b><br><br>{self._mads_base_desc}")
|
||||
|
||||
if ui_state.CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||
ui_state.params.remove("EnforceTorqueControl")
|
||||
ui_state.params.remove("NeuralNetworkLateralControl")
|
||||
torque_allowed = False
|
||||
else:
|
||||
self._mads_toggle.set_description(f"<b>{self._mads_check_compat_desc}</b><br><br>{self._mads_base_desc}")
|
||||
ui_state.params.remove("EnforceTorqueControl")
|
||||
ui_state.params.remove("NeuralNetworkLateralControl")
|
||||
torque_allowed = False
|
||||
|
||||
self._mads_toggle.action_item.set_enabled(ui_state.is_offroad())
|
||||
self._mads_settings_button.action_item.set_enabled(ui_state.is_offroad() and self._mads_toggle.action_item.get_state())
|
||||
|
||||
@@ -5,45 +5,13 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from collections.abc import Callable
|
||||
import pyray as rl
|
||||
|
||||
from cereal import custom
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.models import ModelsLayout
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state, device
|
||||
from openpilot.system.ui.lib.application import FontWeight, gui_app
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
|
||||
class CurrentModelInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self.current_model_header = UnifiedLabel(tr("active model"), 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.current_model_text = UnifiedLabel(tr("default model"), 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN, scroll=True)
|
||||
|
||||
self.info_header = UnifiedLabel("cache size", 48, max_width=max_width, text_color=header_color, font_weight=FontWeight.DISPLAY)
|
||||
self.info_text = UnifiedLabel("0 mb", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def _render(self, _):
|
||||
self.current_model_header.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self.current_model_header.render()
|
||||
|
||||
self.current_model_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self.current_model_text.render()
|
||||
|
||||
self.info_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self.info_header.render()
|
||||
|
||||
self.info_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.info_text.render()
|
||||
|
||||
class ModelsLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
@@ -52,35 +20,25 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.original_back_callback = back_callback
|
||||
self.focused_widget = None
|
||||
|
||||
self.current_model_info = CurrentModelInfo()
|
||||
self._download_progress = "."
|
||||
self._download_frame = 0
|
||||
self._was_downloading = False
|
||||
|
||||
self.select_model_btn = BigButton(tr("select model"))
|
||||
self.select_model_btn.set_click_callback(self._show_folders)
|
||||
self.current_model_btn = BigButton(tr("current model"))
|
||||
self.current_model_btn.set_click_callback(self._show_folders)
|
||||
|
||||
self.cancel_download_btn = BigButton(tr("cancel download"))
|
||||
self.cancel_download_btn.set_click_callback(lambda: ui_state.params.remove("ModelManager_DownloadIndex"))
|
||||
|
||||
self.main_items = [self.current_model_info, self.select_model_btn, self.cancel_download_btn]
|
||||
self.main_items = [self.current_model_btn, self.cancel_download_btn]
|
||||
self._scroller.add_widgets(self.main_items)
|
||||
|
||||
@property
|
||||
def model_manager(self):
|
||||
return ui_state.sm["modelManagerSP"]
|
||||
|
||||
def _get_grouped_bundles(self, favorites = None):
|
||||
def _get_grouped_bundles(self):
|
||||
bundles = self.model_manager.availableBundles
|
||||
folders = {}
|
||||
for bundle in bundles:
|
||||
folder = next((override.value for override in bundle.overrides if override.key == "folder"), "")
|
||||
folders.setdefault(folder, []).append(bundle)
|
||||
|
||||
if favorites:
|
||||
for fav_bundle in [bundle for bundle in bundles if bundle.ref in favorites]:
|
||||
folders.setdefault("favorites", []).append(fav_bundle)
|
||||
|
||||
return folders
|
||||
|
||||
def _show_selection_view(self, items, back_callback: Callable):
|
||||
@@ -91,25 +49,18 @@ class ModelsLayoutMici(NavScroller):
|
||||
self.set_back_callback(back_callback)
|
||||
|
||||
def _show_folders(self):
|
||||
self.focused_widget = self.select_model_btn
|
||||
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
self.focused_widget = self.current_model_btn
|
||||
folders = self._get_grouped_bundles()
|
||||
folder_buttons = []
|
||||
default_btn = BigButton(tr("default model"))
|
||||
default_btn.set_click_callback(self._select_default)
|
||||
folder_buttons.append(default_btn)
|
||||
|
||||
for folder in sorted(folders.keys(), key=lambda f: max((bundle.index for bundle in folders[f]), default=-1), reverse=True):
|
||||
if folder.lower() in ["release models", "master models", "favorites"]:
|
||||
if folder.lower() in ["release models", "master models"]:
|
||||
btn = BigButton(folder.lower())
|
||||
btn.set_click_callback(lambda f=folder: self._select_folder(f))
|
||||
if folder.lower() == "favorites":
|
||||
folder_buttons.insert(0, btn)
|
||||
else:
|
||||
folder_buttons.append(btn)
|
||||
folder_buttons.append(btn)
|
||||
self._show_selection_view(folder_buttons, self._reset_main_view)
|
||||
|
||||
def _select_model(self, bundle):
|
||||
@@ -121,10 +72,7 @@ class ModelsLayoutMici(NavScroller):
|
||||
self._reset_main_view()
|
||||
|
||||
def _select_folder(self, folder_name):
|
||||
favs = ui_state.params.get("ModelManager_Favs")
|
||||
favorites = set(favs.split(';')) if favs else set()
|
||||
|
||||
folders = self._get_grouped_bundles(favorites)
|
||||
folders = self._get_grouped_bundles()
|
||||
bundles = sorted(folders.get(folder_name, []), key=lambda b: b.index, reverse=True)
|
||||
|
||||
btns = []
|
||||
@@ -138,62 +86,29 @@ class ModelsLayoutMici(NavScroller):
|
||||
def _reset_main_view(self):
|
||||
self._scroller._items = self.main_items
|
||||
self.set_back_callback(self.original_back_callback)
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(0)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
if self._was_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = False
|
||||
if self.focused_widget and self.focused_widget in self.main_items:
|
||||
x = self._scroller._pad
|
||||
for item in self.main_items:
|
||||
if not item.is_visible:
|
||||
continue
|
||||
if item == self.focused_widget:
|
||||
break
|
||||
x += item.rect.width + self._scroller._spacing
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
self._scroller.scroll_to(x)
|
||||
self.focused_widget = None
|
||||
else:
|
||||
self._scroller.scroll_panel.set_offset(0)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
self.select_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_info.current_model_header._shimmer = False
|
||||
self.current_model_info.info_header._shimmer = False
|
||||
|
||||
manager = self.model_manager
|
||||
self._download_frame += 1
|
||||
should_update = self._download_frame % (gui_app.target_fps / 2) == 0
|
||||
if should_update:
|
||||
self._download_progress = self._download_progress + "." if len(self._download_progress) < 3 else ""
|
||||
|
||||
is_downloading = (manager.selectedBundle
|
||||
and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading)
|
||||
if self._was_downloading and not is_downloading:
|
||||
device.set_override_interactive_timeout(None)
|
||||
self._was_downloading = is_downloading
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("active model"))
|
||||
self.current_model_info.current_model_text.set_text(manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else tr("default model"))
|
||||
self.current_model_info.info_header.set_text(tr("cache size"))
|
||||
self.current_model_info.info_text.set_text(f"{ModelsLayout.calculate_cache_size():.2f} MB")
|
||||
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.failed:
|
||||
self.current_model_info.info_header.set_text(tr("error") + self._download_progress)
|
||||
self.current_model_info.info_text.set_text(tr("download failed"))
|
||||
|
||||
elif manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
if manager.selectedBundle and manager.selectedBundle.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
self.current_model_btn.set_value("downloading...")
|
||||
self.cancel_download_btn.set_visible(True)
|
||||
device.set_override_interactive_timeout(5)
|
||||
progress = 0.0
|
||||
count = 0
|
||||
for model in manager.selectedBundle.models:
|
||||
count += 1
|
||||
p = model.artifact.downloadProgress
|
||||
if p.status == custom.ModelManagerSP.DownloadStatus.downloading:
|
||||
progress += p.progress
|
||||
elif p.status in (custom.ModelManagerSP.DownloadStatus.downloaded,
|
||||
custom.ModelManagerSP.DownloadStatus.cached):
|
||||
progress += 100.0
|
||||
|
||||
self.current_model_info.current_model_header.set_text(tr("downloading"))
|
||||
self.current_model_info.current_model_header._shimmer = True
|
||||
self.current_model_info.current_model_text.set_text(f"{manager.selectedBundle.internalName.lower()}")
|
||||
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_text.set_text(f"{progress/count:.2f}%")
|
||||
|
||||
else:
|
||||
self.current_model_btn.set_value(manager.activeBundle.internalName.lower() if manager.activeBundle else tr("default model"))
|
||||
self.cancel_download_btn.set_visible(False)
|
||||
self.current_model_btn.set_enabled(ui_state.is_offroad())
|
||||
self.current_model_btn.set_text(tr("current model"))
|
||||
|
||||
@@ -5,88 +5,30 @@ This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings import settings as OP
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.settings import SettingsBigButton
|
||||
from openpilot.selfdrive.ui.mici.layouts.settings.device import DeviceLayoutMici
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
|
||||
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
|
||||
from openpilot.selfdrive.ui.mici.widgets.button import BigButton
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
|
||||
ICON_SIZE = 70
|
||||
BIG_ICON_SIZE = 110
|
||||
|
||||
|
||||
class SettingsLayoutSP(OP.SettingsLayout):
|
||||
def __init__(self):
|
||||
OP.SettingsLayout.__init__(self)
|
||||
|
||||
device_panel = DeviceLayoutMici()
|
||||
self._scroller._items[2].set_click_callback(lambda: gui_app.push_widget(device_panel))
|
||||
|
||||
self.icon_offroad_enable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/always_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_disable = gui_app.texture("../../sunnypilot/selfdrive/assets/icons_mici/disable_offroad.png", BIG_ICON_SIZE,
|
||||
BIG_ICON_SIZE)
|
||||
self.icon_offroad_slider = gui_app.texture("icons_mici/settings/device/lkas.png", BIG_ICON_SIZE, BIG_ICON_SIZE)
|
||||
|
||||
sunnylink_panel = SunnylinkLayoutMici(back_callback=gui_app.pop_widget)
|
||||
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
|
||||
sunnylink_btn = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
|
||||
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
|
||||
|
||||
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
|
||||
models_btn = SettingsBigButton(tr("models"), "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
|
||||
models_btn.set_click_callback(lambda: gui_app.push_widget(models_panel))
|
||||
|
||||
# onroad: enable button sits at the front (left of toggles)
|
||||
self._enable_offroad_btn_onroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_onroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_onroad.set_visible(lambda: ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
# offroad: enable button sits at the end (right of developer)
|
||||
self._enable_offroad_btn_offroad = BigCircleButton(self.icon_offroad_enable, red=True)
|
||||
self._enable_offroad_btn_offroad.set_click_callback(lambda: self._handle_always_offroad(True))
|
||||
self._enable_offroad_btn_offroad.set_visible(lambda: not ui_state.started and not ui_state.always_offroad)
|
||||
|
||||
self._disable_offroad_btn = BigCircleButton(self.icon_offroad_disable, red=False)
|
||||
self._disable_offroad_btn.set_click_callback(lambda: self._handle_always_offroad(False))
|
||||
self._disable_offroad_btn.set_visible(lambda: ui_state.always_offroad)
|
||||
|
||||
items = self._scroller._items.copy()
|
||||
|
||||
items.insert(1, sunnylink_btn)
|
||||
items.insert(2, models_btn)
|
||||
|
||||
# front slots (only one ever visible at a time): exit-always-offroad, then enable-onroad
|
||||
items.insert(0, self._enable_offroad_btn_onroad)
|
||||
items.insert(0, self._disable_offroad_btn)
|
||||
# end slot: enable-offroad (right of developer)
|
||||
items.append(self._enable_offroad_btn_offroad)
|
||||
|
||||
self._scroller._items.clear()
|
||||
for item in items:
|
||||
self._scroller.add_widget(item)
|
||||
|
||||
def _update_state(self):
|
||||
super()._update_state()
|
||||
|
||||
def _handle_always_offroad(self, enable: bool):
|
||||
|
||||
def _set_offroad_status(status: bool):
|
||||
if not ui_state.engaged:
|
||||
ui_state.params.put_bool("OffroadMode", status)
|
||||
ui_state.always_offroad = status
|
||||
|
||||
if not enable:
|
||||
dlg = BigConfirmationDialog(tr("slide to exit always offroad"), self.icon_offroad_slider, red=False,
|
||||
confirm_callback=lambda: _set_offroad_status(False))
|
||||
else:
|
||||
if ui_state.engaged:
|
||||
gui_app.push_widget(BigDialog(tr("disengage to enable always offroad"), "", ))
|
||||
return
|
||||
|
||||
dlg = BigConfirmationDialog(tr("slide to force offroad"), self.icon_offroad_slider, red=True,
|
||||
confirm_callback=lambda: _set_offroad_status(True))
|
||||
gui_app.push_widget(dlg)
|
||||
|
||||
@@ -4,8 +4,6 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import pyray as rl
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from cereal import custom
|
||||
@@ -15,43 +13,11 @@ from openpilot.selfdrive.ui.sunnypilot.mici.layouts.onboarding import SunnylinkC
|
||||
from openpilot.selfdrive.ui.sunnypilot.mici.widgets.sunnylink_pairing_dialog import SunnylinkPairingDialog
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
from openpilot.sunnypilot.sunnylink.api import UNREGISTERED_SUNNYLINK_DONGLE_ID
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
|
||||
from openpilot.system.ui.lib.application import gui_app, MousePos
|
||||
from openpilot.system.ui.lib.multilang import tr
|
||||
from openpilot.system.ui.widgets import Widget
|
||||
from openpilot.system.ui.widgets.label import UnifiedLabel
|
||||
from openpilot.system.ui.widgets.scroller import NavScroller
|
||||
from openpilot.system.version import sunnylink_consent_version, sunnylink_consent_declined
|
||||
|
||||
class SunnylinkInfo(Widget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.set_rect(rl.Rectangle(0, 0, 360, 180))
|
||||
|
||||
header_color = rl.Color(255, 255, 255, int(255 * 0.9))
|
||||
subheader_color = rl.Color(255, 255, 255, int(255 * 0.9 * 0.65))
|
||||
max_width = int(self._rect.width - 20)
|
||||
self.device_id_header = UnifiedLabel(tr("device id"), 48, max_width=max_width, text_color=header_color,
|
||||
font_weight=FontWeight.DISPLAY, shimmer=True)
|
||||
self.device_id_text = UnifiedLabel(UNREGISTERED_SUNNYLINK_DONGLE_ID, 32, max_width=max_width, text_color=subheader_color,
|
||||
font_weight=FontWeight.ROMAN, scroll=True)
|
||||
|
||||
self.sponsor_header = UnifiedLabel(tr("sponsor tier"), 48, max_width=max_width, text_color=header_color,
|
||||
font_weight=FontWeight.DISPLAY, shimmer=True)
|
||||
self.sponsor_text = UnifiedLabel("N/A", 32, max_width=max_width, text_color=subheader_color, font_weight=FontWeight.ROMAN)
|
||||
|
||||
def _render(self, _):
|
||||
self.device_id_header.set_position(self._rect.x + 20, self._rect.y - 10)
|
||||
self.device_id_header.render()
|
||||
|
||||
self.device_id_text.set_position(self._rect.x + 20, self._rect.y + 68 - 25)
|
||||
self.device_id_text.render()
|
||||
|
||||
self.sponsor_header.set_position(self._rect.x + 20, self._rect.y + 114 - 30)
|
||||
self.sponsor_header.render()
|
||||
|
||||
self.sponsor_text.set_position(self._rect.x + 20, self._rect.y + 161 - 25)
|
||||
self.sponsor_text.render()
|
||||
|
||||
class SunnylinkLayoutMici(NavScroller):
|
||||
def __init__(self, back_callback: Callable):
|
||||
@@ -61,8 +27,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
self._backup_in_progress = False
|
||||
self._sunnylink_enabled = ui_state.params.get("SunnylinkEnabled")
|
||||
|
||||
self._sunnylink_info = SunnylinkInfo()
|
||||
|
||||
self._sunnylink_toggle = BigToggle(text=tr("enable sunnylink"),
|
||||
initial_state=self._sunnylink_enabled,
|
||||
toggle_callback=self._sunnylink_toggle_callback)
|
||||
@@ -76,7 +40,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
toggle_callback=self._sunnylink_uploader_callback)
|
||||
|
||||
self._scroller.add_widgets([
|
||||
self._sunnylink_info,
|
||||
self._sunnylink_toggle,
|
||||
self._sunnylink_sponsor_button,
|
||||
self._sunnylink_pair_button,
|
||||
@@ -96,10 +59,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
self._sunnylink_uploader_toggle.set_visible(self._sunnylink_enabled)
|
||||
self.handle_backup_restore_progress()
|
||||
|
||||
self._sunnylink_info.device_id_text.set_text(ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID)
|
||||
self._sunnylink_info.sponsor_text.set_text(ui_state.sunnylink_state.get_sponsor_tier().name.lower() or "N/A")
|
||||
self._sunnylink_info.set_visible(self._sunnylink_enabled)
|
||||
|
||||
if ui_state.sunnylink_state.is_sponsor():
|
||||
self._sunnylink_sponsor_button.set_text(tr("thanks"))
|
||||
self._sunnylink_sponsor_button.set_value(ui_state.sunnylink_state.get_sponsor_tier().name.lower())
|
||||
@@ -116,11 +75,6 @@ class SunnylinkLayoutMici(NavScroller):
|
||||
def show_event(self):
|
||||
super().show_event()
|
||||
ui_state.update_params()
|
||||
ui_state.sunnylink_state.set_settings_open(True)
|
||||
|
||||
def hide_event(self):
|
||||
super().hide_event()
|
||||
ui_state.sunnylink_state.set_settings_open(False)
|
||||
|
||||
@staticmethod
|
||||
def _sunnylink_toggle_callback(state: bool):
|
||||
@@ -240,14 +194,9 @@ class SunnylinkPairBigButton(BigButton):
|
||||
def _handle_mouse_release(self, mouse_pos: MousePos):
|
||||
super()._handle_mouse_release(mouse_pos)
|
||||
|
||||
network_type = ui_state.sm["deviceState"].networkType
|
||||
|
||||
dlg: BigDialog | SunnylinkPairingDialog | None = None
|
||||
|
||||
if network_type == 0:
|
||||
dlg = BigDialog(tr("no internet"), tr("please connect to WiFi & try again"))
|
||||
elif UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
dlg = BigDialog(tr("sunnylink dongle id not found"), tr("please reboot & try again"))
|
||||
if UNREGISTERED_SUNNYLINK_DONGLE_ID == (ui_state.params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID):
|
||||
dlg = BigDialog(tr("sunnylink Dongle ID not found. Please reboot & try again."), "")
|
||||
elif self.sponsor_pairing:
|
||||
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
|
||||
elif not self.sponsor_pairing:
|
||||
|
||||
@@ -141,8 +141,7 @@ class DeveloperUiRenderer(Widget):
|
||||
|
||||
# Add torque-specific elements if using torque control
|
||||
if sm['controlsState'].lateralControlState.which() == 'torqueState':
|
||||
override_active = ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled
|
||||
if sm.valid['liveTorqueParameters'] or override_active:
|
||||
if sm.valid['liveTorqueParameters']:
|
||||
elements.extend([
|
||||
self.friction_elem.update(sm, ui_state.is_metric),
|
||||
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),
|
||||
|
||||
@@ -8,7 +8,8 @@ import pyray as rl
|
||||
from dataclasses import dataclass
|
||||
|
||||
from openpilot.common.constants import CV
|
||||
from openpilot.selfdrive.ui.ui_state import ui_state
|
||||
|
||||
|
||||
from openpilot.system.ui.lib.text_measure import measure_text_cached
|
||||
|
||||
|
||||
@@ -247,12 +248,12 @@ class FrictionCoefficientElement:
|
||||
self.unit = ""
|
||||
|
||||
def update(self, sm, is_metric: bool) -> UiElement:
|
||||
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
|
||||
return UiElement(f"{ui_state.torque_override_friction:.3f}", "FRIC.", self.unit, rl.WHITE)
|
||||
|
||||
ltp = sm['liveTorqueParameters']
|
||||
value = f"{ltp.frictionCoefficientFiltered:.3f}"
|
||||
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
|
||||
friction_coef = ltp.frictionCoefficientFiltered
|
||||
live_valid = ltp.liveValid
|
||||
|
||||
value = f"{friction_coef:.3f}"
|
||||
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
|
||||
return UiElement(value, "FRIC.", self.unit, color)
|
||||
|
||||
|
||||
@@ -261,12 +262,12 @@ class LatAccelFactorElement:
|
||||
self.unit = ""
|
||||
|
||||
def update(self, sm, is_metric: bool) -> UiElement:
|
||||
if ui_state.enforce_torque_control and ui_state.custom_torque_params and ui_state.torque_override_enabled:
|
||||
return UiElement(f"{ui_state.torque_override_lat_accel_factor:.3f}", "L.A.F.", self.unit, rl.WHITE)
|
||||
|
||||
ltp = sm['liveTorqueParameters']
|
||||
value = f"{ltp.latAccelFactorFiltered:.3f}"
|
||||
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
|
||||
lat_accel_factor = ltp.latAccelFactorFiltered
|
||||
live_valid = ltp.liveValid
|
||||
|
||||
value = f"{lat_accel_factor:.3f}"
|
||||
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
|
||||
return UiElement(value, "L.A.F.", self.unit, color)
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,12 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
from cereal import messaging, log, car, custom
|
||||
from cereal import messaging, log, custom
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.ui.sunnypilot.layouts.settings.display import OnroadBrightness
|
||||
from openpilot.sunnypilot.sunnylink.sunnylink_state import SunnylinkState
|
||||
from openpilot.system.ui.lib.application import gui_app
|
||||
from openpilot.system.ui.sunnypilot.widgets.screen_saver import ScreenSaverSP
|
||||
|
||||
OpenpilotState = log.SelfdriveState.OpenpilotState
|
||||
MADSState = custom.ModularAssistiveDrivingSystem.ModularAssistiveDrivingSystemState
|
||||
@@ -26,20 +27,24 @@ class OnroadTimerStatus(Enum):
|
||||
|
||||
class UIStateSP:
|
||||
def __init__(self):
|
||||
self.params = Params()
|
||||
self.CP_SP: custom.CarParamsSP | None = None
|
||||
self.has_icbm: bool = False
|
||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||
self.params = Params()
|
||||
self.sm_services_ext = [
|
||||
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
|
||||
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
|
||||
]
|
||||
|
||||
self.sunnylink_state = SunnylinkState()
|
||||
self.screensaver = ScreenSaverSP()
|
||||
|
||||
self.update_params()
|
||||
|
||||
self.onroad_brightness_timer: int = 0
|
||||
self.custom_interactive_timeout: int = 0
|
||||
self._sp_initialized: bool = False
|
||||
self.custom_interactive_timeout: int = self.params.get("InteractivityTimeout", return_default=True)
|
||||
self.reset_onroad_sleep_timer()
|
||||
self.CP_SP: custom.CarParamsSP | None = None
|
||||
self.has_icbm: bool = False
|
||||
self.is_sp_release: bool = self.params.get_bool("IsReleaseSpBranch")
|
||||
|
||||
def update(self) -> None:
|
||||
if self.sunnylink_enabled:
|
||||
@@ -126,8 +131,6 @@ class UIStateSP:
|
||||
if CP_SP_bytes is not None:
|
||||
self.CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||
self.has_icbm = self.CP_SP.intelligentCruiseButtonManagementAvailable and self.params.get_bool("IntelligentCruiseButtonManagement")
|
||||
|
||||
self._enforce_constraints()
|
||||
self.active_bundle = self.params.get("ModelManager_ActiveBundle")
|
||||
self.blindspot = self.params.get_bool("BlindSpot")
|
||||
self.chevron_metrics = self.params.get("ChevronInfo")
|
||||
@@ -143,70 +146,30 @@ class UIStateSP:
|
||||
self.standstill_timer = self.params.get_bool("StandstillTimer")
|
||||
self.sunnylink_enabled = self.params.get_bool("SunnylinkEnabled")
|
||||
self.torque_bar = self.params.get_bool("TorqueBar")
|
||||
self.enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||
self.custom_torque_params = self.params.get_bool("CustomTorqueParams")
|
||||
self.torque_override_enabled = self.params.get_bool("TorqueParamsOverrideEnabled")
|
||||
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.true_v_ego_ui = self.params.get_bool("TrueVEgoUI")
|
||||
self.turn_signals = self.params.get_bool("ShowTurnSignals")
|
||||
self.boot_offroad_mode = self.params.get("DeviceBootMode", return_default=True)
|
||||
self.always_offroad = self.params.get_bool("OffroadMode")
|
||||
|
||||
if not self._sp_initialized:
|
||||
self._sp_initialized = True
|
||||
self.reset_onroad_sleep_timer()
|
||||
|
||||
def _enforce_constraints(self) -> None:
|
||||
has_long = self.has_longitudinal_control
|
||||
CP = self.CP
|
||||
|
||||
if CP is not None:
|
||||
# Angle steering: no torque-based lateral controls
|
||||
if CP.steerControlType == car.CarParams.SteerControlType.angle:
|
||||
self.params.remove("EnforceTorqueControl")
|
||||
self.params.remove("NeuralNetworkLateralControl")
|
||||
|
||||
# Alpha longitudinal: clear if not available
|
||||
if not CP.alphaLongitudinalAvailable:
|
||||
self.params.remove("AlphaLongitudinalEnabled")
|
||||
|
||||
# BSM not available: clear BSM-dependent settings
|
||||
if not CP.enableBsm:
|
||||
self.params.remove("AutoLaneChangeBsmDelay")
|
||||
else:
|
||||
# No CarParams: clear all car-dependent params as safety default
|
||||
self.params.remove("EnforceTorqueControl")
|
||||
self.params.remove("NeuralNetworkLateralControl")
|
||||
self.params.remove("AlphaLongitudinalEnabled")
|
||||
|
||||
# No longitudinal control: no experimental mode or DEC
|
||||
if not has_long:
|
||||
self.params.remove("ExperimentalMode")
|
||||
self.params.remove("DynamicExperimentalControl")
|
||||
|
||||
# ICBM: clear if not available or if full longitudinal control is active
|
||||
if self.CP_SP is not None:
|
||||
if not self.CP_SP.intelligentCruiseButtonManagementAvailable or has_long:
|
||||
self.params.remove("IntelligentCruiseButtonManagement")
|
||||
self.has_icbm = False
|
||||
else:
|
||||
self.params.remove("IntelligentCruiseButtonManagement")
|
||||
self.has_icbm = False
|
||||
|
||||
# Cruise features requiring longitudinal or ICBM
|
||||
if not (has_long or self.has_icbm):
|
||||
self.params.remove("CustomAccIncrementsEnabled")
|
||||
self.params.remove("SmartCruiseControlVision")
|
||||
self.params.remove("SmartCruiseControlMap")
|
||||
self.screensaver_enabled = self.params.get_bool("ScreenSaverEnabled")
|
||||
|
||||
|
||||
class DeviceSP:
|
||||
@staticmethod
|
||||
def _set_awake(on: bool, _ui_state):
|
||||
def _set_awake(_device, on: bool, _ui_state):
|
||||
if _ui_state.boot_offroad_mode == 1 and not on:
|
||||
_ui_state.params.put_bool("OffroadMode", True)
|
||||
|
||||
if not on and _ui_state.screensaver_enabled:
|
||||
if _ui_state.screensaver.was_dismissed:
|
||||
_ui_state.screensaver.deinit()
|
||||
gui_app.pop_widget()
|
||||
return True
|
||||
else:
|
||||
_ui_state.screensaver.initialize(dismiss_callback=lambda: _device._set_awake(False))
|
||||
gui_app.push_widget(_ui_state.screensaver)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def set_onroad_brightness(_ui_state, awake: bool, cur_brightness: float) -> float:
|
||||
if not awake or not _ui_state.started:
|
||||
@@ -215,6 +178,7 @@ class DeviceSP:
|
||||
if _ui_state.onroad_brightness_timer != 0:
|
||||
if _ui_state.onroad_brightness == OnroadBrightness.AUTO_DARK:
|
||||
return max(30.0, cur_brightness)
|
||||
# For AUTO (Default) and Manual modes (while timer running), use standard brightness
|
||||
return cur_brightness
|
||||
|
||||
# 0: Auto (Default), 1: Auto (Dark), 2: Screen Off
|
||||
|
||||
@@ -74,7 +74,7 @@ class UIState(UIStateSP):
|
||||
|
||||
# Core state variables
|
||||
self.is_metric: bool = self.params.get_bool("IsMetric")
|
||||
self.is_release = False # self.params.get_bool("IsReleaseBranch")
|
||||
self.is_release = self.params.get_bool("IsReleaseBranch")
|
||||
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
|
||||
self.started: bool = False
|
||||
self.ignition: bool = False
|
||||
@@ -299,11 +299,11 @@ class Device(DeviceSP):
|
||||
|
||||
def _set_awake(self, on: bool):
|
||||
if on != self._awake:
|
||||
DeviceSP._set_awake(on, ui_state)
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
if DeviceSP._set_awake(self, on, ui_state):
|
||||
self._awake = on
|
||||
cloudlog.debug(f"setting display power {int(on)}")
|
||||
HARDWARE.set_display_power(on)
|
||||
gui_app.set_should_render(on)
|
||||
|
||||
|
||||
# Global instance
|
||||
|
||||
@@ -1 +1 @@
|
||||
#define SUNNYPILOT_VERSION "2026.001.007"
|
||||
#define SUNNYPILOT_VERSION "2026.001.000"
|
||||
|
||||
@@ -33,7 +33,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.enabled = False
|
||||
self.active = False
|
||||
self.available = False
|
||||
self.lateral_mismatch_counter = 0
|
||||
self.allow_always = False
|
||||
self.no_main_cruise = False
|
||||
self.selfdrive = selfdrive
|
||||
@@ -105,17 +104,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.events.remove(old_event)
|
||||
self.events_sp.add(new_event)
|
||||
|
||||
def data_sample(self):
|
||||
# When the safety and selfdrived do not agree on controls_allowed_lateral
|
||||
# we want to disengage sunnypilot. However the status from the panda goes through
|
||||
# another socket other than the CAN messages and one can arrive earlier than the other.
|
||||
# Therefore we allow a mismatch for two samples, then we trigger the disengagement.
|
||||
if not self.active or self.selfdrive.enabled:
|
||||
self.lateral_mismatch_counter = 0
|
||||
elif any(not ps.controlsAllowedLateral for ps in self.selfdrive.sm['pandaStates']
|
||||
if ps.safetyModel not in IGNORED_SAFETY_MODES):
|
||||
self.lateral_mismatch_counter += 1
|
||||
|
||||
def update_events(self, CS: structs.CarState):
|
||||
if not self.selfdrive.enabled and self.enabled:
|
||||
if CS.standstill:
|
||||
@@ -147,7 +135,6 @@ class ModularAssistiveDrivingSystem:
|
||||
self.events.remove(EventName.speedTooLow)
|
||||
self.events.remove(EventName.cruiseDisabled)
|
||||
self.events.remove(EventName.manualRestart)
|
||||
self.events.remove(EventName.espActive)
|
||||
|
||||
selfdrive_enable_events = self.events.has(EventName.pcmEnable) or self.events.has(EventName.buttonEnable)
|
||||
set_speed_btns_enable = any(be.type in SET_SPEED_BUTTONS for be in CS.buttonEvents)
|
||||
@@ -199,9 +186,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if self.state_machine.state == State.paused:
|
||||
self.events_sp.add(EventNameSP.silentLkasEnable)
|
||||
|
||||
if self.lateral_mismatch_counter >= 200:
|
||||
self.events_sp.add(EventNameSP.controlsMismatchLateral)
|
||||
|
||||
self.events.remove(EventName.pcmDisable)
|
||||
self.events.remove(EventName.buttonCancel)
|
||||
self.events.remove(EventName.pedalPressed)
|
||||
@@ -211,8 +195,6 @@ class ModularAssistiveDrivingSystem:
|
||||
if not self.enabled_toggle:
|
||||
return
|
||||
|
||||
self.data_sample()
|
||||
|
||||
self.update_events(CS)
|
||||
|
||||
if not self.CP.passive and self.selfdrive.initialized:
|
||||
|
||||
@@ -13,7 +13,7 @@ if PC:
|
||||
model_dir = Dir("models").abspath
|
||||
cmd = f'python3 {Dir("#sunnypilot/modeld_v2").abspath}/install_models_pc.py {model_dir}'
|
||||
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
inputs.append(File(f"models/{model_name}.onnx"))
|
||||
inputs.append(File(f"models/{model_name}_tinygrad.pkl"))
|
||||
@@ -42,7 +42,7 @@ def tg_compile(flags, model_name):
|
||||
)
|
||||
|
||||
# Compile models
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_on_policy', 'driving_policy']:
|
||||
for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'driving_policy']:
|
||||
if File(f"models/{model_name}.onnx").exists():
|
||||
tg_compile(tg_flags, model_name)
|
||||
|
||||
|
||||
@@ -33,20 +33,29 @@ def compile_v2_warp(cam_w, cam_h, buffer_length):
|
||||
|
||||
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||
big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
|
||||
new_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||
new_big_frame_np = np.random.randint(0, 256, yuv_size, dtype=np.uint8)
|
||||
full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8)
|
||||
big_full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8)
|
||||
|
||||
for i in range(10):
|
||||
new_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
|
||||
img_inputs = [full_buffer,
|
||||
Tensor.from_blob(new_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||
new_big_frame_np = (32 * np.random.randn(yuv_size).astype(np.float32) + 128).clip(0, 255).astype(np.uint8)
|
||||
big_img_inputs = [big_full_buffer,
|
||||
Tensor.from_blob(new_big_frame_np.ctypes.data, (yuv_size,), dtype='uint8').realize(),
|
||||
Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')]
|
||||
inputs = img_inputs + big_img_inputs
|
||||
Device.default.synchronize()
|
||||
|
||||
inputs_np = [x.numpy() for x in inputs]
|
||||
inputs_np[0] = full_buffer_np
|
||||
inputs_np[3] = big_full_buffer_np
|
||||
|
||||
st = time.perf_counter()
|
||||
_ = update_img_jit(*inputs)
|
||||
out = update_img_jit(*inputs)
|
||||
full_buffer = out[0].contiguous().realize().clone()
|
||||
big_full_buffer = out[2].contiguous().realize().clone()
|
||||
mt = time.perf_counter()
|
||||
Device.default.synchronize()
|
||||
et = time.perf_counter()
|
||||
@@ -116,8 +125,8 @@ class Warp:
|
||||
self.full_buffers['img'], road_blob, self.transforms['img'],
|
||||
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
|
||||
)
|
||||
out_road = res[0].realize()
|
||||
out_wide = res[1].realize()
|
||||
self.full_buffers['img'], out_road = res[0].realize(), res[1].realize()
|
||||
self.full_buffers['big_img'], out_wide = res[2].realize(), res[3].realize()
|
||||
|
||||
return {road: out_road, wide: out_wide}
|
||||
|
||||
|
||||
@@ -8,14 +8,16 @@ from openpilot.sunnypilot import get_file_hash
|
||||
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
|
||||
MODEL_HASH_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "tests", "model_hash")
|
||||
VISION_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_vision.onnx")
|
||||
POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_policy.onnx")
|
||||
OFF_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_off_policy.onnx")
|
||||
ON_POLICY_ONNX_PATH = os.path.join(BASEDIR, "selfdrive", "modeld", "models", "driving_on_policy.onnx")
|
||||
|
||||
|
||||
def update_model_hash():
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH, "w") as f:
|
||||
f.write(combined_hash)
|
||||
|
||||
@@ -1 +1 @@
|
||||
32f57bdc91f910df1f48ddae7c59aaf6e751f9df6756da481a210577dbce8bcf
|
||||
793b5d480edb5a30eed3d0d3bdb43259522978670f6bc3dea7a4d661261d3c48
|
||||
|
||||
@@ -6,16 +6,17 @@ See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
|
||||
from openpilot.sunnypilot import get_file_hash
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, POLICY_ONNX_PATH
|
||||
from openpilot.sunnypilot.models.default_model import MODEL_HASH_PATH, VISION_ONNX_PATH, OFF_POLICY_ONNX_PATH, ON_POLICY_ONNX_PATH
|
||||
import hashlib
|
||||
|
||||
|
||||
class TestDefaultModel:
|
||||
def test_compare_onnx_hashes(self):
|
||||
vision_hash = get_file_hash(VISION_ONNX_PATH)
|
||||
policy_hash = get_file_hash(POLICY_ONNX_PATH)
|
||||
off_policy_hash = get_file_hash(OFF_POLICY_ONNX_PATH)
|
||||
on_policy_hash = get_file_hash(ON_POLICY_ONNX_PATH)
|
||||
|
||||
combined_hash = hashlib.sha256((vision_hash + policy_hash).encode()).hexdigest()
|
||||
combined_hash = hashlib.sha256((vision_hash + off_policy_hash + on_policy_hash).encode()).hexdigest()
|
||||
|
||||
with open(MODEL_HASH_PATH) as f:
|
||||
current_hash = f.read().strip()
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e459241d896824f5e8207d568847acab3dedd41caae7af59d4c17e043663b0c9
|
||||
size 4035
|
||||
@@ -1,3 +0,0 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:27a0fca872d4586f578d246890b83674cdb7ecb03f58b2b0379b4b64a5816053
|
||||
size 3908
|
||||
@@ -38,8 +38,6 @@ class ControlsExt(ModelStateBase):
|
||||
enforce_torque_control = self.params.get_bool("EnforceTorqueControl")
|
||||
torque_versions = self.params.get("TorqueControlTune")
|
||||
if not enforce_torque_control:
|
||||
if self.CP.lateralTuning.which() == 'torque':
|
||||
return LatControlTorqueV0(self.CP, self.CP_SP, CI, dt) # FIXME-SP: revert when upstream fixes tuning issues with v1
|
||||
return lac
|
||||
|
||||
if torque_versions == 0.0: # v0
|
||||
|
||||
@@ -14,8 +14,11 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
LAT_PLAN_MIN_IDX = 5
|
||||
LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan
|
||||
|
||||
KP = 1.0
|
||||
KI = 0.3
|
||||
# from selfdrive/controls/lib/latcontrol_torque.py
|
||||
KP = 0.8
|
||||
KI = 0.15
|
||||
INTERP_SPEEDS = [1, 1.5, 2.0, 3.0, 5, 7.5, 10, 15, 30]
|
||||
KP_INTERP = [250, 120, 65, 30, 11.5, 5.5, 3.5, 2.0, KP]
|
||||
|
||||
|
||||
def get_predicted_lateral_jerk(lat_accels, t_diffs):
|
||||
@@ -58,10 +61,9 @@ class LatControlTorqueExtBase:
|
||||
self.lookahead_lateral_jerk: float = 0.0
|
||||
|
||||
self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space()
|
||||
self.torque_params = lac_torque.torque_params
|
||||
|
||||
self._ff = 0.0
|
||||
self._pid = PIDController(KP, KI)
|
||||
self._pid = PIDController([INTERP_SPEEDS, KP_INTERP], KI)
|
||||
self._pid_log = None
|
||||
self._setpoint = 0.0
|
||||
self._measurement = 0.0
|
||||
|
||||
@@ -75,14 +75,14 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
def update_feedforward_torque_space(self, CS):
|
||||
torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo),
|
||||
self.torque_params, gravity_adjusted=False)
|
||||
self.lac_torque.torque_params, gravity_adjusted=False)
|
||||
self._pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation,
|
||||
CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True)
|
||||
CS.vEgo, CS.aEgo), self.lac_torque.torque_params, gravity_adjusted=True)
|
||||
self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone,
|
||||
FRICTION_THRESHOLD, self.torque_params)
|
||||
FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
def update_output_torque(self, CS):
|
||||
freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5
|
||||
@@ -159,6 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase):
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.model.friction_override:
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params)
|
||||
self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.lac_torque.torque_params)
|
||||
|
||||
self.update_output_torque(CS)
|
||||
|
||||
@@ -4,6 +4,7 @@ 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 cereal import car
|
||||
@@ -17,6 +18,7 @@ RELAXED_MIN_BUCKET_POINTS = np.array([1, 200, 300, 500, 500, 300, 200, 1])
|
||||
ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda']
|
||||
|
||||
|
||||
|
||||
class TorqueEstimatorExt:
|
||||
def __init__(self, CP: car.CarParams):
|
||||
self.CP = CP
|
||||
@@ -26,7 +28,6 @@ class TorqueEstimatorExt:
|
||||
self.enforce_torque_control_toggle = self._params.get_bool("EnforceTorqueControl") # only during init
|
||||
self.use_params = self.CP.brand in ALLOWED_CARS and self.CP.lateralTuning.which() == 'torque'
|
||||
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
||||
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
|
||||
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
||||
self.min_bucket_points = RELAXED_MIN_BUCKET_POINTS
|
||||
self.factor_sanity = 0.0
|
||||
@@ -50,14 +51,13 @@ class TorqueEstimatorExt:
|
||||
def _update_params(self):
|
||||
if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0:
|
||||
self.use_live_torque_params = self._params.get_bool("LiveTorqueParamsToggle")
|
||||
self.custom_torque_params = self._params.get_bool("CustomTorqueParams")
|
||||
self.torque_override_enabled = self._params.get_bool("TorqueParamsOverrideEnabled")
|
||||
|
||||
def update_use_params(self):
|
||||
self._update_params()
|
||||
|
||||
if self.enforce_torque_control_toggle:
|
||||
if self.custom_torque_params and self.torque_override_enabled:
|
||||
if self.torque_override_enabled:
|
||||
self.use_params = False
|
||||
else:
|
||||
self.use_params = self.use_live_torque_params
|
||||
|
||||
@@ -30,6 +30,10 @@ class SunnylinkApi(BaseApi):
|
||||
|
||||
return super().api_get(endpoint, method, timeout, access_token, session, json, **kwargs)
|
||||
|
||||
def resume_queued(self, timeout=10, **kwargs):
|
||||
sunnylinkId, commaId = self._resolve_dongle_ids()
|
||||
return self.api_get(f"ws/{sunnylinkId}/resume_queued", "POST", timeout, access_token=self.get_token(), **kwargs)
|
||||
|
||||
def get_token(self, payload_extra=None, expiry_hours=1):
|
||||
# Add your additional data here
|
||||
additional_data = {}
|
||||
|
||||
@@ -18,7 +18,7 @@ import time
|
||||
|
||||
from jsonrpc import dispatcher
|
||||
from functools import partial
|
||||
from openpilot.common.params import Params, ParamKeyType
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.realtime import set_core_affinity
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware.hw import Paths
|
||||
@@ -31,10 +31,8 @@ import cereal.messaging as messaging
|
||||
from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param
|
||||
from openpilot.sunnypilot.sunnylink.api import SunnylinkApi
|
||||
from openpilot.sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string
|
||||
from openpilot.sunnypilot.sunnylink.capabilities import generate_capabilities, CAPABILITY_LABELS
|
||||
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://ws.stg.api.sunnypilot.ai')
|
||||
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
|
||||
LOCAL_PORT_WHITELIST = {8022}
|
||||
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
|
||||
@@ -46,15 +44,12 @@ params = Params()
|
||||
|
||||
# Parameters that should never be remotely modified
|
||||
BLOCKED_PARAMS = {
|
||||
"AdbEnabled",
|
||||
"CompletedSunnylinkConsentVersion",
|
||||
"CompletedTrainingVersion",
|
||||
"GithubUsername", # Could grant SSH access
|
||||
"GithubSshKeys", # Direct SSH key injection
|
||||
"HasAcceptedTerms",
|
||||
"HasAcceptedTermsSP",
|
||||
"OnroadCycleRequested", # Prevent remote cycle trigger
|
||||
"ParamsVersion", # Device-managed version counter
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +64,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None:
|
||||
threading.Thread(target=ws_recv, args=(ws, end_event), name='ws_recv'),
|
||||
threading.Thread(target=ws_send, args=(ws, end_event), name='ws_send'),
|
||||
threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'),
|
||||
threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'),
|
||||
threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'),
|
||||
threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'),
|
||||
threading.Thread(target=stat_handler, args=(end_event, Paths.stats_sp_root(), True), name='stat_handler'),
|
||||
@@ -151,6 +147,37 @@ def ws_ping(ws: WebSocket, end_event: threading.Event) -> None:
|
||||
cloudlog.debug("sunnylinkd.ws_ping.end_event is set, exiting ws_ping thread")
|
||||
|
||||
|
||||
def ws_queue(end_event: threading.Event) -> None:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
sunnylink_api = SunnylinkApi(sunnylink_dongle_id)
|
||||
resume_requested = False
|
||||
tries = 0
|
||||
|
||||
while not end_event.is_set() and not resume_requested:
|
||||
try:
|
||||
if not resume_requested:
|
||||
cloudlog.debug("sunnylinkd.ws_queue.resume_queued")
|
||||
sunnylink_api.resume_queued(timeout=29)
|
||||
resume_requested = True
|
||||
tries = 0
|
||||
except Exception as e:
|
||||
if isinstance(e, (ConnectionError, TimeoutError)):
|
||||
cloudlog.warning(f"sunnylinkd.ws_queue.resume_queued.{type(e).__name__}")
|
||||
else:
|
||||
cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception")
|
||||
|
||||
resume_requested = False
|
||||
tries += 1
|
||||
time.sleep(backoff(tries))
|
||||
|
||||
if end_event.is_set():
|
||||
cloudlog.debug("end_event is set, exiting ws_queue thread")
|
||||
elif resume_requested:
|
||||
cloudlog.debug(f"Resume requested to server after {tries} tries")
|
||||
else:
|
||||
cloudlog.error(f"Reached end of ws_queue while end_event is not set and resume_requested is {resume_requested}")
|
||||
|
||||
|
||||
def sunny_log_handler(end_event: threading.Event, comma_prime_cellular_end_event: threading.Event) -> None:
|
||||
while not end_event.wait(0.1):
|
||||
if not comma_prime_cellular_end_event.is_set():
|
||||
@@ -204,18 +231,34 @@ def getParamsAllKeysV1() -> dict[str, str]:
|
||||
|
||||
@dispatcher.add_method
|
||||
def getParamsMetadata() -> str:
|
||||
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
|
||||
|
||||
Reads settings_ui.json, injects live capabilities from CarParams, compresses,
|
||||
and returns. Single RPC for the frontend to get the complete settings UI and
|
||||
runtime capabilities.
|
||||
"""
|
||||
"""Compressed equivalent of getParamsAllKeysV1 — same struct, gzipped + base64."""
|
||||
try:
|
||||
schema = generate_schema()
|
||||
schema["capabilities"] = generate_capabilities()
|
||||
schema["capability_labels"] = CAPABILITY_LABELS
|
||||
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
|
||||
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
|
||||
with open(METADATA_PATH) as f:
|
||||
metadata = json.load(f)
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
|
||||
metadata = {}
|
||||
|
||||
try:
|
||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||
|
||||
params_list: list[dict] = []
|
||||
for key in available_keys:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
|
||||
param_entry: dict = {
|
||||
"key": key,
|
||||
"type": int(params.get_type(key).value),
|
||||
"default_value": base64.b64encode(value).decode('utf-8') if value else None,
|
||||
}
|
||||
|
||||
if key in metadata:
|
||||
param_entry["_extra"] = metadata[key]
|
||||
|
||||
params_list.append(param_entry)
|
||||
|
||||
raw = json.dumps(params_list, separators=(',', ':')).encode('utf-8')
|
||||
return base64.b64encode(gzip.compress(raw)).decode('utf-8')
|
||||
except Exception:
|
||||
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
|
||||
raise
|
||||
@@ -227,25 +270,12 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s
|
||||
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
|
||||
|
||||
try:
|
||||
zero_values: dict[int, bytes] = {
|
||||
ParamKeyType.STRING.value: b"",
|
||||
ParamKeyType.BOOL.value: b"0",
|
||||
ParamKeyType.INT.value: b"0",
|
||||
ParamKeyType.FLOAT.value: b"0.0",
|
||||
ParamKeyType.TIME.value: b"",
|
||||
ParamKeyType.JSON.value: b"{}",
|
||||
ParamKeyType.BYTES.value: b"",
|
||||
}
|
||||
|
||||
param_keys_validated = [key for key in params_keys if key in available_keys]
|
||||
params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []}
|
||||
for key in param_keys_validated:
|
||||
value = get_param_as_byte(key)
|
||||
if value is None:
|
||||
value = get_param_as_byte(key, get_default=True)
|
||||
if value is None:
|
||||
param_type = params.get_type(key)
|
||||
value = zero_values.get(param_type.value, b"")
|
||||
continue
|
||||
|
||||
params_dict["params"].append({
|
||||
"key": key,
|
||||
@@ -276,13 +306,6 @@ def saveParams(params_to_update: dict[str, str], compression: bool = False) -> N
|
||||
except Exception as e:
|
||||
cloudlog.error(f"sunnylinkd.saveParams.exception {e}")
|
||||
|
||||
# Increment version counter for frontend change detection
|
||||
try:
|
||||
current = int(params.get("ParamsVersion") or "0")
|
||||
params.put("ParamsVersion", str(current + 1))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]:
|
||||
sunnylink_dongle_id = params.get("SunnylinkDongleId")
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
"""
|
||||
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
|
||||
|
||||
This file is part of sunnypilot and is licensed under the MIT License.
|
||||
See the LICENSE.md file in the root directory for more details.
|
||||
"""
|
||||
import json
|
||||
|
||||
from cereal import car, custom, messaging
|
||||
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.sunnypilot.car.tesla.values import TeslaFlagsSP
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.swaglog import cloudlog
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
|
||||
# Wire-protocol version for the capabilities payload. Bump on breaking changes
|
||||
# only; additive fields are backward-compatible and do not require a bump.
|
||||
PROTOCOL_VERSION = 1
|
||||
|
||||
# All capability fields that rules may reference.
|
||||
# Non-boolean fields must have defaults in CAPABILITY_DEFAULTS.
|
||||
CAPABILITY_FIELDS = (
|
||||
"protocol_version",
|
||||
"has_longitudinal_control",
|
||||
"has_icbm",
|
||||
"icbm_available",
|
||||
"torque_allowed",
|
||||
"brand",
|
||||
"pcm_cruise",
|
||||
"alpha_long_available",
|
||||
"steer_control_type",
|
||||
"enable_bsm",
|
||||
"is_release",
|
||||
"is_sp_release",
|
||||
"is_development",
|
||||
"tesla_has_vehicle_bus",
|
||||
"has_stop_and_go",
|
||||
"stock_longitudinal",
|
||||
"device_type",
|
||||
"subaru_has_sng",
|
||||
"hyundai_alpha_long_available",
|
||||
)
|
||||
|
||||
CAPABILITY_LABELS: dict[str, str] = {
|
||||
"protocol_version": "Capabilities protocol version",
|
||||
"has_longitudinal_control": "sunnypilot longitudinal control",
|
||||
"has_icbm": "ICBM enabled",
|
||||
"icbm_available": "ICBM available",
|
||||
"torque_allowed": "torque steering (not available for angle steering vehicles)",
|
||||
"brand": "Vehicle brand",
|
||||
"pcm_cruise": "PCM cruise",
|
||||
"alpha_long_available": "Alpha Longitudinal available",
|
||||
"steer_control_type": "Steer control type",
|
||||
"enable_bsm": "BSM available",
|
||||
"is_release": "Release branch",
|
||||
"is_sp_release": "SP release branch",
|
||||
"is_development": "Development branch",
|
||||
"tesla_has_vehicle_bus": "Tesla vehicle bus",
|
||||
"has_stop_and_go": "Stop and Go",
|
||||
"stock_longitudinal": "stock longitudinal",
|
||||
"device_type": "Device type",
|
||||
"subaru_has_sng": "Subaru Stop-and-Go available",
|
||||
"hyundai_alpha_long_available": "Hyundai Alpha Longitudinal available",
|
||||
}
|
||||
|
||||
# Explicit defaults for non-boolean capability fields
|
||||
CAPABILITY_DEFAULTS: dict[str, bool | str | int] = {
|
||||
"brand": "",
|
||||
"steer_control_type": "",
|
||||
"device_type": "",
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
}
|
||||
|
||||
|
||||
def _bundle_field(bundle: dict | None, key: str) -> str:
|
||||
return bundle.get(key, "") if isinstance(bundle, dict) else ""
|
||||
|
||||
|
||||
def _resolve_brand_capabilities(caps: dict, bundle_platform: str, CP) -> None:
|
||||
"""Set brand-specific capabilities from bundle platform or CarParams fallback.
|
||||
|
||||
Bundle (manual car selection) is a pre-fingerprint approximation.
|
||||
CarParams (auto-fingerprint) is the authoritative post-fingerprint source.
|
||||
Mirrors the per-brand update_settings() logic in device UI layouts.
|
||||
"""
|
||||
brand = caps["brand"]
|
||||
|
||||
if brand == "hyundai":
|
||||
if bundle_platform:
|
||||
try:
|
||||
unsupported = set().union(*UNSUPPORTED_LONGITUDINAL_CAR.values())
|
||||
caps["hyundai_alpha_long_available"] = HYUNDAI_CAR[bundle_platform] not in unsupported
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown hyundai platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["hyundai_alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
|
||||
|
||||
elif brand == "subaru":
|
||||
if bundle_platform:
|
||||
try:
|
||||
flags = SUBARU_CAR[bundle_platform].config.flags
|
||||
caps["subaru_has_sng"] = not bool(flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
except KeyError:
|
||||
cloudlog.exception(f"capabilities: unknown subaru platform {bundle_platform!r}")
|
||||
elif CP is not None:
|
||||
caps["subaru_has_sng"] = not bool(CP.flags & (SubaruFlags.GLOBAL_GEN2 | SubaruFlags.HYBRID))
|
||||
caps["has_stop_and_go"] = caps["subaru_has_sng"]
|
||||
|
||||
|
||||
def generate_capabilities(params: Params | None = None) -> dict:
|
||||
"""Generate a SettingsCapabilities dict from CarParams + boolean params.
|
||||
|
||||
When CarPlatformBundle is present, brand and platform come from the bundle
|
||||
(mirrors Raylib). CarParams* deserialization is the fallback before the bundle
|
||||
is written (early after first pairing).
|
||||
"""
|
||||
params = params or Params()
|
||||
|
||||
caps: dict = {field: CAPABILITY_DEFAULTS.get(field, False) for field in CAPABILITY_FIELDS}
|
||||
|
||||
# Wire-protocol version is always set explicitly.
|
||||
caps["protocol_version"] = PROTOCOL_VERSION
|
||||
|
||||
# Hardware + boolean params (no CarParams dependency)
|
||||
caps["device_type"] = HARDWARE.get_device_type()
|
||||
caps["is_release"] = False # params.get_bool("IsReleaseBranch")
|
||||
caps["is_sp_release"] = params.get_bool("IsReleaseSpBranch")
|
||||
caps["is_development"] = params.get_bool("IsDevelopmentBranch")
|
||||
caps["stock_longitudinal"] = params.get_bool("ToyotaEnforceStockLongitudinal")
|
||||
|
||||
bundle = params.get("CarPlatformBundle")
|
||||
bundle_brand = _bundle_field(bundle, "brand")
|
||||
bundle_platform = _bundle_field(bundle, "platform")
|
||||
|
||||
# Bundle-first brand resolution; CP is fallback only.
|
||||
if bundle_brand:
|
||||
caps["brand"] = bundle_brand
|
||||
|
||||
# CarParams-derived capabilities
|
||||
CP = None
|
||||
CP_bytes = params.get("CarParamsPersistent")
|
||||
if CP_bytes is not None:
|
||||
try:
|
||||
CP = messaging.log_from_bytes(CP_bytes, car.CarParams)
|
||||
caps["alpha_long_available"] = bool(CP.alphaLongitudinalAvailable)
|
||||
if CP.alphaLongitudinalAvailable:
|
||||
caps["has_longitudinal_control"] = params.get_bool("AlphaLongitudinalEnabled")
|
||||
else:
|
||||
caps["has_longitudinal_control"] = bool(CP.openpilotLongitudinalControl)
|
||||
# CP.steerControlType is the physical control mode (angle / torque).
|
||||
# CP.lateralTuning.which() returns the tuning class (pid / torque / indi)
|
||||
# which is a separate concept and is not interchangeable.
|
||||
caps["steer_control_type"] = str(CP.steerControlType)
|
||||
caps["torque_allowed"] = CP.steerControlType != car.CarParams.SteerControlType.angle
|
||||
if not caps["brand"] and CP.brand:
|
||||
caps["brand"] = str(CP.brand)
|
||||
caps["pcm_cruise"] = bool(CP.pcmCruise)
|
||||
caps["enable_bsm"] = bool(CP.enableBsm)
|
||||
# Generic SnG fallback. Brand-specific opaque flags below override.
|
||||
caps["has_stop_and_go"] = bool(CP.openpilotLongitudinalControl)
|
||||
except Exception:
|
||||
CP = None
|
||||
cloudlog.exception("capabilities: failed to deserialize CarParamsPersistent")
|
||||
|
||||
# CarParamsSP-derived capabilities
|
||||
CP_SP_bytes = params.get("CarParamsSPPersistent")
|
||||
if CP_SP_bytes is not None:
|
||||
try:
|
||||
CP_SP = messaging.log_from_bytes(CP_SP_bytes, custom.CarParamsSP)
|
||||
caps["icbm_available"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable)
|
||||
caps["has_icbm"] = bool(CP_SP.intelligentCruiseButtonManagementAvailable) and params.get_bool("IntelligentCruiseButtonManagement")
|
||||
caps["tesla_has_vehicle_bus"] = bool(CP_SP.flags & TeslaFlagsSP.HAS_VEHICLE_BUS)
|
||||
except Exception:
|
||||
cloudlog.exception("capabilities: failed to deserialize CarParamsSPPersistent")
|
||||
|
||||
_resolve_brand_capabilities(caps, bundle_platform, CP)
|
||||
|
||||
return caps
|
||||
|
||||
|
||||
def generate_capabilities_json(params: Params | None = None) -> str:
|
||||
"""Generate SettingsCapabilities as a JSON string."""
|
||||
return json.dumps(generate_capabilities(params), separators=(",", ":"))
|
||||
@@ -1,588 +0,0 @@
|
||||
# sunnylink Settings UI Guide
|
||||
|
||||
> One YAML file per page. Edit, run the compiler, commit. The sunnylink frontend updates automatically.
|
||||
|
||||
For detailed architecture, capability fields, parity analysis, and dialog mappings, see [REFERENCE.md](REFERENCE.md).
|
||||
|
||||
## What you edit (and what's generated)
|
||||
|
||||
| File | What | When to edit |
|
||||
|------|------|-------------|
|
||||
| `settings_ui_src/pages/<page>.yaml` | One YAML per page (panel). Contains panel metadata + sections + items + sub_panels inline. | Adding/changing/removing a setting. |
|
||||
| `settings_ui_src/pages/vehicle.yaml` | Per-brand settings page (`kind: vehicle`). Each brand is a section. | Adding/changing a vehicle-specific setting. |
|
||||
| `settings_ui_src/_macros.yaml` | Named rule fragments referenced via `{$ref: "#/macros/<name>"}`. | Adding a reusable rule (e.g. a new platform gate). |
|
||||
| **`settings_ui.json`** | **Generated from src tree by `compile_settings_ui.py`. Do not edit by hand.** | Never. Compiler emits it; frontend reads it. |
|
||||
|
||||
Pages today: `steering, cruise, display, visuals, toggles, device, software, developer, models, vehicle` (10).
|
||||
|
||||
Run `python sunnypilot/sunnylink/tools/compile_settings_ui.py` after edits. Add `--check` in CI to fail on out-of-sync `settings_ui.json`.
|
||||
|
||||
Display metadata (titles, descriptions, options, min/max/step/unit) is inline on each item. There is no separate metadata file.
|
||||
|
||||
## Page file shape
|
||||
|
||||
A page YAML contains the whole panel: metadata at the top, then `sections`. Each section has its own `items` and (optionally) `sub_panels`. Sub-panels are nested inside the section they belong to. Items appear in the order written in the file.
|
||||
|
||||
```yaml
|
||||
# yaml-language-server: $schema=../_schemas/page.schema.json
|
||||
id: steering
|
||||
label: Steering
|
||||
icon: steering_wheel
|
||||
order: 1
|
||||
remote_configurable: true
|
||||
description: Lateral control, lane changes, and steering behavior
|
||||
|
||||
sections:
|
||||
- id: mads
|
||||
title: Modular Assistive Driving System (MADS)
|
||||
items:
|
||||
- key: Mads
|
||||
widget: toggle
|
||||
title: Enable Modular Assistive Driving System (MADS)
|
||||
description: |
|
||||
Enable the beloved MADS feature. Disable toggle to revert back
|
||||
to stock sunnypilot engagement/disengagement.
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
|
||||
sub_panels:
|
||||
- id: mads_settings
|
||||
label: MADS Settings
|
||||
trigger_key: Mads
|
||||
trigger_condition: {type: param, key: Mads, equals: true}
|
||||
items:
|
||||
- key: MadsMainCruiseAllowed
|
||||
widget: toggle
|
||||
title: Toggle with Main Cruise
|
||||
description: |
|
||||
Note: For vehicles without LFA/LKAS button, disabling this will
|
||||
prevent lateral control engagement.
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {$ref: "#/macros/mads_full_platforms"}
|
||||
```
|
||||
|
||||
The vehicle page has the same shape but declares `kind: vehicle`; each section's `id` becomes a brand key under `vehicle_settings` in the compiled JSON.
|
||||
|
||||
## Macros (named rule fragments)
|
||||
|
||||
`_macros.yaml` declares reusable rule lists. Reference them from any rules array via `{$ref: "#/macros/<name>"}`.
|
||||
|
||||
```yaml
|
||||
macros:
|
||||
offroad: [{type: offroad_only}]
|
||||
longitudinal: [{type: capability, field: has_longitudinal_control, equals: true}]
|
||||
mads_full_platforms:
|
||||
- type: not
|
||||
condition:
|
||||
type: any
|
||||
conditions:
|
||||
- {type: capability, field: brand, equals: rivian}
|
||||
- type: all
|
||||
conditions:
|
||||
- {type: capability, field: brand, equals: tesla}
|
||||
- type: not
|
||||
condition: {type: capability, field: tesla_has_vehicle_bus, equals: true}
|
||||
```
|
||||
|
||||
In an item:
|
||||
|
||||
```yaml
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {$ref: "#/macros/mads_full_platforms"}
|
||||
```
|
||||
|
||||
The compiler splices a list-context `$ref` into its parent list. Macros may reference other macros up to depth 3; cycles are an error.
|
||||
|
||||
## Compiler workflow
|
||||
|
||||
```
|
||||
1. common/params_keys.h — add/remove the C++ param key
|
||||
2. params_metadata.json — automated via update_params_metadata.py
|
||||
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
|
||||
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
|
||||
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
|
||||
7. commit
|
||||
```
|
||||
|
||||
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
|
||||
|
||||
## Compiled output reference (schema contract)
|
||||
|
||||
The tables below describe the **compiled** `settings_ui.json` schema — what the frontend consumes at runtime. JSON snippets show the wire shape; in the src tree you author YAML that compiles to the same shape. Use these as a contract reference for valid fields, their meanings, and rule types.
|
||||
|
||||
## Quick reference: widget types
|
||||
|
||||
| Widget | Use for | Fields needed |
|
||||
|--------|---------|---------------|
|
||||
| `toggle` | On/off boolean | `title` |
|
||||
| `multiple_button` | 2-4 discrete options | `title` + `options` array |
|
||||
| `option` | Numeric range or dropdown | `title` + `min/max/step` or `options` |
|
||||
| `info` | Read-only display | `title` |
|
||||
|
||||
## Quick reference: item fields
|
||||
|
||||
| Field | Required | Description |
|
||||
|-------|----------|-------------|
|
||||
| `key` | Yes | Param key name (must exist in `params_keys.h`) |
|
||||
| `widget` | Yes | `toggle`, `option`, `multiple_button`, `button`, `info` |
|
||||
| `title` | Yes | Display name shown to the user |
|
||||
| `description` | No | Inline explanatory text below the title. May be empty when only `details` is used. |
|
||||
| `details` | No | Extended help text shown in a modal when the user taps an "i" button on the row. Independent of `description`: either, both, or neither may be present. |
|
||||
| `options` | For selectors | Array of `{"value": 0, "label": "Off"}` objects (see per-option enablement below) |
|
||||
| `min`, `max`, `step` | For sliders | Numeric range constraints |
|
||||
| `unit` | No | Unit label. Static: `"seconds"`. Dynamic: `{"metric": "km/h", "imperial": "mph"}` (resolved by IsMetric) |
|
||||
| `visibility` | No | Rules for show/hide. Settings are never hidden, always dimmed with UNAVAILABLE badge when rules fail |
|
||||
| `enablement` | No | Rules for enabled/disabled (all must pass). Dimmed with badge when rules fail |
|
||||
| `blocked` | No | `true` for device-only settings that cannot be modified remotely. Frontend shows as read-only |
|
||||
| `title_param_suffix` | No | Dynamic title suffix. Example: `{"param": "IsMetric", "values": {"0": "mph", "1": "km/h"}}` |
|
||||
| `sub_items` | No | Nested child items |
|
||||
| `needs_onroad_cycle` | No | `true` if changing this param triggers a system restart. Frontend shows a "Restart" badge. See [REFERENCE.md - Remote Onroad Cycle](REFERENCE.md#remote-onroad-cycle) |
|
||||
|
||||
## Quick reference: rule types
|
||||
|
||||
| Rule | Example | Use for |
|
||||
|------|---------|---------|
|
||||
| `offroad_only` | `{"type": "offroad_only"}` | Grey out while driving |
|
||||
| `not_engaged` | `{"type": "not_engaged"}` | Grey out only while engaged (started + selfdrive/MADS active) |
|
||||
| `capability` | `{"type": "capability", "field": "has_longitudinal_control", "equals": true}` | Car-dependent visibility |
|
||||
| `param` | `{"type": "param", "key": "Mads", "equals": true}` | Show/enable based on another setting |
|
||||
| `param_compare` | `{"type": "param_compare", "key": "SpeedLimitMode", "op": ">", "value": 0}` | Numeric comparison |
|
||||
| `not` | `{"type": "not", "condition": {...}}` | Negate a rule |
|
||||
| `any` | `{"type": "any", "conditions": [...]}` | OR logic |
|
||||
| `all` | `{"type": "all", "conditions": [...]}` | AND logic (for nesting inside `any`/`not`) |
|
||||
| `$ref` | `{"$ref": "#/macros/offroad"}` | Reference a named rule fragment in `_macros.yaml` |
|
||||
|
||||
**Visibility design**: Settings are always visible. When visibility rules fail, the setting is dimmed with an UNAVAILABLE badge, so users know it exists but is not applicable.
|
||||
|
||||
**Enablement rules**: Grayed out (disabled) when rules fail. Frontend shows a contextual badge explaining why.
|
||||
|
||||
**Capability fields** (referenced in rules): `has_longitudinal_control`, `has_icbm`, `icbm_available`, `torque_allowed`, `brand`, `pcm_cruise`, `alpha_long_available`, `steer_control_type`, `enable_bsm`, `is_release`, `is_sp_release`, `is_development`, `tesla_has_vehicle_bus`, `has_stop_and_go`, `stock_longitudinal`
|
||||
|
||||
---
|
||||
|
||||
## How to
|
||||
|
||||
### Pick a writability rule (offroad / not_engaged / param-based)
|
||||
|
||||
| Use this | When | Why |
|
||||
|---|---|---|
|
||||
| `offroad_only` | Param can only be safely changed when the car is parked. Most user-facing toggles. | Strictest. Frontend shows "device is driving" badge and disables the row. |
|
||||
| `not_engaged` | Param can be changed while the car is started but only when sunnypilot/MADS is **not** actively driving. | Less strict than offroad. Matches Raylib `engaged = started AND (selfdriveState.enabled OR mads.enabled)`. Use for items the device must apply mid-drive (e.g. test maneuvers, longitudinal stock-vs-OP toggle). |
|
||||
| `param`-based | Behavior depends on another setting's value (parent toggle, mode selector, etc.). | Composes with `not`/`any`/`all` for arbitrary logic. |
|
||||
| `capability`-based | Behavior depends on the connected car or device (brand, longitudinal, hardware). | Resolved on the device from `CarParams` / hardware. See [`capabilities.py`](../capabilities.py) for the full field list. |
|
||||
| (no rule) | Param is always writable, no gating. | Rare. Prefer at least `offroad_only` unless the param is genuinely safe to flip mid-drive. |
|
||||
|
||||
Default for new toggles: `enablement: [{$ref: "#/macros/offroad"}]`. Drop down to `not_engaged` only if you've confirmed mid-drive write is safe in the controls/UI code path.
|
||||
|
||||
### Use `details` for safety notes / extended help
|
||||
|
||||
Inline `description` shows under the title. For longer caveats, safety notes, or "learn more" content, use `details` — the frontend renders an info button that opens a modal. Either field may be present alone or both together.
|
||||
|
||||
```yaml
|
||||
- key: AutoLaneChangeTimer
|
||||
widget: option
|
||||
title: Auto Lane Change by Blinker
|
||||
description: |-
|
||||
Set a timer to delay the auto lane change operation when the blinker is used.
|
||||
No nudge on the steering wheel is required to auto lane change if a timer is set.
|
||||
Default is Nudge.
|
||||
details: |-
|
||||
Please use caution when using this feature. Only use the blinker when traffic
|
||||
and road conditions permit.
|
||||
options: [...]
|
||||
```
|
||||
|
||||
For an item that is intentionally minimal inline (no inline body, only the modal):
|
||||
|
||||
```yaml
|
||||
- key: SomeAdvancedToggle
|
||||
widget: toggle
|
||||
title: Some Advanced Feature
|
||||
details: |-
|
||||
Long-form rationale, caveats, links, etc. — kept entirely behind the info button.
|
||||
```
|
||||
|
||||
### Add a toggle
|
||||
|
||||
1. Register in `common/params_keys.h`:
|
||||
```cpp
|
||||
{"MyToggle", {PERSISTENT | BACKUP, BOOL}},
|
||||
```
|
||||
|
||||
2. Open `settings_ui_src/pages/<page>.yaml`. Add the item to the right section:
|
||||
```yaml
|
||||
- key: MyToggle
|
||||
widget: toggle
|
||||
title: My Feature
|
||||
description: What this feature does.
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
```
|
||||
|
||||
If changing the param requires an onroad cycle to take effect, add `needs_onroad_cycle: true`.
|
||||
|
||||
3. Compile + validate + test:
|
||||
```
|
||||
python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
python sunnypilot/sunnylink/tools/validate_settings_ui.py
|
||||
uv run python -m pytest sunnypilot/sunnylink/tests/
|
||||
```
|
||||
|
||||
### Add a multi-button option
|
||||
|
||||
```yaml
|
||||
- key: MySelector
|
||||
widget: multiple_button
|
||||
title: Mode
|
||||
options:
|
||||
- {value: 0, label: Off}
|
||||
- {value: 1, label: On}
|
||||
- {value: 2, label: Auto}
|
||||
```
|
||||
|
||||
### Add a slider or range
|
||||
|
||||
```yaml
|
||||
- key: MyRange
|
||||
widget: option
|
||||
title: Follow Distance
|
||||
description: Time gap to lead vehicle.
|
||||
min: 0.5
|
||||
max: 3.0
|
||||
step: 0.1
|
||||
unit: seconds
|
||||
```
|
||||
|
||||
### Add a slider with metric/imperial units
|
||||
|
||||
```yaml
|
||||
- key: MinSpeed
|
||||
widget: option
|
||||
title: Minimum Speed
|
||||
min: 0
|
||||
max: 100
|
||||
step: 5
|
||||
unit: {metric: km/h, imperial: mph}
|
||||
```
|
||||
|
||||
Frontend resolves the unit string based on the device's `IsMetric` param. Static units (e.g. `seconds`, `m/s²`) stay plain strings.
|
||||
|
||||
### Add a dynamic title suffix
|
||||
|
||||
```yaml
|
||||
- key: FollowDistance
|
||||
widget: option
|
||||
title: Follow Distance
|
||||
title_param_suffix:
|
||||
param: IsMetric
|
||||
values: {'0': mph, '1': km/h}
|
||||
min: 0.5
|
||||
max: 3.0
|
||||
step: 0.1
|
||||
```
|
||||
|
||||
Renders as "Follow Distance: mph" / "Follow Distance: km/h".
|
||||
|
||||
### Add a device-only read-only setting
|
||||
|
||||
```yaml
|
||||
- key: OnroadCyclePendingRemote
|
||||
widget: info
|
||||
title: Pending Remote Cycle
|
||||
blocked: true
|
||||
```
|
||||
|
||||
Frontend treats `blocked: true` items as read-only.
|
||||
|
||||
### Add a dropdown option
|
||||
|
||||
```yaml
|
||||
- key: MyDropdown
|
||||
widget: option
|
||||
title: Recording Quality
|
||||
options:
|
||||
- {value: 0, label: Low (720p)}
|
||||
- {value: 1, label: Medium (1080p)}
|
||||
- {value: 2, label: High (4K)}
|
||||
```
|
||||
|
||||
### Per-option enablement rules
|
||||
|
||||
```yaml
|
||||
- key: MadsSteeringMode
|
||||
widget: multiple_button
|
||||
title: Steering Mode on Brake Pedal
|
||||
options:
|
||||
- value: 0
|
||||
label: Remain Active
|
||||
enablement:
|
||||
- {$ref: "#/macros/mads_full_platforms"}
|
||||
- value: 1
|
||||
label: Pause
|
||||
enablement:
|
||||
- {$ref: "#/macros/mads_full_platforms"}
|
||||
- value: 2
|
||||
label: Disengage
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
```
|
||||
|
||||
When an option's enablement fails, that option is grayed out but still visible.
|
||||
|
||||
### Show only when another setting is on
|
||||
|
||||
```yaml
|
||||
- key: ChildSetting
|
||||
widget: toggle
|
||||
title: Child Feature
|
||||
visibility:
|
||||
- {type: param, key: ParentToggle, equals: true}
|
||||
```
|
||||
|
||||
(With the "dim instead of hide" design, this setting is dimmed, not hidden, when the rule fails.)
|
||||
|
||||
### Show only for specific brands
|
||||
|
||||
```yaml
|
||||
- key: LongFeature
|
||||
widget: toggle
|
||||
title: Longitudinal Feature
|
||||
visibility:
|
||||
- {$ref: "#/macros/longitudinal"}
|
||||
```
|
||||
|
||||
### Combine multiple conditions
|
||||
|
||||
The `enablement` array is implicit-AND: every entry must pass. Use `any` for OR, `all` for nested AND, `not` for negation. Wrap repeated combinations in a macro so future you doesn't re-derive the logic.
|
||||
|
||||
**AND across two params** (writable only when both Mads is on AND ICBM is enabled):
|
||||
```yaml
|
||||
enablement:
|
||||
- {type: param, key: Mads, equals: true}
|
||||
- {type: param, key: IntelligentCruiseButtonManagement, equals: true}
|
||||
```
|
||||
|
||||
**OR across two params** (writable when either is on):
|
||||
```yaml
|
||||
enablement:
|
||||
- type: any
|
||||
conditions:
|
||||
- {type: param, key: ExperimentalMode, equals: true}
|
||||
- {type: param, key: DynamicExperimentalControl, equals: true}
|
||||
```
|
||||
|
||||
**Mixed: capability AND param** (only on longitudinal cars when ShowAdvancedControls is on):
|
||||
```yaml
|
||||
enablement:
|
||||
- {$ref: "#/macros/longitudinal"}
|
||||
- {$ref: "#/macros/advanced_only"}
|
||||
```
|
||||
|
||||
**Three-way: offroad AND torque-allowed AND not-NNLC** (real example: `EnforceTorqueControl`):
|
||||
```yaml
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {type: capability, field: torque_allowed, equals: true}
|
||||
- {type: param, key: NeuralNetworkLateralControl, equals: false}
|
||||
```
|
||||
|
||||
**Negation across multiple platforms** (everything except Rivian + Tesla-no-bus):
|
||||
```yaml
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {$ref: "#/macros/mads_full_platforms"} # macro encapsulates the not(any(rivian, all(tesla, not(bus)))) logic
|
||||
```
|
||||
|
||||
If the same multi-condition block appears in 2+ items, **promote it to a macro** in `_macros.yaml`. Re-run `python sunnypilot/sunnylink/tools/apply_macros.py` to substitute existing inlined matches automatically.
|
||||
|
||||
### Mutual exclusion
|
||||
|
||||
```yaml
|
||||
- key: FeatureAlpha
|
||||
widget: toggle
|
||||
title: Feature Alpha
|
||||
enablement:
|
||||
- {type: param, key: FeatureBeta, equals: false}
|
||||
|
||||
- key: FeatureBeta
|
||||
widget: toggle
|
||||
title: Feature Beta
|
||||
enablement:
|
||||
- {type: param, key: FeatureAlpha, equals: false}
|
||||
```
|
||||
|
||||
### Add a section
|
||||
|
||||
In the page YAML, add an entry to the `sections` list:
|
||||
```yaml
|
||||
sections:
|
||||
- id: my_section
|
||||
title: My Section
|
||||
description: Optional subtitle
|
||||
enablement:
|
||||
- {$ref: "#/macros/longitudinal"}
|
||||
items:
|
||||
- {key: ..., widget: toggle, title: ...}
|
||||
```
|
||||
|
||||
Sections support `visibility`, `enablement`, and `attestation_required`. When section-level rules fail, all items within are dimmed.
|
||||
|
||||
### Add a sub-panel
|
||||
|
||||
Sub-panels nest inside the section they belong to:
|
||||
```yaml
|
||||
sections:
|
||||
- id: parent_section
|
||||
title: Parent
|
||||
items: [...]
|
||||
sub_panels:
|
||||
- id: my_sub
|
||||
label: Advanced Settings
|
||||
trigger_key: ParentParam
|
||||
trigger_condition: {type: param, key: ParentParam, equals: true}
|
||||
items:
|
||||
- {key: ..., widget: toggle, title: ...}
|
||||
```
|
||||
|
||||
### Add vehicle-brand settings
|
||||
|
||||
Edit `pages/vehicle.yaml`. Each section is a brand:
|
||||
```yaml
|
||||
id: vehicle
|
||||
kind: vehicle
|
||||
sections:
|
||||
- id: rivian
|
||||
title: Rivian Settings
|
||||
description: ''
|
||||
items:
|
||||
- key: RivianFeature
|
||||
widget: toggle
|
||||
title: Rivian One Pedal
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
```
|
||||
|
||||
`kind: vehicle` tells the compiler to emit this page as `vehicle_settings.<brand>` in the wire JSON.
|
||||
|
||||
### Add a feature with toggles, sub-panel, and macro
|
||||
|
||||
Example: "Smart Wipers" with a master toggle, intensity selector, and sub-panel for advanced tuning, gated to torque-steering Hyundais on offroad.
|
||||
|
||||
1. **Param keys** — register all 4 in `common/params_keys.h`.
|
||||
|
||||
2. **Decide on a macro** — if "torque Hyundai" gating is reused, add to `_macros.yaml`:
|
||||
```yaml
|
||||
torque_hyundai:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {type: capability, field: brand, equals: hyundai}
|
||||
- {type: capability, field: torque_allowed, equals: true}
|
||||
```
|
||||
|
||||
3. **Edit the relevant page** — `pages/visuals.yaml` (or wherever the feature lives). Add a new section + sub_panel:
|
||||
```yaml
|
||||
sections:
|
||||
- id: smart_wipers
|
||||
title: Smart Wipers
|
||||
description: Camera-driven wiper control (Hyundai/Kia, torque only)
|
||||
items:
|
||||
- key: SmartWipersEnabled
|
||||
widget: toggle
|
||||
title: Enable Smart Wipers
|
||||
enablement:
|
||||
- {$ref: "#/macros/torque_hyundai"}
|
||||
- key: SmartWipersIntensity
|
||||
widget: multiple_button
|
||||
title: Sensitivity
|
||||
options:
|
||||
- {value: 0, label: Low}
|
||||
- {value: 1, label: Medium}
|
||||
- {value: 2, label: High}
|
||||
visibility:
|
||||
- {type: param, key: SmartWipersEnabled, equals: true}
|
||||
enablement:
|
||||
- {$ref: "#/macros/torque_hyundai"}
|
||||
sub_panels:
|
||||
- id: smart_wipers_tuning
|
||||
label: Smart Wipers Tuning
|
||||
trigger_key: SmartWipersEnabled
|
||||
trigger_condition: {type: param, key: SmartWipersEnabled, equals: true}
|
||||
items:
|
||||
- key: SmartWipersHysteresis
|
||||
widget: option
|
||||
title: Hysteresis (frames)
|
||||
min: 1
|
||||
max: 30
|
||||
step: 1
|
||||
enablement:
|
||||
- {$ref: "#/macros/offroad"}
|
||||
- {$ref: "#/macros/advanced_only"}
|
||||
```
|
||||
|
||||
4. **Compile / validate / test**:
|
||||
```
|
||||
python sunnypilot/sunnylink/tools/compile_settings_ui.py
|
||||
python sunnypilot/sunnylink/tools/validate_settings_ui.py
|
||||
uv run python -m pytest sunnypilot/sunnylink/tests/
|
||||
```
|
||||
|
||||
`apply_macros.py` is automatic for newly-added items only if you wrote the rule list inline; for greenfield items, you'd write `$ref` directly.
|
||||
|
||||
### Change a toggle's behavior
|
||||
|
||||
1. Find the item in `pages/<page>.yaml`.
|
||||
2. Edit `visibility`/`enablement`/`options[].enablement` directly. Use macros where possible.
|
||||
3. **Add a regression test** in `sunnypilot/sunnylink/tests/test_settings_changes.py` that asserts the new gate exists. Use existing tests (e.g. `TestMadsBrandGates`, `TestNotEngagedReplacement`) as templates: lookup item by key, assert `_references_capability_field(rules, "...")` or `_flatten_rule_types(rules)` contains/excludes a type. This freezes the new behavior so a future edit won't silently revert it.
|
||||
4. Compile + run the full suite. Per-bug test should pass; structural tests should remain green.
|
||||
|
||||
### Change a widget type or options
|
||||
|
||||
Editing `widget:` from `toggle` to `multiple_button` is a frontend behavior change. Whenever you change widget shape:
|
||||
- The param's underlying type (bool / int / string) must match what the new widget writes. `toggle` writes bool; `multiple_button`/`option` write int/string. Update `params_keys.h` if the type changes.
|
||||
- Add an `options:` list when switching to `multiple_button` or `option`.
|
||||
- Old values stored on devices may not be valid for the new widget. Consider a migration in `sunnypilot/system/updated/` if users have stale values.
|
||||
|
||||
### Deprecate or remove a setting
|
||||
|
||||
1. Remove the item from `pages/<page>.yaml`.
|
||||
2. Remove the param key from `common/params_keys.h` **only after** confirming nothing in `selfdrive/`, `sunnypilot/`, or any controls code reads it.
|
||||
3. If the param has been on user devices, drop it via a migration (see `sunnypilot/system/updated/`) so stale values don't linger.
|
||||
4. Compile + validate + test. The validator's "no duplicate keys" + structural checks will fail if anything still references the removed key.
|
||||
|
||||
### Move a setting to another page
|
||||
|
||||
Cut the item block from one page YAML, paste into the target page's section. Compile + validate. The "no duplicate keys" check catches forgotten copies.
|
||||
|
||||
### Change display text
|
||||
|
||||
Edit `title:` or `description:` in the page YAML and recompile to regenerate `settings_ui.json`.
|
||||
|
||||
### Reorder sections, sub-panels, and items
|
||||
|
||||
Reorder them within their parent list in the YAML. The compiler preserves authored order — no `order:` field required at the section/sub_panel/item level (panel-level `order:` controls which page comes first in the side nav).
|
||||
|
||||
---
|
||||
|
||||
### Capability labels and tooltips
|
||||
|
||||
The schema response includes `capability_labels`, which map capability field names to descriptions. The frontend uses these to show contextual tooltips when a capability rule prevents a setting from being used.
|
||||
|
||||
The device defines these labels in `capabilities.py:CAPABILITY_LABELS`. Examples:
|
||||
|
||||
- `has_longitudinal_control` → "sunnypilot longitudinal control"
|
||||
- `torque_allowed` → "torque steering (not available for angle steering vehicles)"
|
||||
- `brand` → "Vehicle brand"
|
||||
|
||||
### Centralized param enforcement
|
||||
|
||||
The device-side UI enforces capability constraints in `selfdrive/ui/sunnypilot/ui_state.py:_enforce_constraints()`, which removes incompatible params based on car capabilities. This is the single source of truth for such constraints.
|
||||
|
||||
Settings layouts should not duplicate these params.remove() calls. Instead, rely on schema rules and centralized enforcement to prevent duplicate logic and ensure consistency.
|
||||
|
||||
Example constraints in `_enforce_constraints()`:
|
||||
- Angle steering cars: remove `EnforceTorqueControl` and `NeuralNetworkLateralControl`
|
||||
- No CarParams: remove all car-dependent params
|
||||
- No longitudinal: remove `ExperimentalMode`
|
||||
- No ICBM: remove `IntelligentCruiseButtonManagement`
|
||||
@@ -1071,10 +1071,6 @@
|
||||
"title": "Panda Som Reset Triggered",
|
||||
"description": ""
|
||||
},
|
||||
"ParamsVersion": {
|
||||
"title": "Params Version",
|
||||
"description": ""
|
||||
},
|
||||
"PlanplusControl": {
|
||||
"title": "Plan Plus Controls",
|
||||
"description": "Adjust planplus model recentering strength. The higher this number the more aggressively the model will recover to lanecenter, too high and it will ping-pong",
|
||||
@@ -1130,6 +1126,14 @@
|
||||
"title": "Route Count",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverEnabled": {
|
||||
"title": "Enable Screen Saver",
|
||||
"description": ""
|
||||
},
|
||||
"ScreenSaverTimeout": {
|
||||
"title": "Screen Saver Timeout",
|
||||
"description": ""
|
||||
},
|
||||
"SecOCKey": {
|
||||
"title": "Sec Oc Key",
|
||||
"description": ""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user