Compare commits

..

77 Commits

Author SHA1 Message Date
Jason Wen
6654e9cdf9 Merge branch 'master' into tinygrad-sync-4/25 2026-05-17 21:20:04 -04:00
Jason Wen
059d0b6c4c sunnylink SDUI: tweak DisableUpdate param for clarity (#1842)
* sunnylink SDUI: tweak DisableUpdate param for clarity

* sync
2026-05-17 20:40:56 -04:00
github-actions[bot]
c51ffe3808 [bot] Update Python packages (#1827)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-05-14 02:24:09 -04:00
Jason Wen
a15aed1a79 sunnylink: add CarParams fallback for brand-specific capabilities (#1839)
Brand-specific capabilities (hyundai_alpha_long_available,
subaru_has_sng) only resolved from CarPlatformBundle, which requires
manual car selection. Auto-fingerprinted vehicles had no bundle,
leaving these capabilities at default false — hiding vehicle settings
on the dashboard despite working on the device UI.

Add _resolve_brand_capabilities() with bundle-first, CP-fallback
pattern matching the device UI layouts (hyundai.py, subaru.py).

Fixes https://community.sunnypilot.ai/t/5126
2026-05-13 01:29:18 -04:00
Nayan
78007e82e0 ui: show default model name (#1837)
* py py py

* sunnylink too

* refactor

* this is not needed anymore

* mici mici

* ugh

* retry CI

* ui: refactor default model name handling

Move DEFAULT_MODEL constant into sunnypilot/models/default_model.py
and remove the one-liner common/model.py. Strip the hardcoded
" (Default)" suffix from the constant value so each UI site appends
it contextually, keeping the raw model name clean for the schema
payload to sunnylink.

Replace the DefaultModel param approach with schema["default_model"]
injected at schema assembly time, eliminating a redundant param write
on every sunnylinkd start. Remove DefaultModel from params_keys.h and
params_metadata.json.

Update update_default_model_name() to do a targeted regex replacement
instead of overwriting the whole file, since the constant now lives in
a module with other code.

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-05-11 01:19:10 -04:00
Jason Wen
b1a6223b14 ci: simplify cereal validation to sparse-checkout + pycapnp, drop scons (#1836)
* ci: simplify cereal validation to sparse-checkout + pycapnp, drop scons build

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* more

* fix: resolve cereal_dir to absolute path before passing to capnp.load

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* ci: init opendbc submodule after sparse checkout to resolve car.capnp symlink

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* try to break it

* Revert "try to break it"

This reverts commit 79ce135c5f.

* try to break it

* Revert "try to break it"

This reverts commit 1eaa9e79e6.

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-10 01:04:45 -04:00
Nayan
e771dfa007 sunnylink: fix max time offroad values (#1835)
fix sunnylink values
2026-05-09 23:47:07 -04:00
Jason Wen
c28eb95874 manager: disable DEVELOPMENT_ONLY reset (#1833) 2026-05-07 18:42:06 -04:00
Jason Wen
7ed960f713 release: ignore upstream IsReleaseBranch (#1831) 2026-05-07 10:53:21 -04:00
Jason Wen
7e2b8430c5 ui: update gates for certain toggles (#1830)
* don't use upstream's

* clean

* update schema

* fix

* mismatch test and fix
2026-05-06 21:27:43 -04:00
Jason Wen
521fa09b0d sunnylink SDUI: update stale reference in docs 2026-05-06 12:14:47 -04:00
Jason Wen
b9aa1962ca Update CHANGELOG.md 2026-05-05 22:59:44 -04:00
Jason Wen
6b1b6aca05 Update CHANGELOG.md 2026-05-05 21:14:19 -04:00
Jason Wen
41a8bc3fc4 Update CHANGELOG.md 2026-05-05 20:54:43 -04:00
Jason Wen
540f4f5933 Platform List: dynamically migrate CarPlatformBundle (#1828) 2026-05-05 20:27:11 -04:00
DevTekVE
53e5ae0578 sunnylink: switch athena domain (#1826)
use new domain
2026-05-05 04:31:07 -04:00
Amy Jeanes
2182be05ea ui: fix cellular toggles (#1810)
UI: Fix ToggleSP._render() missing return value breaking toggle callbacks

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-05-03 15:52:35 -04:00
Nayan
3e44c90c68 [MICI] ui: align upstream changes with sunnypilot settings buttons (#1782)
* align with upstream

* lint

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-05-03 15:29:19 -04:00
discountchubbs
6523084bfc commit 2026-05-03 11:48:11 -07:00
Jason Wen
2d35bd895f sunnylink: SDUI (#1780)
* 1

* fix

* uh

* more

* for now

* v2

* update

* more subs

* readme

* vehicle

* cycle init

* options

* new

* combine

* new

* dynamic unit

* default

* move it

* syncing more

* more

* boom shakalaka

* rearrange

* partial availability

* move

* more

* more

* hide

* move

* revert

* more

* new

* set

* sync

* dynamic

* slightly more

* update

* update

* less

* in another pr

* more

* move and fix

* device type

* order

* order

* order

* fixes

* ew

* Revert "fixes"

This reverts commit 53a2adb45a.

* sunnylink: bundle-first capabilities + protocol_version

Bundle-first brand resolution, fix steer_control_type to use
CP.steerControlType (physical) not lateralTuning.which() (tuning class),
add Subaru/Hyundai opaque per-platform flags from CarPlatformBundle, and
embed PROTOCOL_VERSION in the capabilities payload.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: tighten settings_ui gates against Raylib parity

Brand gates on MadsMainCruiseAllowed/MadsUnifiedEngagementMode (rivian +
tesla-no-bus); convert enablement->visibility on items Raylib hides
(DisableUpdates, EnableGithubRunner, EnableCopyparty, QuickBootToggle,
HyundaiLongitudinalTuning, LaneTurnValue, LagdToggleDelay); drop spurious
offroad_only on DynamicExperimentalControl + DisengageOnAccelerator and
SpeedLimit policy/offset over-gates; replace offroad_only with not_engaged
on AlphaLongitudinalEnabled/Toyota long toggles; extend release-branch
NOTs with is_sp_release; add Test Maneuvers section with attestation,
move LongitudinalManeuverMode in alongside new LateralManeuverMode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: extend settings_ui validator for new fields

Add SchemaItem fields title_param_suffix (object form), needs_onroad_cycle,
blocked, requires_attestation; add option-level enablement to SchemaOption;
add visibility/enablement/attestation_required to PanelSection; add
not_engaged Rule type; replace vehicle_settings inline shape with
VehicleBrandSettings def (title/description/items).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: inject torque tune options from versions JSON

Read latcontrol_torque_versions.json at schema generation time and
override TorqueControlTune options so adding a version to the JSON
flows to the dashboard without editing settings_ui.json by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: per-bug regression tests for settings_ui parity

Cover the audit findings end-to-end: MADS brand gates, Test Maneuvers
section + attestation, validator accepts the JSON, dynamic torque
options match the versions JSON, SP dev items gate on is_sp_release as
well as is_release, spurious offroad_only dropped from
DisengageOnAccelerator and DynamicExperimentalControl, and offroad_only
replaced with not_engaged on AlphaLongitudinalEnabled and the Toyota
long toggles. Validator test skips when jsonschema is unavailable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: protocol_version sentinel + opaque-flag tests

Pin PROTOCOL_VERSION to KNOWN_PROTOCOL_VERSIONS so a bump shows up in
review, and assert subaru_has_sng / hyundai_alpha_long_available are
declared with safe False defaults.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* schema: convert ShowAdvancedControls visibility to enablement

Items previously hidden when ShowAdvancedControls is off now render
as disabled with an ADVANCED tooltip. Same treatment for dependency-
based hides (LagdToggleDelay, LaneTurnValue) so users see what's
available and why it's locked. CameraOffset gets the Advanced gate it
was missing.

Affected: DisableUpdates, EnableGithubRunner, EnableCopyparty,
QuickBootToggle, LaneTurnValue, LagdToggleDelay, CameraOffset, and the
test_maneuvers section.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: route resume_queued to athena direct

Backend is retiring the CloudFront proxy on stg.api.sunnypilot.ai that
currently forwards ws|settings|navigation to athena.sunnylink.ai. Once
retired, /ws/{id}/resume_queued only resolves via athena.sunnylink.ai.

SunnylinkApi.resume_queued now temporarily swaps self.api_host to
ATHENA_HOST while re-entering api_get (preserves the SunnylinkEnabled
gate and existing JWT/UA header handling). Other device calls
(device/{id}/roles, device/{id}/users, v2/pilotauth/, backups) continue
to hit the sunnylink main API host unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* legacy code

* no

* new

* tests

* lint

* fix

* too verbose

* sunnylink: per-page YAML authoring for settings_ui + named rule macros

Replace hand-edited 2200-line settings_ui.json with a per-page YAML source
tree under settings_ui_src/, plus named rule fragments (`$ref` macros) so
the same condition isn't repeated verbatim across items.

Authoring surface (settings_ui_src/):
  pages/<page>.yaml   one file per panel; sections/items/sub_panels inline
  pages/vehicle.yaml  per-brand settings (kind: vehicle -> vehicle_settings)
  _macros.yaml        named rule fragments referenced via {$ref: "#/macros/<name>"}
  _schemas/*.json     JSON Schemas for IDE autocomplete + CI validation

Pages today: steering, cruise, display, visuals, toggles, device, software,
developer, models, vehicle (10).

Tools:
  compile_settings_ui.py  read src tree, resolve $refs, emit settings_ui.json
                          (--check mode for CI to fail on hand-edits)
  extract_settings_ui.py  one-shot: settings_ui.json -> per-page YAML
  apply_macros.py         one-shot: substitute matching rule blocks with $ref

Rule-block dedup wins (top duplicates collapsed to single macros):
  offroad (9x), longitudinal (4x), longitudinal_and_icbm (5x),
  hide_on_mici (4x), advanced_only (3x), mads_full_platforms (4x ~30-line
  nested any/all/not -> 1 macro + 4x one-line $ref).

Validator drift fixes:
  - Accept not_engaged rule type (already in schema.json + used by
    AlphaLongitudinalEnabled, ToyotaEnforceStockLongitudinal,
    ToyotaStopAndGoHack, LateralManeuverMode, LongitudinalManeuverMode).
  - Enforce {title, description, items} shape on every vehicle brand
    instead of allowing bare-list variants.

Wire format unchanged: compiled settings_ui.json structurally identical to
the prior hand-edited file, so the device generator (generate_settings_schema.py),
SettingsCapabilities, and frontend renderer all keep working without changes.

Tests: 16 new compiler tests (roundtrip, $ref semantics, depth/cycle limits,
page-tree integrity, vehicle kind routing). Existing 42 regression tests in
test_settings_changes.py and test_settings_schema.py remain green.

README rewritten for the new flow:
  - Decision table for offroad_only / not_engaged / param / capability
  - Multi-condition composition examples (AND/OR/mixed/3-way/negation)
  - Full feature walkthrough (multi-toggle + sub_panel + new macro)
  - Workflows for changing rules, widget type, deprecating an item
  - All How-To examples migrated from JSON to YAML

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* sunnylink: add `details` field for popover/modal help text

Adds an optional `details` field on settings items, distinct from the inline
`description`. When present, the frontend renders an info ("i") button on the
row that opens a modal with this content. Either field may be present alone,
or both together; an item with only `details` and no `description` shows just
the title + info button (no inline body).

Migrates `AutoLaneChangeTimer` as the first user: the safety caveat
("Please use caution when using this feature. Only use the blinker when
traffic and road conditions permit.") moves out of the inline description
into `details`, leaving the description focused on what the timer actually
does.

Schema changes:
- settings_ui.schema.json: SchemaItem.details (string, optional)
- settings_ui_src/_schemas/page.schema.json: Item.details (mirror)
- compile_settings_ui.py / extract_settings_ui.py: ordered after description

Frontend support lands in sunnylink-frontend on the design-overhaul branch.
Old frontends ignore the unknown key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ui: refactor UIStateSP init to eliminate getattr and double update_params

- Remove update_params() and reset_onroad_sleep_timer() from UIStateSP.__init__;
  __init__ is now pure attr initialization with no I/O or side effects
- Add _sp_initialized flag to UIStateSP.update_params() to fire
  reset_onroad_sleep_timer() exactly once after first real param load,
  keeping the call in SP territory and out of stock _initialize()
- Replace getattr(self, 'has_longitudinal_control') / getattr(self, 'CP')
  in _enforce_constraints with direct attr access; safe because
  _enforce_constraints is only called from update_params(), which only
  runs after UIState._initialize() has set both attrs
- Rename _enforce_sp_constraints → _enforce_constraints

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix

* exec

* more exec

* humanize

* sync

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:18:15 -04:00
Jason Wen
855d5022ad MADS: suppress espActive event when long is not engaged (#1823) 2026-05-03 02:30:19 -04:00
github-actions[bot]
6a363365ab [bot] Update Python packages (#1822)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-05-03 01:00:33 -04:00
James Vecellio-Grant
94737c523d Merge branch 'master' into tinygrad-sync-4/25 2026-04-28 11:34:56 -07:00
discountchubbs
46fd88376e big or small brain? 2026-04-28 11:34:04 -07:00
Jason Wen
ddb9039493 torque: show static override values in Dev UI & gate useParams on custom torque tune (#1821)
torque: show static override values in Dev UI
2026-04-28 07:26:05 -04:00
Jason Wen
0b7df7df10 DM: wheel touch enforcement in MADS (#1820) 2026-04-28 01:29:54 -04:00
discountchubbs
3ac95a7475 no HACKS 2026-04-27 20:27:54 -07:00
discountchubbs
4cc84c5680 no hacks 2026-04-27 20:20:50 -07:00
github-actions[bot]
dd3feac854 [bot] Update Python packages (#1812)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-27 23:20:24 -04:00
discountchubbs
0768b2408c try this 2026-04-27 11:02:37 -07:00
discountchubbs
402f3c8966 legacy 2026-04-27 09:29:15 -07:00
discountchubbs
ae44e4d998 fetch 2026-04-27 07:59:20 -07:00
discountchubbs
ccf40652b6 correct tg 2026-04-26 20:01:21 -07:00
discountchubbs
271ed5e091 dm 2026-04-26 13:03:04 -07:00
discountchubbs
41dea5d48d Revert "sync dmonitoring too"
This reverts commit dc11e5fd84.
2026-04-26 13:02:31 -07:00
discountchubbs
dc11e5fd84 sync dmonitoring too 2026-04-26 12:53:45 -07:00
discountchubbs
ced4a664cc use upstreams 2026-04-26 11:18:51 -07:00
discountchubbs
03db277c22 dev 2026-04-26 11:15:34 -07:00
discountchubbs
11ed3800bf ugh 2026-04-26 11:10:17 -07:00
discountchubbs
92526b878c json v17 2026-04-26 11:07:17 -07:00
discountchubbs
66ff8ae52c shebang 2026-04-26 00:33:45 -07:00
discountchubbs
d85cb76304 lint 2026-04-26 00:32:31 -07:00
James Vecellio-Grant
b4c613680e Update SConscript 2026-04-26 00:29:30 -07:00
discountchubbs
f7511491f7 sync new modeld changes 2026-04-26 00:27:47 -07:00
discountchubbs
88b30e199b fix to new tg 2026-04-25 23:47:26 -07:00
discountchubbs
2898f394dd bump 2026-04-25 23:38:31 -07:00
discountchubbs
554cf9ca4a modeld: sync tinygrad 2026-04-25 23:28:02 -07:00
Jason Wen
10a33a4bf1 tg: reapply changes (#1817)
* Reapply "bump tg (#37700)"

This reverts commit 9022b4d322.

* fixup! Reapply "bump tg (#37700)"

* Revert "Revert "autodetect tg backend (#37778)""

This reverts commit b782958a

* Reapply "autodetect tg backend: use CPU:LLVM on Linux (#37785)"

This reverts commit 3fa6726f88.

* Reapply "Modeld: support uncompiled"

This reverts commit 8c240cc1a4.

* fixup! Reapply "bump tg (#37700)"

* fixup! Reapply "autodetect tg backend: use CPU:LLVM on Linux (#37785)"

* fixup! Revert "Revert "autodetect tg backend (#37778)""

* fixup! Reapply "autodetect tg backend: use CPU:LLVM on Linux (#37785)"

* fixup! Reapply "autodetect tg backend: use CPU:LLVM on Linux (#37785)"

* fixup! Revert "Revert "autodetect tg backend (#37778)""

* fixup! Reapply "bump tg (#37700)"

* fixup! Reapply "bump tg (#37700)"
2026-04-26 01:32:55 -04:00
DevTekVE
8714203d2c sunnylink: Remove unused API endpoint (#1814)
remove ws_queue thread and associated api endpoint
- Simplified thread management by removing unused `ws_queue`.
- Eliminated `resume_queued` API call for better maintainability.
2026-04-25 11:21:55 -04:00
Nayan
18406e77ee [MICI] ui: add sunnylink info & connectivity check (#1798)
* add info & connectivity check

* meh, no icon

* lint

* fix state

* good bot

---------

Co-authored-by: DevTekVE <devtekve@gmail.com>
2026-04-19 11:55:00 -04:00
Jason Wen
fdd43f49e0 ci: validate upstream compat by schema diff typeId (#1809) 2026-04-18 16:06:15 -04:00
Jason Wen
f93481d0d4 Reapply "controls: always default Torque Lateral Control to v0 Tune" (#1806) (#1807)
* Reapply "controls: always default Torque Lateral Control to v0 Tune" (#1806)

This reverts commit 5f6e05410d.

* this is better
2026-04-18 03:10:40 -04:00
Jason Wen
5f6e05410d Revert "controls: always default Torque Lateral Control to v0 Tune" (#1806)
Revert "controls: always default Torque Lateral Control to v0 Tune (#1804)"

This reverts commit d1d6fae613.
2026-04-18 00:48:28 -04:00
Jason Wen
d1d6fae613 controls: always default Torque Lateral Control to v0 Tune (#1804)
* controls: always default Torque Lateral Control to v0 Tune

* add fixme
2026-04-17 03:28:58 -04:00
github-actions[bot]
35aeeee657 [bot] Update Python packages (#1799)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-17 03:07:56 -04:00
Jason Wen
a6fdef77df Sync: commaai/openpilot:mastersunnypilot/sunnypilot:master (#1803) 2026-04-17 02:47:36 -04:00
Jason Wen
df66604a45 Merge branch 'upstream/openpilot/master' into sync-20260417
# Conflicts:
#	docs/CARS.md
#	opendbc_repo
#	panda
#	system/updated/updated.py
#	tinygrad_repo
2026-04-17 02:36:53 -04:00
Trey Moen
c001f3c9b4 esim: implement download profile (#37806) 2026-04-15 22:06:54 -07:00
Adeeb Shihadeh
752fe03118 docs: add feedback guide (#37842)
* docs: add feedback guide

* lil more feedback

* the
2026-04-15 19:13:30 -07:00
Adeeb Shihadeh
84c276bb6c misc docs updates (#37839)
* that's home

* more extensions

* lil fixups

* lil more

* collapse car ports

* fix links
2026-04-15 18:37:23 -07:00
Adeeb Shihadeh
9042cfa1ad rm home 2026-04-15 15:55:37 -07:00
Adeeb Shihadeh
83e6e7da93 docs: setup zensical (#37838) 2026-04-15 15:46:43 -07:00
Adeeb Shihadeh
31403f4a5c jotplugger: render segment range slices properly (#37837) 2026-04-15 10:50:24 -07:00
Ethan Reish
117d5cee4f updated: complete branch migration dict for mici and tizi (#37830) 2026-04-15 10:46:15 -07:00
Adeeb Shihadeh
fa18e6395c tools: remove sentry logging (#37836) 2026-04-15 10:26:12 -07:00
Adeeb Shihadeh
75e352e5d0 jotpluggler: CAN parsing is best effort (#37835) 2026-04-15 10:24:37 -07:00
Shane Smiskol
63ab2fb1b3 four: calibrate DM orientation (#37149)
* calib face

* clean up

* fix double neg

* cmt

* local changes

* keep for dm preview for now

* comments

* comments

* Update selfdrive/ui/mici/onroad/driver_state.py

* Update selfdrive/ui/mici/onroad/driver_state.py
2026-04-15 10:15:32 -07:00
commaci-public
97f1bac71d [bot] Update Python packages (#37815)
Update Python packages

Co-authored-by: Vehicle Researcher <user@comma.ai>
2026-04-15 09:54:46 -07:00
Trey Moen
00b7c8e8ad esim: implement ES9P HTTP and notification processing (#37807) 2026-04-15 09:44:28 -07:00
Daniel Koepping
1276452cfc add blocked-startup offroad label (#37833)
add blocked-startup offroad label
2026-04-15 00:15:57 -07:00
Daniel Koepping
8b2eac4d1f use warning as alert icon (#37832)
replace bell with warning icon
2026-04-15 00:09:59 -07:00
Daniel Koepping
e78e6261ca Add touch zone for alerts (#37819)
* add touch zone for alerts

* adjust touch zone

* 200 px zones

* 50/30 split

* adjust zones
2026-04-14 23:19:40 -07:00
Daniel Koepping
d5f1d8c33a fix compare_logs crash on list field size changes (#37829)
fix crash on list-length changes
2026-04-14 20:18:31 -07:00
stef
e16d422cf4 body quality of life (#37803)
* turn off ir leds on notCar

* reduce startup time on notCar

* fix: check notCar once after onroad is true

* save a disk read by using existing is_onroad
2026-04-14 19:13:57 -07:00
Adeeb Shihadeh
f70a156c7e docs: fix invalid img tags in car docs (#37828) 2026-04-14 16:35:34 -07:00
Adeeb Shihadeh
d204d626bd jp: skip unparsable messages (#37816) 2026-04-14 14:53:49 -07:00
Daniel Koepping
4a15bdcdae Add alerts pill (#37821)
* add notification pill

* add update

* comment

* lfs upload

* rename

* fix

* try widget

* white is banned!

* same for bell

* need everywhere?

---------

Co-authored-by: Shane Smiskol <shane@smiskol.com>
2026-04-14 12:02:30 -07:00
146 changed files with 10313 additions and 3470 deletions

View File

@@ -23,56 +23,43 @@ 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
- name: Checkout sunnypilot cereal
uses: actions/checkout@v6
- name: Checkout upstream openpilot
with:
sparse-checkout: cereal
- name: Init sunnypilot opendbc submodule
run: git submodule update --init --depth 1 opendbc_repo
- name: Checkout upstream openpilot cereal
uses: actions/checkout@v6
with:
repository: 'commaai/openpilot'
path: openpilot
submodules: true
path: upstream_openpilot
sparse-checkout: cereal
ref: "refs/heads/master"
- 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'
- 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: |
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
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

View File

@@ -29,9 +29,9 @@ jobs:
# Build
- name: Build docs
run: |
# TODO: can we install just the "docs" dependency group without the normal deps?
pip install mkdocs
mkdocs build
git lfs pull
pip install zensical
python scripts/docs.py build
# Push to docs.comma.ai
- uses: actions/checkout@v6

View File

@@ -172,7 +172,7 @@ jobs:
output_file="${{ env.MODELS_DIR }}/${base_name}_tinygrad.pkl"
echo "Compiling: $onnx_file -> $output_file"
QCOM=1 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 IMAGE=2 python3 "${{ env.TINYGRAD_PATH }}/examples/openpilot/compile3.py" "$onnx_file" "$output_file"
DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0 python3 "${{ env.MODELS_DIR }}/../get_model_metadata.py" "$onnx_file" || true
done

2
.gitignore vendored
View File

@@ -44,8 +44,10 @@ 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

View File

@@ -1,4 +1,4 @@
sunnypilot Version 2026.001.000 (2026-03-xx)
sunnypilot Version 2026.001.000 (2026-05-06)
========================
* What's Changed (sunnypilot/sunnypilot)
* Complete rewrite of the user interface from Qt C++ to Raylib Python
@@ -66,6 +66,64 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
* 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
@@ -84,12 +142,25 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
* 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"
@@ -99,6 +170,20 @@ sunnypilot Version 2026.001.000 (2026-03-xx)
* @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)
========================

View File

@@ -267,9 +267,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
active @2 :Bool;
vTarget @3 :Float32;
aTarget @4 :Float32;
capDelta @5 :Float32; # Difference between cluster set-speed and cap (m/s), positive = driver above cap
targetCap @6 :Float32; # Speed limit cap being enforced (m/s)
disableReason @7 :AssistDisableReason;
}
enum Source {
@@ -285,19 +282,6 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 {
pending @3; # Awaiting new speed limit.
adapting @4; # Reducing speed to match new speed limit.
active @5; # Cruising at speed limit.
capping @6; # Silently capping speed based on limit.
tempPaused @7; # Temporarily paused by user.
}
enum AssistDisableReason {
none @0;
userCancel @1;
userTempPause @2;
longOverride @3;
belowFloor @4;
autoResume @5;
mapGap @6;
gateDisabled @7;
}
}
@@ -358,7 +342,6 @@ struct OnroadEventSP @0xda96579883444c35 {
speedLimitChanged @21;
speedLimitPending @22;
e2eChime @23;
speedLimitCapActive @24;
}
}

View File

@@ -1,222 +1,256 @@
#!/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, List, Tuple
from typing import Any
DEBUG = False
NO_DISCRIMINANT = 0xFFFF
def print_debug(string: str) -> None:
if DEBUG:
print(string)
def hex_id(value: int) -> str:
return f"0x{value:016x}"
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_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 get_schema_fields(schema_struct: Any) -> List[Tuple[str, Any]]:
"""
Retrieve all fields from a given schema structure.
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
Args:
schema_struct: The Cap'n Proto schema structure
if proto.which() == "group":
type_desc = {"kind": "group", "typeId": hex_id(proto.group.typeId)}
else:
type_desc = encode_type(proto.slot.type)
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 []
return {
"name": name,
"ordinal": ordinal,
"discriminant": discriminant,
"type": type_desc,
}
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 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 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 _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 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 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 compare_schemas(original_instances: List[Any], read_instances: List[Any]) -> bool:
"""
Compare original and read-back instances to detect potential breaking changes.
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])
Args:
original_instances: List of originally generated instances
read_instances: List of instances read back from file
Returns:
Boolean indicating whether schemas appear compatible
"""
if len(original_instances) != len(read_instances):
print("❌ Schema Compatibility Warning: Instance count mismatch")
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"):
return False
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
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
def main():
"""
CLI entry point for schema compatibility testing.
"""
# Setup argument parser
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:
parser = argparse.ArgumentParser(
description='Cap\'n Proto Schema Compatibility Testing Tool',
epilog='Test schema compatibility by generating and reading back instances.'
description="sunnypilot <-> upstream cereal compatibility validator (schema-level)."
)
# 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
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")
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:
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)
dump_schema(args.cereal_dir, args.file)
return 0
return run_read(args.cereal_dir, args.file)
if __name__ == "__main__":
main()
sys.exit(main())

View File

@@ -1 +0,0 @@
#define DEFAULT_MODEL "POP model (Default)"

View File

@@ -204,6 +204,7 @@ 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}},
@@ -261,9 +262,6 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}},
{"SpeedLimitPolicy", {PERSISTENT | BACKUP, INT, "3"}},
{"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}},
{"SpeedLimitUpshiftAccept", {PERSISTENT | BACKUP, INT, "0"}},
{"SpeedLimitMinCapFloor", {PERSISTENT | BACKUP, INT, "25"}},
{"SpeedLimitCapAudioCue", {PERSISTENT | BACKUP, INT, "1"}},
// Smart Cruise Control
{"MapTargetVelocities", {CLEAR_ON_ONROAD_TRANSITION, STRING}},

View File

@@ -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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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 EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 GM connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Chevrolet Bolt EUV 2022-23">Buy Here</a></sub></details>|<a href="https://youtu.be/xvwzGMUA210" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[<sup>1</sup>](#footnotes)|3 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None|<a href="https://youtu.be/VT-i3yRsX2s?t=2736" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Focus 2018[<sup>2</sup>](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Kuga Plug-in Hybrid 2024|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>|
|Ford|Mustang Mach-E 2021-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>|
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hatchback 2017-18|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2025-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Palisade 2020-22|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|Santa Cruz 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2019-20|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2020-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2019|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 (with HDA II) 2024-25|Highway Driving Assist II|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2021-23|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2018-20|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Kia Stinger 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=MJ94qoofYw0" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Kia|Stinger 2022-23|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|GS F 2016|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=MAN TGE 2017-24">Buy Here</a></sub></details>|<a href="https://youtu.be/4100gLeabmo" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Mazda connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Mazda CX-9 2021-23">Buy Here</a></sub></details>|<a href="https://youtu.be/dA3duO4a0O4" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Nissan[<sup>5</sup>](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2025|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru Crosstrek 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2018-19|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Subaru A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Subaru XV 2018-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|<a href="https://youtu.be/Agww7oE1k-s?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Subaru|XV 2020-21|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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 2018-20|All|Stock|0 mph[<sup>10</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=fkcjviZY9CM" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Toyota|Camry 2021-24|All|openpilot|0 mph[<sup>10</sup>](#footnotes)|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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 2018-20|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Camry Hybrid 2018-20">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=Q2DYY0AWKgk" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hatchback 2019-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 Hybrid 2020-22|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota Highlander 2017-19">Buy Here</a></sub></details>|<a href="https://www.youtube.com/watch?v=0wS0wXSLzoo" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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 2020-23|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 Toyota A connector<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Toyota RAV4 Hybrid 2017-18">Buy Here</a></sub></details>|<a href="https://youtu.be/LhT5VzJVfNI?t=26" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>||
|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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 2019-21|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>||
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 OBD-C cable (2 ft)<br>- 1 VW J533 connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Volkswagen Arteon Shooting Brake 2020-23">Buy Here</a></sub></details>|<a href="https://youtu.be/FAomFKPFlDA" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>||
|Volkswagen[<sup>11</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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>|||

24
docs/DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,24 @@
# 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/

View File

@@ -1,26 +0,0 @@
# 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
docs/assets/comma-logo.png Symbolic link
View File

@@ -0,0 +1 @@
../../selfdrive/assets/icons_mici/settings/comma_icon.png

View File

@@ -1,5 +0,0 @@
# 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.

View File

@@ -1,65 +0,0 @@
# 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`

View File

@@ -1,5 +0,0 @@
# 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/.

View File

@@ -1,85 +0,0 @@
# 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.

View File

@@ -1,9 +1,3 @@
# openpilot 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**: 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).
{{GLOSSARY_DEFINITIONS}}

View File

@@ -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.bz2
## rlog.zst
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.
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 [Capn Proto](https://capnproto.org/) messages.
## {f,e,d}camera.hevc
@@ -18,12 +18,10 @@ 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.bz2 & qcamera.ts
## qlog.zst & 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 and store forever, yet useful enough for most analysis and debugging.
qlogs and qcameras are designed to be small enough to upload instantly on slow internet, yet useful enough for most analysis and debugging.

View File

@@ -0,0 +1,36 @@
# 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).

View File

@@ -7,25 +7,11 @@ 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)

View File

@@ -1,44 +0,0 @@
[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;
}

216
docs/ext/glossary.py Normal file
View File

@@ -0,0 +1,216 @@
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)

8
docs/ext/glossary.toml Normal file
View File

@@ -0,0 +1,8 @@
[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)."

View File

@@ -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.

View File

View File

@@ -1,68 +0,0 @@
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)

View File

@@ -8,7 +8,7 @@ A car port enables openpilot support on a particular car. Each car model openpil
# Structure of a car port
Virtually all car-specific code is contained in two other repositories: [opendbc](https://github.com/commaai/opendbc) and [panda](https://github.com/commaai/panda).
All car-specific code is contained in the [opendbc](https://github.com/commaai/opendbc) project.
## opendbc
@@ -23,8 +23,8 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b
## safety
* `opendbc_repo/opendbc/safety/modes/[brand].h`: Brand-specific safety logic
* `opendbc_repo/opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
* `opendbc/safety/modes/[brand].h`: Brand-specific safety logic
* `opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
## openpilot
@@ -32,8 +32,20 @@ For historical reasons, openpilot still contains a small amount of car-specific
* `selfdrive/car/car_specific.py`: Brand-specific event logic
# Overview
# How do I port car?
[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/.

View File

@@ -1,15 +1,15 @@
# connect to a comma four
# connect to a comma 3X or comma four
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).
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).
## Serial Console
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.
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 the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
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 four, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
Login to the default user with:
* 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`
`ssh comma@192.168.43.1 -i ~/.ssh/my_github_key`
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 on the comma four.
> The default port for ADB is 5555.
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,6 +79,7 @@ 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

View File

@@ -1 +0,0 @@
getting-started/what-is-openpilot.md

12
docs/index.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,42 @@
.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;
}

View File

@@ -1,44 +0,0 @@
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

2
panda

Submodule panda updated: c0cc96fbad...0a9ef7ab54

View File

@@ -82,7 +82,7 @@ dependencies = [
[project.optional-dependencies]
docs = [
"Jinja2",
"mkdocs",
"zensical",
]
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, ./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, *.pem, ./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]

63
scripts/docs.py Normal file
View File

@@ -0,0 +1,63 @@
"""
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()

Binary file not shown.

Binary file not shown.

View File

@@ -1,6 +1,6 @@
{% set footnote_tag = '[<sup>{}</sup>](#footnotes)' %}
{% set star_icon = '[![star](assets/icon-star-{}.svg)](##)' %}
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg"></img></a>' %}
{% set video_icon = '<a href="{}" target="_blank"><img height="18px" src="assets/icon-youtube.svg" /></a>' %}
{# Force hardware column wider by using a blank image with max width. #}
{% set width_tag = '<a href="##"><img width=2000></a>%s<br>&nbsp;' %}
{% set hardware_col_name = 'Hardware Needed' %}

View File

@@ -86,7 +86,7 @@ class Car:
self.can_callbacks = can_comm_callbacks(self.can_sock, self.pm.sock['sendcan'])
is_release = self.params.get_bool("IsReleaseBranch")
is_release = False # self.params.get_bool("IsReleaseBranch")
is_release_sp = self.params.get_bool("IsReleaseSpBranch")
if CI is None:

View File

@@ -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"></img></a>'
'<img height="18px" src="https://media.githubusercontent.com/media/commaai/openpilot/master/docs/assets/icon-youtube.svg" /></a>'
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
ARROW_SYMBOL = "➡️"

View File

@@ -1,6 +1,22 @@
import os
import glob
import json
import os
from itertools import product
from SCons.Script import Value
from openpilot.common.file_chunker import chunk_file, get_chunk_paths
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.selfdrive.modeld.helpers import CompileConfig
from tinygrad import Device
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
MODELD_CONFIGS = [CompileConfig(cam_w, cam_h, prepare_only, 'driving_')
for (cam_w, cam_h), prepare_only in product(CAMERA_CONFIGS, [True, False])]
DM_WARP_CONFIGS = [CompileConfig(cam_w, cam_h, True, 'dm_') for cam_w, cam_h in CAMERA_CONFIGS]
Import('env', 'arch')
chunker_file = File("#common/file_chunker.py")
@@ -13,31 +29,72 @@ 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
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 fastest TG config
available = set(Device.get_available_devices())
if '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 OPENPILOT_HACKS=1'
else:
tg_backend = 'CPU' if arch == 'Darwin' else 'CPU:LLVM'
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
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 ''
# Get model metadata
for model_name in ['driving_vision', 'driving_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} python3 {Dir("#selfdrive/modeld").abspath}/get_model_metadata.py {fn}.onnx'
lenv.Command(fn + "_metadata.pkl", [fn + ".onnx"] + tinygrad_files + script_files, cmd)
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)
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} 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, compile_warp_cmd)
modeld_dir = Dir("#selfdrive/modeld").abspath
compile_modeld_script = [File(f"{modeld_dir}/compile_modeld.py")]
compile_dm_warp_script = [File(f"{modeld_dir}/compile_dm_warp.py")]
driving_onnx_deps = [File(f"models/{m}.onnx").abspath for m in ['driving_vision', 'driving_policy']]
driving_metadata_deps = [File(f"models/{m}_metadata.pkl").abspath for m in ['driving_vision', 'driving_policy']]
model_w, model_h = MEDMODEL_INPUT_SIZE
frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
for cfg in MODELD_CONFIGS:
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_modeld.py '
f'--model-size {model_w}x{model_h} '
f'--nv12 {",".join(str(x) for x in cfg.nv12)} '
f'--vision-onnx {File("models/driving_vision.onnx").abspath} '
f'--policy-onnx {File("models/driving_policy.onnx").abspath} '
f'--output {cfg.pkl_path} --frame-skip {frame_skip}'
+ (' --prepare-only' if cfg.prepare_only else ''))
node = lenv.Command(cfg.pkl_path, tinygrad_files + compile_modeld_script + driving_onnx_deps + driving_metadata_deps + [chunker_file, compiled_flags_node], cmd)
onnx_sizes_sum = sum(os.path.getsize(f) for f in driving_onnx_deps)
chunk_targets = get_chunk_paths(cfg.pkl_path, estimate_pickle_max_size(onnx_sizes_sum))
def do_chunk(target, source, env, pkl=cfg.pkl_path, chunks=chunk_targets):
chunk_file(pkl, chunks)
lenv.Command(chunk_targets, node, do_chunk)
dm_w, dm_h = DM_INPUT_SIZE
for cfg in DM_WARP_CONFIGS:
cmd = (f'{tg_flags} {mac_brew_string} {image_flag} python3 {modeld_dir}/compile_dm_warp.py '
f'--nv12 {",".join(str(x) for x in cfg.nv12)} --warp-to {dm_w}x{dm_h} '
f'--output {cfg.pkl_path}')
lenv.Command(cfg.pkl_path, tinygrad_files + compile_dm_warp_script + compile_modeld_script + [compiled_flags_node], cmd)
def tg_compile(flags, model_name):
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + '"'
@@ -47,7 +104,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],
[onnx_path] + tinygrad_files + [chunker_file, compiled_flags_node],
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {pkl}',
)
def do_chunk(target, source, env):
@@ -58,7 +115,4 @@ def tg_compile(flags, model_name):
do_chunk,
)
# Compile small models
for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']:
tg_compile(tg_flags, model_name)
tg_compile(tg_flags, 'dmonitoring_model')

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
import argparse
import pickle
import time
from tinygrad.tensor import Tensor
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from openpilot.selfdrive.modeld.compile_modeld import NV12Frame, warp_perspective_tinygrad, _parse_size, _parse_nv12
def make_warp_dm(nv12: NV12Frame, dm_w, dm_h):
cam_w, cam_h, stride, _, _, _ = nv12
stride_pad = stride - cam_w
def warp_dm(input_frame, M_inv):
M_inv = M_inv.to(Device.DEFAULT).realize()
return warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv,
(dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
return warp_dm
def compile_dm_warp(nv12: NV12Frame, dm_w, dm_h, pkl_path):
print(f"Compiling DM warp for {nv12.width}x{nv12.height} -> {dm_w}x{dm_h}...")
warp_dm_jit = TinyJit(make_warp_dm(nv12, dm_w, dm_h), prune=True)
for i in range(10):
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
M_inv = Tensor(Tensor.randn(3, 3).mul(8).realize().numpy(), device='NPY')
Device.default.synchronize()
st = time.perf_counter()
warp_dm_jit(frame, M_inv).realize()
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
with open(pkl_path, "wb") as f:
pickle.dump(warp_dm_jit, f)
print(f" Saved to {pkl_path}")
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--nv12', type=_parse_nv12, required=True,
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
p.add_argument('--warp-to', type=_parse_size, required=True, help='DM input WxH')
p.add_argument('--output', required=True)
args = p.parse_args()
dm_w, dm_h = args.warp_to
compile_dm_warp(args.nv12, dm_w, dm_h, args.output)

View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
import argparse
import pickle
import time
from functools import partial
from collections import namedtuple
import numpy as np
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from tinygrad.nn.onnx import OnnxRunner
# https://github.com/tinygrad/tinygrad/issues/15682
from tinygrad.uop.ops import UOp, Ops
_orig = UOp.__reduce__
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
NV12Frame = namedtuple("NV12Frame", ['width', 'height', 'stride', 'y_height', 'uv_height', 'size'])
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
w_dst, h_dst = dst_shape
h_src, w_src = src_shape
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
return src_flat[idx]
def frames_to_tensor(frames):
H = (frames.shape[0] * 2) // 3
W = frames.shape[1]
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
frames[1:H:2, 0::2],
frames[0:H:2, 1::2],
frames[1:H:2, 1::2],
frames[H:H+H//4].reshape((H//2, W//2)),
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
return in_img1
def make_frame_prepare(nv12: NV12Frame, model_w, model_h):
cam_w, cam_h, stride, y_height, uv_height, _ = nv12
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
M_inv, (model_w, model_h),
(cam_h, cam_w), stride_pad).realize()
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
tensor = frames_to_tensor(yuv)
return tensor
return frame_prepare_tinygrad
def make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip):
img = vision_input_shapes['img'] # (1, 12, 128, 256)
n_frames = img[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img[2], img[3])
fb = policy_input_shapes['features_buffer'] # (1, 25, 512)
dp = policy_input_shapes['desire_pulse'] # (1, 25, 8)
tc = policy_input_shapes['traffic_convention'] # (1, 2)
npy = {
'desire': np.zeros(dp[2], dtype=np.float32),
'traffic_convention': np.zeros(tc, dtype=np.float32),
'tfm': np.zeros((3, 3), dtype=np.float32),
'big_tfm': np.zeros((3, 3), dtype=np.float32),
}
input_queues = {
'img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
'big_img_q': Tensor.zeros(img_buf_shape, dtype='uint8').contiguous().realize(),
'feat_q': Tensor.zeros(frame_skip * (fb[1] - 1) + 1, fb[0], fb[2]).contiguous().realize(),
'desire_q': Tensor.zeros(frame_skip * dp[1], dp[0], dp[2]).contiguous().realize(),
**{k: Tensor(v, device='NPY').realize() for k, v in npy.items()},
}
return input_queues, npy
def shift_and_sample(buf, new_val, sample_fn):
buf.assign(buf[1:].cat(new_val, dim=0).contiguous())
return sample_fn(buf)
def sample_skip(buf, frame_skip):
return buf[::frame_skip].contiguous().flatten(0, 1).unsqueeze(0)
def sample_desire(buf, frame_skip):
return buf.reshape(-1, frame_skip, *buf.shape[1:]).max(1).flatten(0, 1).unsqueeze(0)
def make_run_policy(vision_runner, policy_runner, nv12: NV12Frame, model_w, model_h,
vision_features_slice, frame_skip, prepare_only=False):
frame_prepare = make_frame_prepare(nv12, model_w, model_h)
sample_skip_fn = partial(sample_skip, frame_skip=frame_skip)
sample_desire_fn = partial(sample_desire, frame_skip=frame_skip)
def run_policy(img_q, big_img_q, feat_q, desire_q, desire, traffic_convention, tfm, big_tfm, frame, big_frame):
tfm = tfm.to(Device.DEFAULT)
big_tfm = big_tfm.to(Device.DEFAULT)
desire = desire.to(Device.DEFAULT)
traffic_convention = traffic_convention.to(Device.DEFAULT)
Tensor.realize(tfm, big_tfm, desire, traffic_convention)
img = shift_and_sample(img_q, frame_prepare(frame, tfm).unsqueeze(0), sample_skip_fn)
big_img = shift_and_sample(big_img_q, frame_prepare(big_frame, big_tfm).unsqueeze(0), sample_skip_fn)
if prepare_only:
return img, big_img
vision_out = next(iter(vision_runner({'img': img, 'big_img': big_img}).values())).cast('float32')
new_feat = vision_out[:, vision_features_slice].reshape(1, -1).unsqueeze(0)
feat_buf = shift_and_sample(feat_q, new_feat, sample_skip_fn)
desire_buf = shift_and_sample(desire_q, desire.reshape(1, 1, -1), sample_desire_fn)
inputs = {'features_buffer': feat_buf, 'desire_pulse': desire_buf, 'traffic_convention': traffic_convention}
policy_out = next(iter(policy_runner(inputs).values())).cast('float32')
return vision_out, policy_out
return run_policy
def compile_modeld(nv12: NV12Frame, model_w, model_h, prepare_only, frame_skip,
vision_onnx, policy_onnx, pkl_path):
from get_model_metadata import metadata_path_for
print(f"Compiling combined policy JIT for {nv12.width}x{nv12.height} (prepare_only={prepare_only})...")
vision_runner = OnnxRunner(vision_onnx)
policy_runner = OnnxRunner(policy_onnx)
with open(metadata_path_for(vision_onnx), 'rb') as f:
vision_metadata = pickle.load(f)
vision_features_slice = vision_metadata['output_slices']['hidden_state']
vision_input_shapes = vision_metadata['input_shapes']
with open(metadata_path_for(policy_onnx), 'rb') as f:
policy_input_shapes = pickle.load(f)['input_shapes']
_run = make_run_policy(vision_runner, policy_runner, nv12, model_w, model_h,
vision_features_slice, frame_skip, prepare_only)
run_policy_jit = TinyJit(_run, prune=True)
N_RUNS = 3
SEED = 42
def random_inputs_run_fn(fn, seed, test_val=None, test_buffers=None, expect_match=True):
input_queues, npy = make_input_queues(vision_input_shapes, policy_input_shapes, frame_skip)
np.random.seed(seed)
Tensor.manual_seed(seed)
for i in range(N_RUNS):
frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
big_frame = Tensor.randint(nv12.size, low=0, high=256, dtype='uint8').realize()
for v in npy.values():
v[:] = np.random.randn(*v.shape).astype(v.dtype)
Device.default.synchronize()
st = time.perf_counter()
outs = fn(**input_queues, frame=frame, big_frame=big_frame)
mt = time.perf_counter()
for o in outs:
# .realize() not needed once jitted, but needed for unjitted fn
o.realize()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/{N_RUNS}] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
val = [np.copy(v.numpy()) for v in outs]
buffers = [np.copy(v.numpy().copy()) for v in input_queues.values()]
if test_val is not None:
match = all(np.array_equal(a, b) for a, b in zip(val, test_val, strict=True))
assert match == expect_match, f"outputs {'differ from' if expect_match else 'match'} baseline (seed={seed})"
if test_buffers is not None:
match = all(np.array_equal(a, b) for a, b in zip(buffers, test_buffers, strict=True))
assert match == expect_match, f"buffers {'differ from' if expect_match else 'match'} baseline (seed={seed})"
return fn, val, buffers
print('run unjitted')
_, test_val, test_buffers = random_inputs_run_fn(_run, seed=SEED)
print('capture + replay')
run_policy_jit, _, _ = random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers)
print('pickle round trip')
with open(pkl_path, "wb") as f:
pickle.dump(run_policy_jit, f)
print(f" Saved to {pkl_path}")
with open(pkl_path, "rb") as f:
run_policy_jit = pickle.load(f)
random_inputs_run_fn(run_policy_jit, SEED, test_val, test_buffers, expect_match=True)
random_inputs_run_fn(run_policy_jit, SEED+1, test_val, test_buffers, expect_match=False)
def _parse_size(s):
w, h = s.lower().split('x')
return int(w), int(h)
def _parse_nv12(s):
parts = s.split(',')
assert len(parts) == len(NV12Frame._fields), \
f"--nv12 expects {','.join(NV12Frame._fields)} (got {s!r})"
return NV12Frame(*(int(x) for x in parts))
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument('--model-size', type=_parse_size, required=True, help='model input WxH')
p.add_argument('--nv12', type=_parse_nv12, required=True,
help=f'NV12 frame layout: {",".join(NV12Frame._fields)}')
p.add_argument('--vision-onnx', required=True)
p.add_argument('--policy-onnx', required=True)
p.add_argument('--output', required=True)
p.add_argument('--prepare-only', action='store_true')
p.add_argument('--frame-skip', type=int, required=True)
args = p.parse_args()
model_w, model_h = args.model_size
compile_modeld(args.nv12, model_w, model_h, args.prepare_only, args.frame_skip,
args.vision_onnx, args.policy_onnx, args.output)

View File

@@ -1,209 +0,0 @@
#!/usr/bin/env python3
import time
import pickle
import numpy as np
from pathlib import Path
from tinygrad.tensor import Tensor
from tinygrad.helpers import Context
from tinygrad.device import Device
from tinygrad.engine.jit import TinyJit
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE, DM_INPUT_SIZE
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
MODELS_DIR = Path(__file__).parent / 'models'
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
IMG_BUFFER_SHAPE = (30, MEDMODEL_INPUT_SIZE[1] // 2, MEDMODEL_INPUT_SIZE[0] // 2)
def warp_pkl_path(w, h):
return MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
def dm_warp_pkl_path(w, h):
return MODELS_DIR / f'dm_warp_{w}x{h}_tinygrad.pkl'
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
w_dst, h_dst = dst_shape
h_src, w_src = src_shape
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
return src_flat[idx]
def frames_to_tensor(frames, model_w, model_h):
H = (frames.shape[0] * 2) // 3
W = frames.shape[1]
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
frames[1:H:2, 0::2],
frames[0:H:2, 1::2],
frames[1:H:2, 1::2],
frames[H:H+H//4].reshape((H//2, W//2)),
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
return in_img1
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
M_inv, (model_w, model_h),
(cam_h, cam_w), stride_pad).realize()
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
tensor = frames_to_tensor(yuv, model_w, model_h)
return tensor
return frame_prepare_tinygrad
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)
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
def make_update_both_imgs(frame_prepare, model_w, model_h):
update_img = make_update_img_input(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_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
def make_warp_dm(cam_w, cam_h, dm_w, dm_h):
stride, y_height, _, _ = get_nv12_info(cam_w, cam_h)
stride_pad = stride - cam_w
def warp_dm(input_frame, M_inv):
M_inv = M_inv.to(Device.DEFAULT)
result = warp_perspective_tinygrad(input_frame[:cam_h*stride], M_inv, (dm_w, dm_h), (cam_h, cam_w), stride_pad).reshape(-1, dm_h * dm_w)
return result
return warp_dm
def compile_modeld_warp(cam_w, cam_h):
model_w, model_h = MEDMODEL_INPUT_SIZE
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
print(f"Compiling modeld warp for {cam_w}x{cam_h}...")
frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h)
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
update_img_jit = TinyJit(update_both_imgs, prune=True)
full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
big_full_buffer = Tensor.zeros(IMG_BUFFER_SHAPE, dtype='uint8').contiguous().realize()
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()
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()
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
pkl_path = warp_pkl_path(cam_w, cam_h)
with open(pkl_path, "wb") as f:
pickle.dump(update_img_jit, f)
print(f" Saved to {pkl_path}")
jit = pickle.load(open(pkl_path, "rb"))
jit(*inputs)
def compile_dm_warp(cam_w, cam_h):
dm_w, dm_h = DM_INPUT_SIZE
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
print(f"Compiling DM warp for {cam_w}x{cam_h}...")
warp_dm = make_warp_dm(cam_w, cam_h, dm_w, dm_h)
warp_dm_jit = TinyJit(warp_dm, prune=True)
for i in range(10):
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()
warp_dm_jit(*inputs)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
pkl_path = dm_warp_pkl_path(cam_w, cam_h)
with open(pkl_path, "wb") as f:
pickle.dump(warp_dm_jit, f)
print(f" Saved to {pkl_path}")
def run_and_save_pickle():
for cam_w, cam_h in CAMERA_CONFIGS:
compile_modeld_warp(cam_w, cam_h)
compile_dm_warp(cam_w, cam_h)
if __name__ == "__main__":
run_and_save_pickle()

View File

@@ -1,12 +1,12 @@
#!/usr/bin/env python3
import os
from openpilot.system.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
set_tinygrad_backend_from_compiled_flags()
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
@@ -21,15 +21,14 @@ 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 = Path(__file__).parent / 'models/dmonitoring_model_tinygrad.pkl'
METADATA_PATH = Path(__file__).parent / 'models/dmonitoring_model_metadata.pkl'
MODELS_DIR = Path(__file__).parent / 'models'
MODEL_PKL_PATH = MODELS_DIR / 'dmonitoring_model_tinygrad.pkl'
METADATA_PATH = MODELS_DIR / 'dmonitoring_model_metadata.pkl'
class ModelState:
inputs: dict[str, np.ndarray]
output: np.ndarray
def __init__(self):
def __init__(self, cam_w: int, cam_h: int):
with open(METADATA_PATH, 'rb') as f:
model_metadata = pickle.load(f)
self.input_shapes = model_metadata['input_shapes']
@@ -41,22 +40,18 @@ class ModelState:
self.warp_inputs_np = {'transform': np.zeros((3,3), dtype=np.float32)}
self.warp_inputs = {k: Tensor(v, device='NPY') for k,v in self.warp_inputs_np.items()}
self.frame_buf_params = None
self.frame_buf_params = get_nv12_info(cam_w, cam_h)
self.tensor_inputs = {k: Tensor(v, device='NPY').realize() for k,v in self.numpy_inputs.items()}
self._blob_cache : dict[int, Tensor] = {}
self.image_warp = None
self.model_run = pickle.loads(read_file_chunked(str(MODEL_PKL_PATH)))
with open(CompileConfig(cam_w, cam_h, prefix='dm_', prepare_only=True).pkl_path, "rb") as f:
self.image_warp = pickle.load(f)
def run(self, buf: VisionBuf, calib: np.ndarray, transform: np.ndarray) -> tuple[np.ndarray, float]:
self.numpy_inputs['calib'][0,:] = calib
t1 = time.perf_counter()
if self.image_warp is None:
self.frame_buf_params = get_nv12_info(buf.width, buf.height)
warp_path = MODELS_DIR / f'dm_warp_{buf.width}x{buf.height}_tinygrad.pkl'
with open(warp_path, "rb") as f:
self.image_warp = pickle.load(f)
ptr = buf.data.ctypes.data
# There is a ringbuffer of imgs, just cache tensors pointing to all of them
if ptr not in self._blob_cache:
@@ -110,9 +105,6 @@ def get_driverstate_packet(model_output, frame_id: int, location_ts: int, exec_t
def main():
config_realtime_process(7, 5)
model = ModelState()
cloudlog.warning("models loaded, dmonitoringmodeld starting")
cloudlog.warning("connecting to driver stream")
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_DRIVER, True)
while not vipc_client.connect(False):
@@ -120,6 +112,9 @@ def main():
assert vipc_client.is_connected()
cloudlog.warning(f"connected with buffer size: {vipc_client.buffer_len}")
model = ModelState(vipc_client.width, vipc_client.height)
cloudlog.warning("models loaded, dmonitoringmodeld starting")
sm = SubMaster(["liveCalibration"])
pm = PubMaster(["driverStateV2"])

View File

@@ -7,6 +7,10 @@ from typing import Any
from tinygrad.nn.onnx import OnnxPBParser
def metadata_path_for(onnx_path) -> pathlib.Path:
p = pathlib.Path(onnx_path)
return p.parent / (p.stem + '_metadata.pkl')
class MetadataOnnxPBParser(OnnxPBParser):
def _parse_ModelProto(self) -> dict:
@@ -48,7 +52,7 @@ if __name__ == "__main__":
'output_shapes': dict(get_name_and_shape(x) for x in model["graph"]["output"]),
}
metadata_path = model_path.parent / (model_path.stem + '_metadata.pkl')
metadata_path = metadata_path_for(model_path)
with open(metadata_path, 'wb') as f:
pickle.dump(metadata, f)

View File

@@ -0,0 +1,31 @@
import json
import os
from dataclasses import dataclass
from pathlib import Path
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
MODELS_DIR = Path(__file__).resolve().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'])
@dataclass
class CompileConfig:
cam_w: int
cam_h: int
prepare_only: bool
prefix: str
@property
def pkl_path(self):
return str(MODELS_DIR / f'{self.prefix}{"warp_" if self.prepare_only else ""}{self.cam_w}x{self.cam_h}_tinygrad.pkl')
@property
def nv12(self):
return (self.cam_w, self.cam_h, *get_nv12_info(self.cam_w, self.cam_h))

View File

@@ -1,7 +1,8 @@
#!/usr/bin/env python3
import os
from openpilot.system.hardware import TICI
os.environ['DEV'] = 'QCOM' if TICI else 'CPU'
from openpilot.selfdrive.modeld.helpers import MODELS_DIR, CompileConfig, set_tinygrad_backend_from_compiled_flags
set_tinygrad_backend_from_compiled_flags()
USBGPU = "USBGPU" in os.environ
if USBGPU:
os.environ['DEV'] = 'AMD'
@@ -12,7 +13,6 @@ 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
@@ -26,6 +26,7 @@ from openpilot.common.transformations.model import get_warp_matrix
from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper
from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan
from openpilot.selfdrive.modeld.parse_model_outputs import Parser
from openpilot.selfdrive.modeld.compile_modeld import make_input_queues
from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState
from openpilot.common.file_chunker import read_file_chunked
from openpilot.selfdrive.modeld.constants import ModelConstants, Plan
@@ -37,18 +38,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'
LAT_SMOOTH_SECONDS = 0.0
LONG_SMOOTH_SECONDS = 0.3
MIN_LAT_CONTROL_SPEED = 0.3
IMG_QUEUE_SHAPE = (6*(ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ + 1), 128, 256)
assert IMG_QUEUE_SHAPE[0] == 30
def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action,
@@ -83,108 +79,39 @@ class FrameMeta:
if vipc is not None:
self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof
class InputQueues:
def __init__ (self, model_fps, env_fps, n_frames_input):
assert env_fps % model_fps == 0
assert env_fps >= model_fps
self.model_fps = model_fps
self.env_fps = env_fps
self.n_frames_input = n_frames_input
self.dtypes = {}
self.shapes = {}
self.q = {}
def update_dtypes_and_shapes(self, input_dtypes, input_shapes) -> None:
self.dtypes.update(input_dtypes)
if self.env_fps == self.model_fps:
self.shapes.update(input_shapes)
else:
for k in input_shapes:
shape = list(input_shapes[k])
if 'img' in k:
n_channels = shape[1] // self.n_frames_input
shape[1] = (self.env_fps // self.model_fps + (self.n_frames_input - 1)) * n_channels
else:
shape[1] = (self.env_fps // self.model_fps) * shape[1]
self.shapes[k] = tuple(shape)
def reset(self) -> None:
self.q = {k: np.zeros(self.shapes[k], dtype=self.dtypes[k]) for k in self.dtypes.keys()}
def enqueue(self, inputs:dict[str, np.ndarray]) -> None:
for k in inputs.keys():
if inputs[k].dtype != self.dtypes[k]:
raise ValueError(f'supplied input <{k}({inputs[k].dtype})> has wrong dtype, expected {self.dtypes[k]}')
input_shape = list(self.shapes[k])
input_shape[1] = -1
single_input = inputs[k].reshape(tuple(input_shape))
sz = single_input.shape[1]
self.q[k][:,:-sz] = self.q[k][:,sz:]
self.q[k][:,-sz:] = single_input
def get(self, *names) -> dict[str, np.ndarray]:
if self.env_fps == self.model_fps:
return {k: self.q[k] for k in names}
else:
out = {}
for k in names:
shape = self.shapes[k]
if 'img' in k:
n_channels = shape[1] // (self.env_fps // self.model_fps + (self.n_frames_input - 1))
out[k] = np.concatenate([self.q[k][:, s:s+n_channels] for s in np.linspace(0, shape[1] - n_channels, self.n_frames_input, dtype=int)], axis=1)
elif 'pulse' in k:
# any pulse within interval counts
out[k] = self.q[k].reshape((shape[0], shape[1] * self.model_fps // self.env_fps, self.env_fps // self.model_fps, -1)).max(axis=2)
else:
idxs = np.arange(-1, -shape[1], -self.env_fps // self.model_fps)[::-1]
out[k] = self.q[k][:, idxs]
return out
class ModelState(ModelStateBase):
inputs: dict[str, np.ndarray]
output: np.ndarray
prev_desire: np.ndarray # for tracking the rising edge of the pulse
def __init__(self):
def __init__(self, cam_w: int, cam_h: int):
ModelStateBase.__init__(self)
self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS
with open(VISION_METADATA_PATH, 'rb') as f:
vision_metadata = pickle.load(f)
self.vision_input_shapes = vision_metadata['input_shapes']
self.vision_input_names = list(self.vision_input_shapes.keys())
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:
policy_metadata = pickle.load(f)
self.policy_input_shapes = policy_metadata['input_shapes']
self.policy_output_slices = policy_metadata['output_slices']
policy_output_size = policy_metadata['output_shapes']['outputs'][1]
self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32)
# policy inputs
self.numpy_inputs = {k: np.zeros(self.policy_input_shapes[k], dtype=np.float32) for k in self.policy_input_shapes}
self.full_input_queues = InputQueues(ModelConstants.MODEL_CONTEXT_FREQ, ModelConstants.MODEL_RUN_FREQ, ModelConstants.N_FRAMES)
for k in ['desire_pulse', 'features_buffer']:
self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape})
self.full_input_queues.reset()
self.img_queues = {'img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize(),
'big_img': Tensor.zeros(IMG_QUEUE_SHAPE, dtype='uint8').contiguous().realize()}
self.frame_skip = ModelConstants.MODEL_RUN_FREQ // ModelConstants.MODEL_CONTEXT_FREQ
self.input_queues, self.npy = make_input_queues(self.vision_input_shapes, self.policy_input_shapes, self.frame_skip)
self.full_frames : dict[str, Tensor] = {}
self._blob_cache : dict[int, Tensor] = {}
self.transforms_np = {k: np.zeros((3,3), dtype=np.float32) for k in self.img_queues}
self.transforms = {k: Tensor(v, device='NPY').realize() for k, v in self.transforms_np.items()}
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.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.frame_buf_params = {k: get_nv12_info(cam_w, cam_h) for k in ('img', 'big_img')}
self.run_policy = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=False).pkl_path))
self.warp_enqueue = pickle.loads(read_file_chunked(CompileConfig(cam_w, cam_h, prefix='driving_', prepare_only=True).pkl_path))
self.warp_enqueue(
**self.input_queues,
frame=Tensor.zeros(self.frame_buf_params['img'][3], dtype='uint8').contiguous().realize(),
big_frame=Tensor.zeros(self.frame_buf_params['big_img'][3], dtype='uint8').contiguous().realize())
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()}
@@ -192,18 +119,6 @@ class ModelState(ModelStateBase):
def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray],
inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None:
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.prev_desire[:] = inputs['desire_pulse']
if self.update_imgs is None:
for key in bufs.keys():
w, h = bufs[key].width, bufs[key].height
self.frame_buf_params[key] = get_nv12_info(w, h)
warp_path = MODELS_DIR / f'warp_{w}x{h}_tinygrad.pkl'
with open(warp_path, "rb") as f:
self.update_imgs = pickle.load(f)
for key in bufs.keys():
ptr = bufs[key].data.ctypes.data
yuv_size = self.frame_buf_params[key][3]
@@ -212,31 +127,31 @@ class ModelState(ModelStateBase):
if cache_key not in self._blob_cache:
self._blob_cache[cache_key] = Tensor.from_blob(ptr, (yuv_size,), dtype='uint8')
self.full_frames[key] = self._blob_cache[cache_key]
for key in bufs.keys():
self.transforms_np[key][:,:] = transforms[key][:,:]
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'])
self.img_queues['img'], self.img_queues['big_img'] = out[0].realize(), out[2].realize()
vision_inputs = {'img': out[1], 'big_img': out[3]}
# Model decides when action is completed, so desire input is just a pulse triggered on rising edge
inputs['desire_pulse'][0] = 0
self.npy['desire'][:] = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0)
self.prev_desire[:] = inputs['desire_pulse']
self.npy['traffic_convention'][:] = inputs['traffic_convention']
self.npy['tfm'][:,:] = transforms['img'][:,:]
self.npy['big_tfm'][:,:] = transforms['big_img'][:,:]
if prepare_only:
self.warp_enqueue(**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img'])
return None
self.vision_output = self.vision_run(**vision_inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices))
vision_output, policy_output = self.run_policy(
**self.input_queues, frame=self.full_frames['img'], big_frame=self.full_frames['big_img']
)
self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire})
for k in ['desire_pulse', 'features_buffer']:
self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k]
self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention']
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))
vision_output = vision_output.numpy().flatten()
policy_output = policy_output.numpy().flatten()
vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(vision_output, self.vision_output_slices))
policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(policy_output, self.policy_output_slices))
combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict}
if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()])
if SEND_RAW_PRED:
combined_outputs_dict['raw_pred'] = np.concatenate([vision_output.copy(), policy_output.copy()])
return combined_outputs_dict
@@ -248,11 +163,6 @@ def main(demo=False):
# also need to move the aux USB interrupts for good timings
config_realtime_process(7, 54)
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState()
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# visionipc clients
while True:
available_streams = VisionIpcClient.available_streams("camerad", block=False)
@@ -276,6 +186,11 @@ def main(demo=False):
if use_extra_client:
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
st = time.monotonic()
cloudlog.warning("loading model")
model = ModelState(vipc_client_main.width, vipc_client_main.height)
cloudlog.warning(f"models loaded in {time.monotonic() - st:.1f}s, modeld starting")
# messaging
pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"])
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"])

View File

@@ -434,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['carState'].gasPressed
driver_engaged = sm['carState'].steeringPressed or (sm['selfdriveState'].enabled and 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(

View File

@@ -215,64 +215,48 @@ class TestMonitoring:
events[int((INVISIBLE_SECONDS_TO_RED-1+DT_DMON*d_status.settings._HI_STD_FALLBACK_TIME+0.1)/DT_DMON)].names
@pytest.mark.parametrize("enabled_state, lat_active_state, expected", [
(False, False, False), # Both Disabled
(True, False, True), # OP Enabled, Lat Inactive
(False, True, True), # OP Disabled, Lat Active (e.g. MADS)
(True, True, True) # Both Active
])
def test_enabled_states(enabled_state, lat_active_state, expected):
"""
Test DriverMonitoring.run_step with all 4 combinations of:
- selfdriveState.enabled (True/False)
- carControl.latActive (True/False)
"""
def _build_sm(selfdrive_enabled, lat_active, steering_pressed, gas_pressed):
cs = car.CarState.new_message()
cs.vEgo = 30.0
cs.gearShifter = car.CarState.GearShifter.drive
cs.standstill = False
cs.steeringPressed = False
cs.gasPressed = False
cs.steeringPressed = steering_pressed
cs.gasPressed = gas_pressed
ss = log.SelfdriveState.new_message()
ss.enabled = enabled_state
ss.enabled = selfdrive_enabled
cc = car.CarControl.new_message()
cc.latActive = lat_active_state
cc.latActive = lat_active
mv2 = log.ModelDataV2.new_message()
mv2.meta.disengagePredictions.brakeDisengageProbs = [0.0]
lc = log.LiveCalibrationData.new_message()
lc.rpyCalib = [0.0, 0.0, 0.0]
ds = make_msg(False)
sm = {
'carState': cs,
'selfdriveState': ss,
'carControl': cc,
'modelV2': mv2,
'liveCalibration': lc,
'driverStateV2': ds
return {
'carState': cs, 'selfdriveState': ss, 'carControl': cc,
'modelV2': mv2, 'liveCalibration': lc, 'driverStateV2': make_msg(False),
}
driver_monitoring = DriverMonitoring()
# run_test doesn't assign enabled to a variable, so we need to spy on _update_events to see its value
captured_args = []
original_update_events = driver_monitoring._update_events
@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
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)
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)
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}"
dm._update_events = spy
dm.run_step(sm, demo=False)
assert captured['op_engaged'] == expected_op_engaged
assert captured['driver_engaged'] == expected_driver_engaged

View File

@@ -299,7 +299,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) {
void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, bool is_onroad) {
static Params params;
static SubMaster sm({"deviceState", "driverCameraState"});
@@ -309,6 +309,8 @@ 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);
@@ -354,6 +356,21 @@ 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);
@@ -387,7 +404,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);
process_peripheral_state(panda, &pm, no_fan_control, is_onroad);
}
// Process panda state at 10 Hz

View File

@@ -230,7 +230,7 @@ class SelfdriveD(CruiseHelper):
if self.CP.notCar:
# wait for everything to init first
if self.sm.frame > int(5. / DT_CTRL) and self.initialized:
if self.sm.frame > int(2. / DT_CTRL) and self.initialized:
# body always wants to enable
self.events.add(EventName.pcmEnable)

View File

@@ -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, list(enumerate(v2[n:], n))
yield 'add', dot, [(i, v2[i]) for i in range(n, n2)]
if n1 > n:
yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)]))

View File

@@ -49,6 +49,8 @@ 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)

View File

@@ -36,7 +36,7 @@ class DeveloperLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._is_release = self._params.get_bool("IsReleaseBranch")
self._is_release = False # self._params.get_bool("IsReleaseBranch")
# Build items and keep references for callbacks/state updates
self._adb_toggle = toggle_item(

View File

@@ -42,7 +42,7 @@ class TogglesLayout(Widget):
def __init__(self):
super().__init__()
self._params = Params()
self._is_release = self._params.get_bool("IsReleaseBranch")
self._is_release = False # self._params.get_bool("IsReleaseBranch")
# param, title, desc, icon, needs_restart
self._toggle_defs = {

View File

@@ -7,13 +7,15 @@ 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
from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
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
@@ -28,6 +30,37 @@ 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__()
@@ -84,6 +117,8 @@ 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
@@ -96,6 +131,8 @@ 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(),
@@ -141,13 +178,23 @@ class MiciHomeLayout(Widget):
self._last_refresh = rl.get_time()
self._update_params()
def set_callbacks(self, on_settings: Callable | None = None):
def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
alert_count_callback: Callable[[], int] | 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:
if self._on_settings_click:
self._on_settings_click()
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()
self._did_long_press = False
def _get_version_text(self) -> tuple[str, str, str, str] | None:
@@ -203,3 +250,8 @@ 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()

View File

@@ -60,7 +60,11 @@ 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))
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._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
@@ -68,6 +72,11 @@ 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:

View File

@@ -178,6 +178,8 @@ 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")

View File

@@ -6,11 +6,13 @@ 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)
@@ -59,8 +61,6 @@ 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,16 +113,7 @@ 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 * (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)),
rl.Color(255, 255, 255, int(255 * self._fade_filter.x)),
)
else:
@@ -174,11 +165,22 @@ 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
pitch, yaw, roll = driver_orient
# 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 = self._pitch_filter.update(pitch)
yaw = self._yaw_filter.update(yaw)
@@ -192,7 +194,6 @@ 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)
@@ -200,13 +201,11 @@ 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, yaw))
rotation = math.degrees(math.atan2(pitch * 2, yaw)) # reduce yaw sensitivity
angle_diff = rotation - self._rotation_filter.x
angle_diff = ((angle_diff + 180) % 360) - 180
self._rotation_filter.update(self._rotation_filter.x + angle_diff)

View File

@@ -21,8 +21,6 @@ from openpilot.system.ui.widgets.scroller_tici import Scroller
SPEED_LIMIT_MODE_BUTTONS = [tr("Off"), tr("Info"), tr("Warning"), tr("Assist")]
SPEED_LIMIT_OFFSET_TYPE_BUTTONS = [tr("None"), tr("Fixed"), tr("%")]
SPEED_LIMIT_UPSHIFT_ACCEPT_BUTTONS = [tr("Never Raise"), tr("Accel Pedal Confirm")]
SPEED_LIMIT_CAP_AUDIO_CUE_BUTTONS = [tr("Off"), tr("On")]
SPEED_LIMIT_MODE_DESCRIPTIONS = [
tr("Off: Disables the Speed Limit functions."),
@@ -37,16 +35,6 @@ SPEED_LIMIT_OFFSET_DESCRIPTIONS = [
tr("Percent: Adds a percent offset [Speed Limit + (Offset % Speed Limit)]"),
]
SPEED_LIMIT_UPSHIFT_ACCEPT_DESCRIPTIONS = [
tr("Never Raise: Keeps the current cap when the speed limit changes."),
tr("Accel Pedal Confirm: Accepts new speed limit cap when you release the accelerator pedal."),
]
SPEED_LIMIT_CAP_AUDIO_CUE_DESCRIPTIONS = [
tr("Off: No audio cue when entering speed limit capping mode."),
tr("On: Plays a low chime when entering speed limit capping mode."),
]
class PanelType(IntEnum):
SETTINGS = 0
@@ -98,42 +86,13 @@ class SpeedLimitSettingsLayout(Widget):
label_callback=self._get_offset_label,
)
self._speed_limit_upshift_accept = multiple_button_item_sp(
title=lambda: tr("Speed Limit Cap Upshift"),
description=self._get_upshift_accept_description,
buttons=SPEED_LIMIT_UPSHIFT_ACCEPT_BUTTONS,
param="SpeedLimitUpshiftAccept",
button_width=500,
)
self._speed_limit_min_cap_floor = option_item_sp(
title=lambda: tr("Speed Limit Cap Floor"),
param="SpeedLimitMinCapFloor",
min_value=0,
max_value=40,
description=self._get_min_cap_floor_description,
label_callback=self._get_min_cap_floor_label,
)
self._speed_limit_cap_audio_cue = multiple_button_item_sp(
title=lambda: tr("Speed Limit Cap Audio Cue"),
description=self._get_cap_audio_cue_description,
buttons=SPEED_LIMIT_CAP_AUDIO_CUE_BUTTONS,
param="SpeedLimitCapAudioCue",
button_width=450,
)
items = [
self._speed_limit_mode,
LineSeparatorSP(40),
self._source_button,
LineSeparatorSP(40),
self._speed_limit_offset_type,
self._speed_limit_value_offset,
LineSeparatorSP(40),
self._speed_limit_upshift_accept,
self._speed_limit_min_cap_floor,
self._speed_limit_cap_audio_cue,
self._speed_limit_value_offset
]
return items
@@ -161,23 +120,6 @@ class SpeedLimitSettingsLayout(Widget):
return f"{value} {unit}"
return str(value)
@staticmethod
def _get_upshift_accept_description():
return get_highlighted_description(ui_state.params, "SpeedLimitUpshiftAccept", SPEED_LIMIT_UPSHIFT_ACCEPT_DESCRIPTIONS)
@staticmethod
def _get_min_cap_floor_description():
return ""
@staticmethod
def _get_min_cap_floor_label(value):
unit = tr("km/h") if ui_state.is_metric else tr("mph")
return f"{value} {unit}"
@staticmethod
def _get_cap_audio_cue_description():
return get_highlighted_description(ui_state.params, "SpeedLimitCapAudioCue", SPEED_LIMIT_CAP_AUDIO_CUE_DESCRIPTIONS)
def _update_state(self):
super()._update_state()
@@ -186,7 +128,6 @@ class SpeedLimitSettingsLayout(Widget):
brand = ui_state.CP.brand
has_long = ui_state.has_longitudinal_control
has_icbm = ui_state.has_icbm
pcm_op_long = has_long and ui_state.CP.pcmCruise
"""
Speed Limit Assist is available when:
@@ -203,7 +144,6 @@ class SpeedLimitSettingsLayout(Widget):
else:
sla_available = False
pcm_op_long = False
if not sla_available:
self._speed_limit_mode.action_item.set_enabled_buttons({
@@ -217,10 +157,6 @@ class SpeedLimitSettingsLayout(Widget):
offset_type = ui_state.params.get("SpeedLimitOffsetType", return_default=True)
self._speed_limit_value_offset.set_visible(offset_type != int(SpeedLimitOffsetType.off))
self._speed_limit_upshift_accept.set_visible(pcm_op_long)
self._speed_limit_min_cap_floor.set_visible(pcm_op_long)
self._speed_limit_cap_audio_cue.set_visible(pcm_op_long)
def _render(self, rect):
if self._current_panel == PanelType.POLICY:
self._policy_layout.render(rect)

View File

@@ -10,6 +10,7 @@ import time
import pyray as rl
from cereal import custom
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.ui_state import device, ui_state
from openpilot.system.ui.lib.multilang import tr
@@ -207,7 +208,7 @@ class ModelsLayout(Widget):
for bundle in bundles:
folders.setdefault(next((ov_ride.value for ov_ride in bundle.overrides if ov_ride.key == "folder"), ""), []).append(bundle)
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': tr("Default Model"), 'short_name': "Default"})])]
folders_list = [TreeFolder("", [TreeNode("Default", {'display_name': f"{DEFAULT_MODEL} (Default)", 'short_name': "Default"})])]
for folder, folder_bundles in sorted(folders.items(), key=lambda x: max((bundle.index for bundle in x[1]), default=-1), reverse=True):
folder_bundles.sort(key=lambda bundle: bundle.index, reverse=True)
name = folder + (f" - (Updated: {m.group(1)})" if folder_bundles and (m := re.search(r'\(([^)]*)\)[^(]*$', folder_bundles[0].displayName)) else "")
@@ -243,7 +244,7 @@ class ModelsLayout(Widget):
self._update_lagd_description(live_delay)
self.model_manager = ui_state.sm["modelManagerSP"]
self._handle_bundle_download_progress()
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else tr("Default Model")
active_name = self.model_manager.activeBundle.internalName if self.model_manager and self.model_manager.activeBundle.ref else f"{DEFAULT_MODEL} (Default)"
self.current_model_item.action_item.set_value(active_name)
if not ui_state.is_offroad():

View File

@@ -120,20 +120,12 @@ class SteeringLayout(Widget):
def _update_state(self):
super()._update_state()
torque_allowed = True
torque_allowed = ui_state.CP is not None and ui_state.CP.steerControlType != car.CarParams.SteerControlType.angle
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())

View File

@@ -8,6 +8,7 @@ from collections.abc import Callable
import pyray as rl
from cereal import custom
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
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
@@ -27,7 +28,8 @@ class CurrentModelInfo(Widget):
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)
default_text = f"{DEFAULT_MODEL} (Default)".lower()
self.current_model_text = UnifiedLabel(default_text, 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)
@@ -98,7 +100,7 @@ class ModelsLayoutMici(NavScroller):
folders = self._get_grouped_bundles(favorites)
folder_buttons = []
default_btn = BigButton(tr("default model"))
default_btn = BigButton(f"{DEFAULT_MODEL} (Default)".lower())
default_btn.set_click_callback(self._select_default)
folder_buttons.append(default_btn)
@@ -168,7 +170,8 @@ class ModelsLayoutMici(NavScroller):
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"))
model_text = manager.activeBundle.displayName.lower() if manager.activeBundle.index > 0 else f"{DEFAULT_MODEL} (Default)".lower()
self.current_model_info.current_model_text.set_text(model_text)
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")

View File

@@ -5,8 +5,9 @@ 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 BigButton, BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.button import BigCircleButton
from openpilot.selfdrive.ui.mici.widgets.dialog import BigConfirmationDialog, BigDialog
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.sunnylink import SunnylinkLayoutMici
from openpilot.selfdrive.ui.sunnypilot.mici.layouts.models import ModelsLayoutMici
@@ -32,11 +33,11 @@ class SettingsLayoutSP(OP.SettingsLayout):
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 = BigButton("sunnylink", "", gui_app.texture("icons_mici/settings/developer/ssh.png", ICON_SIZE, ICON_SIZE))
sunnylink_btn = SettingsBigButton(tr("sunnylink"), "", gui_app.texture("icons_mici/settings/developer/ssh.png", 55, 55))
sunnylink_btn.set_click_callback(lambda: gui_app.push_widget(sunnylink_panel))
models_panel = ModelsLayoutMici(back_callback=gui_app.pop_widget)
models_btn = BigButton("models", "", gui_app.texture("../../sunnypilot/selfdrive/assets/offroad/icon_models.png", ICON_SIZE, ICON_SIZE))
models_btn = SettingsBigButton(tr("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)

View File

@@ -4,6 +4,8 @@ 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
@@ -13,11 +15,43 @@ 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
from openpilot.system.ui.lib.application import gui_app, MousePos, FontWeight
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):
@@ -27,6 +61,8 @@ 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)
@@ -40,6 +76,7 @@ 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,
@@ -59,6 +96,10 @@ 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())
@@ -75,6 +116,11 @@ 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):
@@ -194,9 +240,14 @@ 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 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."), "")
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"))
elif self.sponsor_pairing:
dlg = SunnylinkPairingDialog(sponsor_pairing=True)
elif not self.sponsor_pairing:

View File

@@ -141,7 +141,8 @@ class DeveloperUiRenderer(Widget):
# Add torque-specific elements if using torque control
if sm['controlsState'].lateralControlState.which() == 'torqueState':
if sm.valid['liveTorqueParameters']:
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:
elements.extend([
self.friction_elem.update(sm, ui_state.is_metric),
self.lat_accel_factor_elem.update(sm, ui_state.is_metric),

View File

@@ -8,8 +8,7 @@ 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
@@ -248,12 +247,12 @@ class FrictionCoefficientElement:
self.unit = ""
def update(self, sm, is_metric: bool) -> UiElement:
ltp = sm['liveTorqueParameters']
friction_coef = ltp.frictionCoefficientFiltered
live_valid = ltp.liveValid
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)
value = f"{friction_coef:.3f}"
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
ltp = sm['liveTorqueParameters']
value = f"{ltp.frictionCoefficientFiltered:.3f}"
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
return UiElement(value, "FRIC.", self.unit, color)
@@ -262,12 +261,12 @@ class LatAccelFactorElement:
self.unit = ""
def update(self, sm, is_metric: bool) -> UiElement:
ltp = sm['liveTorqueParameters']
lat_accel_factor = ltp.latAccelFactorFiltered
live_valid = ltp.liveValid
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)
value = f"{lat_accel_factor:.3f}"
color = rl.Color(0, 255, 0, 255) if live_valid else rl.WHITE
ltp = sm['liveTorqueParameters']
value = f"{ltp.latAccelFactorFiltered:.3f}"
color = rl.Color(0, 255, 0, 255) if ltp.liveValid else rl.WHITE
return UiElement(value, "L.A.F.", self.unit, color)

View File

@@ -6,7 +6,6 @@ See the LICENSE.md file in the root directory for more details.
"""
import pyray as rl
from cereal import custom
from openpilot.common.constants import CV
from openpilot.selfdrive.ui.mici.onroad.torque_bar import TorqueBar
from openpilot.selfdrive.ui.sunnypilot.onroad.developer_ui import DeveloperUiRenderer, DeveloperUiState, get_bottom_dev_ui_offset
@@ -24,7 +23,6 @@ from openpilot.system.ui.lib.multilang import tr
from openpilot.system.ui.lib.text_measure import measure_text_cached
SLA_ACTIVE_COLOR = rl.Color(0x91, 0x9b, 0x95, 0xff)
AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
class HudRendererSP(HudRenderer):
@@ -91,14 +89,9 @@ class HudRendererSP(HudRenderer):
set_speed_color = COLORS.DARK_GREY
if self.is_cruise_set:
set_speed_color = COLORS.WHITE
assist_state = long_plan_sp.speedLimit.assist.state
# Green for active/adapting/capping states, grey for tempPaused when override, else normal
if assist_state in (AssistState.active, AssistState.adapting, AssistState.capping):
if long_plan_sp.speedLimit.assist.active:
set_speed_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0, 0xff, 0, 0xff)
max_color = SLA_ACTIVE_COLOR if long_override else rl.Color(0x80, 0xd8, 0xa6, 0xff)
elif assist_state == AssistState.tempPaused and long_override:
set_speed_color = SLA_ACTIVE_COLOR
max_color = SLA_ACTIVE_COLOR
else:
if ui_state.status == UIStatus.ENGAGED:
max_color = COLORS.ENGAGED

View File

@@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details.
from dataclasses import dataclass
from enum import StrEnum
import time
import pyray as rl
from cereal import custom
@@ -29,7 +28,6 @@ SET_SPEED_NA = 255
KM_TO_MILE = 0.621371
AssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
AssistDisableReason = custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
@@ -42,7 +40,6 @@ class Colors:
DARK_GREY = rl.Color(77, 77, 77, 255)
SUB_BG = rl.Color(0, 0, 0, 180)
MUTCD_LINES = rl.Color(255, 255, 255, 100)
AMBER = rl.Color(255, 176, 0, 255)
class IconSide(StrEnum):
@@ -113,11 +110,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
self.speed_limit_ahead_valid = False
self.speed_limit_ahead_frame = 0
self.cap_delta = 0.0
self.target_cap = 0.0
self.disable_reason = AssistDisableReason.none
self.disable_reason_timestamp = 0.0
self.is_cruise_set: bool = False
self.is_cruise_available: bool = True
self.set_speed: float = SET_SPEED_NA
@@ -153,10 +145,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
self.speed_limit_final_last = resolver.speedLimitFinalLast * self.speed_conv
self.speed_limit_source = resolver.source
self.speed_limit_assist_state = assist.state
self.cap_delta = assist.capDelta
self.target_cap = assist.targetCap * self.speed_conv
self.disable_reason = assist.disableReason
self.disable_reason_timestamp = time.monotonic()
if sm.updated["liveMapDataSP"]:
lmd = sm["liveMapDataSP"]
@@ -206,15 +194,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
self._draw_sign_main(sign_rect, alpha)
if self.speed_limit_assist_state == AssistState.preActive:
self._draw_pre_active_arrow(sign_rect)
elif self.speed_limit_assist_state == AssistState.tempPaused:
self._draw_temp_paused_icon(sign_rect)
elif self.speed_limit_assist_state == AssistState.capping:
self._draw_cap_badge(sign_rect)
# Also draw ahead info if valid and different from cap (mutual exclusion fix)
if self.speed_limit_ahead_valid and round(self.speed_limit_ahead) != round(self.target_cap):
ahead_info_y = sign_rect.y + sign_rect.height + 10 + 160 + 10
ahead_rect = rl.Rectangle(sign_rect.x, ahead_info_y, sign_rect.width, 100)
self._draw_ahead_info(ahead_rect)
else:
self._draw_ahead_info(sign_rect)
@@ -250,38 +229,6 @@ class SpeedLimitRenderer(Widget, SpeedLimitAlertRenderer):
color = rl.Color(255, 255, 255, int(icon_alpha))
rl.draw_texture_ex(txt_icon, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, color)
def _draw_temp_paused_icon(self, sign_rect):
"""Draw greyed preActive icon when tempPaused."""
# Reuse preActive icon with grey alpha
icon_alpha = 128 # 50% opacity for paused state
txt_icon = self.arrow_blank # Use blank/greyed version
sign_margin = 12
arrow_spacing = int(sign_margin * 1.4)
arrow_x = sign_rect.x + sign_rect.width + arrow_spacing
arrow_y = sign_rect.y + (sign_rect.height - txt_icon.height) / 2
color = rl.Color(145, 155, 149, icon_alpha) # GREY color with alpha
rl.draw_texture_ex(txt_icon, rl.Vector2(arrow_x, arrow_y), 0.0, 1.0, color)
def _draw_cap_badge(self, sign_rect):
"""Draw CAP info panel below speed limit sign during capping."""
rect = rl.Rectangle(sign_rect.x + (sign_rect.width - 170) / 2, sign_rect.y + sign_rect.height + 10, 170, 160)
rl.draw_rectangle_rounded(rect, 0.35, 10, Colors.SUB_BG)
rl.draw_rectangle_rounded_lines_ex(rect, 0.35, 10, 3, Colors.MUTCD_LINES)
mid_x = rect.x + rect.width / 2
label_color = Colors.AMBER if self.cap_delta > 0.5 else Colors.GREY
self._draw_text_centered(self.font_demi, "CAP", 40, rl.Vector2(mid_x, rect.y + 28), label_color)
cap_speed = round(self.target_cap)
self._draw_text_centered(self.font_bold, str(cap_speed), 70, rl.Vector2(mid_x, rect.y + 82), Colors.WHITE)
if self.cap_delta > 0.5:
delta_display = round(self.cap_delta * self.speed_conv)
delta_unit = 'km/h' if ui_state.is_metric else 'mph'
delta_text = f'-{delta_display} {delta_unit}'
self._draw_text_centered(self.font_norm, delta_text, 36, rl.Vector2(mid_x, rect.y + 134), Colors.GREY)
def _render_vienna(self, rect, val, sub, color, has_limit, alpha=1.0):
center = rl.Vector2(rect.x + rect.width / 2, rect.y + rect.height / 2)
radius = (rect.width + 18) / 2

View File

@@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details.
"""
from enum import Enum
from cereal import messaging, log, custom
from cereal import messaging, log, car, 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
@@ -26,22 +26,20 @@ class OnroadTimerStatus(Enum):
class UIStateSP:
def __init__(self):
self.CP_SP: custom.CarParamsSP | None = None
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.sm_services_ext = [
"modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP",
"gpsLocation", "liveTorqueParameters", "carStateSP", "liveMapDataSP", "carParamsSP", "liveDelay"
]
self.sunnylink_state = SunnylinkState()
self.update_params()
self.onroad_brightness_timer: int = 0
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")
self.custom_interactive_timeout: int = 0
self._sp_initialized: bool = False
def update(self) -> None:
if self.sunnylink_enabled:
@@ -128,6 +126,8 @@ 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,11 +143,63 @@ 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")
class DeviceSP:
@staticmethod
@@ -163,7 +215,6 @@ 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

View File

@@ -74,7 +74,7 @@ class UIState(UIStateSP):
# Core state variables
self.is_metric: bool = self.params.get_bool("IsMetric")
self.is_release = self.params.get_bool("IsReleaseBranch")
self.is_release = False # self.params.get_bool("IsReleaseBranch")
self.always_on_dm: bool = self.params.get_bool("AlwaysOnDM")
self.started: bool = False
self.ignition: bool = False

View File

@@ -147,6 +147,7 @@ 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)

View File

@@ -1,5 +1,6 @@
import os
import glob
from tinygrad import Device
Import('env', 'arch')
lenv = env.Clone()
@@ -21,10 +22,19 @@ if PC:
if outputs:
lenv.Command(outputs, inputs, cmd)
tg_flags = {
'larch64': 'DEV=QCOM FLOAT16=1 NOLOCALS=1 JIT_BATCH_SIZE=0',
'Darwin': f'DEV=CPU THREADS=0 HOME={os.path.expanduser("~")}',
}.get(arch, 'DEV=CPU CPU_LLVM=1 THREADS=0')
available = set(Device.get_available_devices())
if '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:LLVM'
# THREADS=0 is need to prevent bug: https://github.com/tinygrad/tinygrad/issues/14689
tg_flags = f'DEV={tg_backend} THREADS=0'
mac_brew_string = f'HOME={os.path.expanduser("~")}' if arch == 'Darwin' else ''
image_flag = {
'larch64': 'IMAGE=2',
@@ -38,7 +48,7 @@ def tg_compile(flags, model_name):
return lenv.Command(
out,
[fn + ".onnx"] + tinygrad_files,
f'{pythonpath_string} {flags} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 {Dir("#tinygrad_repo").abspath}/examples/openpilot/compile3.py {fn}.onnx {out}'
)
# Compile models
@@ -46,9 +56,9 @@ for model_name in ['supercombo', 'driving_vision', 'driving_off_policy', 'drivin
if File(f"models/{model_name}.onnx").exists():
tg_compile(tg_flags, model_name)
script_files = [File("warp.py"), File(Dir("#selfdrive/modeld").File("compile_warp.py").abspath)]
script_files = [File("warp.py")]
pythonpath_string = 'PYTHONPATH="${PYTHONPATH}:' + env.Dir("#tinygrad_repo").abspath + ':' + env.Dir("#").abspath + '"'
compile_warp_cmd = f'{pythonpath_string} {tg_flags} python3 -m sunnypilot.modeld_v2.warp'
compile_warp_cmd = f'{pythonpath_string} {tg_flags} {mac_brew_string} {image_flag} python3 -m sunnypilot.modeld_v2.warp'
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
warp_targets = []

View File

@@ -129,8 +129,7 @@ class ModelState(ModelStateBase):
self.numpy_inputs[key][:] = inputs[key]
imgs_tensors = self.warp.process(bufs, transforms)
for name, tensor in imgs_tensors.items():
self.model_runner.inputs[name] = tensor
self.model_runner.update_vision_inputs(imgs_tensors)
self.model_runner.prepare_inputs(self.numpy_inputs)
if prepare_only:

View File

@@ -2,8 +2,11 @@ import os
os.environ['DEV'] = 'CPU'
import pytest
import numpy as np
from openpilot.selfdrive.modeld.compile_warp import get_nv12_info, CAMERA_CONFIGS
from openpilot.sunnypilot.modeld_v2.warp import Warp, MODEL_W, MODEL_H
from openpilot.sunnypilot.modeld_v2.warp import CAMERA_CONFIGS
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.sunnypilot.modeld_v2.warp import Warp
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
VISION_NAME_PAIRS = [ # needed to account for supercombos input_imgs
('img', 'big_img'),

View File

@@ -6,74 +6,164 @@ from tinygrad.tensor import Tensor
from tinygrad.engine.jit import TinyJit
from tinygrad.device import Device
from typing import NamedTuple
# https://github.com/tinygrad/tinygrad/issues/15682
from tinygrad.uop.ops import UOp, Ops
_orig = UOp.__reduce__
UOp.__reduce__ = lambda self: (UOp.unique, ()) if self.op is Ops.UNIQUE else _orig(self)
from tinygrad.helpers import Context
from openpilot.system.camerad.cameras.nv12_info import get_nv12_info
from openpilot.selfdrive.modeld.compile_warp import (
CAMERA_CONFIGS, MEDMODEL_INPUT_SIZE, make_frame_prepare, make_update_both_imgs,
warp_pkl_path,
)
from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye
class NV12Frame(NamedTuple):
cam_w: int
cam_h: int
stride: int
y_height: int
uv_height: int
size: int
UV_SCALE_MATRIX = np.array([[0.5, 0, 0], [0, 0.5, 0], [0, 0, 1]], dtype=np.float32)
UV_SCALE_MATRIX_INV = np.linalg.inv(UV_SCALE_MATRIX)
CAMERA_CONFIGS = [
(_ar_ox_fisheye.width, _ar_ox_fisheye.height), # tici: 1928x1208
(_os_fisheye.width, _os_fisheye.height), # mici: 1344x760
]
from openpilot.common.transformations.model import MEDMODEL_INPUT_SIZE
MODELS_DIR = Path(__file__).parent / 'models'
MODEL_W, MODEL_H = MEDMODEL_INPUT_SIZE
UPSTREAM_BUFFER_LENGTH = 5
def warp_pkl_path(cam_w, cam_h):
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_tinygrad.pkl'
def warp_perspective_tinygrad(src_flat, M_inv, dst_shape, src_shape, stride_pad):
w_dst, h_dst = dst_shape
h_src, w_src = src_shape
x = Tensor.arange(w_dst).reshape(1, w_dst).expand(h_dst, w_dst).reshape(-1)
y = Tensor.arange(h_dst).reshape(h_dst, 1).expand(h_dst, w_dst).reshape(-1)
# inline 3x3 matmul as elementwise to avoid reduce op (enables fusion with gather)
src_x = M_inv[0, 0] * x + M_inv[0, 1] * y + M_inv[0, 2]
src_y = M_inv[1, 0] * x + M_inv[1, 1] * y + M_inv[1, 2]
src_w = M_inv[2, 0] * x + M_inv[2, 1] * y + M_inv[2, 2]
src_x = src_x / src_w
src_y = src_y / src_w
x_nn_clipped = Tensor.round(src_x).clip(0, w_src - 1).cast('int')
y_nn_clipped = Tensor.round(src_y).clip(0, h_src - 1).cast('int')
idx = y_nn_clipped * (w_src + stride_pad) + x_nn_clipped
return src_flat[idx]
def frames_to_tensor(frames, model_w, model_h):
H = (frames.shape[0] * 2) // 3
W = frames.shape[1]
in_img1 = Tensor.cat(frames[0:H:2, 0::2],
frames[1:H:2, 0::2],
frames[0:H:2, 1::2],
frames[1:H:2, 1::2],
frames[H:H+H//4].reshape((H//2, W//2)),
frames[H+H//4:H+H//2].reshape((H//2, W//2)), dim=0).reshape((6, H//2, W//2))
return in_img1
def make_frame_prepare(cam_w, cam_h, model_w, model_h):
stride, y_height, uv_height, _ = get_nv12_info(cam_w, cam_h)
uv_offset = stride * y_height
stride_pad = stride - cam_w
def frame_prepare_tinygrad(input_frame, M_inv):
# UV_SCALE @ M_inv @ UV_SCALE_INV simplifies to elementwise scaling
M_inv_uv = M_inv * Tensor([[1.0, 1.0, 0.5], [1.0, 1.0, 0.5], [2.0, 2.0, 1.0]])
# deinterleave NV12 UV plane (UVUV... -> separate U, V)
uv = input_frame[uv_offset:uv_offset + uv_height * stride].reshape(uv_height, stride)
with Context(SPLIT_REDUCEOP=0):
y = warp_perspective_tinygrad(input_frame[:cam_h*stride],
M_inv, (model_w, model_h),
(cam_h, cam_w), stride_pad).realize()
u = warp_perspective_tinygrad(uv[:cam_h//2, :cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
v = warp_perspective_tinygrad(uv[:cam_h//2, 1:cam_w:2].flatten(),
M_inv_uv, (model_w//2, model_h//2),
(cam_h//2, cam_w//2), 0).realize()
yuv = y.cat(u).cat(v).reshape((model_h * 3 // 2, model_w))
tensor = frames_to_tensor(yuv, model_w, model_h)
return tensor
return frame_prepare_tinygrad
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, Tensor.cat(tensor[:6], tensor[-6:], dim=0).contiguous().reshape(1, 12, model_h//2, model_w//2)
return update_img_input_tinygrad
def make_update_both_imgs(frame_prepare, model_w, model_h):
update_img = make_update_img_input(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):
r1, r2 = update_img(calib_img_buffer, new_img, M_inv)
w1, w2 = update_img(calib_big_img_buffer, new_big_img, M_inv_big)
return r1, r2, w1, w2
return update_both_imgs_tinygrad
def v2_warp_pkl_path(cam_w, cam_h, buffer_length):
return MODELS_DIR / f'warp_{cam_w}x{cam_h}_b{buffer_length}_tinygrad.pkl'
def compile_v2_warp(cam_w, cam_h, buffer_length):
def compile_v2_warp(cam_w, cam_h, buffer_length, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1], pkl_path=None):
_, _, _, yuv_size = get_nv12_info(cam_w, cam_h)
img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
print(f"Compiling v2 warp for {cam_w}x{cam_h} buffer_length={buffer_length}...")
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
frame_prepare = make_frame_prepare(cam_w, cam_h, model_w, model_h)
update_both_imgs = make_update_both_imgs(frame_prepare, model_w, model_h)
update_img_jit = TinyJit(update_both_imgs, prune=True)
full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
big_full_buffer = Tensor.zeros(img_buffer_shape, dtype='uint8').contiguous().realize()
full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8)
big_full_buffer_np = np.zeros(img_buffer_shape, dtype=np.uint8)
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)
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()
out = update_img_jit(*inputs)
full_buffer = out[0].contiguous().realize().clone()
big_full_buffer = out[2].contiguous().realize().clone()
update_img_jit(*inputs)
mt = time.perf_counter()
Device.default.synchronize()
et = time.perf_counter()
print(f" [{i+1}/10] enqueue {(mt-st)*1e3:6.2f} ms -- total {(et-st)*1e3:6.2f} ms")
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
if pkl_path is None:
pkl_path = v2_warp_pkl_path(cam_w, cam_h, buffer_length)
with open(pkl_path, "wb") as f:
pickle.dump(update_img_jit, f)
print(f" Saved to {pkl_path}")
jit = pickle.load(open(pkl_path, "rb"))
jit(*inputs)
class Warp:
def __init__(self, buffer_length=2):
def __init__(self, buffer_length=2, model_w=MEDMODEL_INPUT_SIZE[0], model_h=MEDMODEL_INPUT_SIZE[1]):
self.buffer_length = buffer_length
self.img_buffer_shape = (buffer_length * 6, MODEL_H // 2, MODEL_W // 2)
self.model_w = model_w
self.model_h = model_h
self.img_buffer_shape = (buffer_length * 6, model_h // 2, model_w // 2)
self.jit_cache = {}
self.full_buffers = {k: Tensor.zeros(self.img_buffer_shape, dtype='uint8').contiguous().realize() for k in ['img', 'big_img']}
@@ -101,8 +191,8 @@ class Warp:
with open(upstream_pkl, 'rb') as f:
self.jit_cache[key] = pickle.load(f)
if key not in self.jit_cache:
frame_prepare = make_frame_prepare(cam_w, cam_h, MODEL_W, MODEL_H)
update_both_imgs = make_update_both_imgs(frame_prepare, MODEL_W, MODEL_H)
frame_prepare = make_frame_prepare(cam_w, cam_h, self.model_w, self.model_h)
update_both_imgs = make_update_both_imgs(frame_prepare, self.model_w, self.model_h)
self.jit_cache[key] = TinyJit(update_both_imgs, prune=True)
if key not in self._nv12_cache:
@@ -116,7 +206,7 @@ class Warp:
if wide_ptr not in self._blob_cache:
self._blob_cache[wide_ptr] = Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
road_blob = self._blob_cache[road_ptr]
wide_blob = self._blob_cache[wide_ptr] if wide_ptr != road_ptr else Tensor.from_blob(wide_ptr, (yuv_size,), dtype='uint8')
wide_blob = self._blob_cache[wide_ptr]
np.copyto(self.transforms_np['img'], transforms[road].reshape(3, 3))
np.copyto(self.transforms_np['big_img'], transforms[wide].reshape(3, 3))
@@ -125,13 +215,11 @@ class Warp:
self.full_buffers['img'], road_blob, self.transforms['img'],
self.full_buffers['big_img'], wide_blob, self.transforms['big_img'],
)
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}
return {road: res[1].realize(), wide: res[3].realize()}
if __name__ == "__main__":
for cam_w, cam_h in CAMERA_CONFIGS:
compile_v2_warp(cam_w, cam_h, 5, pkl_path=warp_pkl_path(cam_w, cam_h))
for bl in [2, 5]:
compile_v2_warp(cam_w, cam_h, bl)

View File

@@ -4,8 +4,9 @@ import hashlib
from openpilot.common.basedir import BASEDIR
from openpilot.sunnypilot import get_file_hash
from openpilot.sunnypilot.models.model_name import DEFAULT_MODEL
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "common", "model.h")
DEFAULT_MODEL_NAME_PATH = os.path.join(BASEDIR, "sunnypilot", "models", "model_name.py")
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")
@@ -25,8 +26,7 @@ def update_model_hash():
def get_current_default_model_name():
print("[GET DEFAULT MODEL NAME]")
with open(DEFAULT_MODEL_NAME_PATH) as f:
name = f.read().split('"')[1]
name = DEFAULT_MODEL
print(f'Current default model name: "{name}"')
return name
@@ -35,7 +35,7 @@ def get_current_default_model_name():
def update_default_model_name(name: str):
print("[CHANGE DEFAULT MODEL NAME]")
with open(DEFAULT_MODEL_NAME_PATH, "w") as f:
f.write(f'#define DEFAULT_MODEL "{name}"\n')
f.write(f'DEFAULT_MODEL = "{name}"\n')
print(f'New default model name: "{name}"')
print("[DONE]")
@@ -51,7 +51,7 @@ if __name__ == "__main__":
exit(0)
current_name = get_current_default_model_name()
new_name = f"{args.new_name} (Default)"
new_name = args.new_name
if current_name == new_name:
print(f'Proposed default model name: "{new_name}"')
confirm = input("Proposed default model name is the same as the current default model name. Confirm? (y/n): ").upper().strip()

View File

@@ -116,7 +116,7 @@ class ModelCache:
class ModelFetcher:
"""Handles fetching and caching of model data from remote source"""
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v16.json"
MODEL_URL = "https://raw.githubusercontent.com/sunnypilot/sunnypilot-models/refs/heads/gh-pages/docs/driving_models_v18.json"
def __init__(self, params: Params):
self.params = params

View File

@@ -0,0 +1 @@
DEFAULT_MODEL = "POP model"

View File

@@ -132,6 +132,11 @@ class ModelRunner(ModularRunner):
return list(self._model_data.input_shapes.keys())
raise ValueError("Model data is not available. Ensure the model is loaded correctly.")
def update_vision_inputs(self, vision_inputs: dict) -> None:
"""Updates the vision inputs in the runner."""
for name, tensor in vision_inputs.items():
self.inputs[name] = tensor
@abstractmethod
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""

View File

@@ -46,14 +46,13 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
assert "/dev/kgsl-3d0" not in str(e), "Model was built on C3 or C3X, but is being loaded on PC"
raise
# Map input names to their required dtype and device from the loaded model
self.input_to_dtype = {}
self.input_to_device = {}
for idx, name in enumerate(self.model_run.captured.expected_names):
info = self.model_run.captured.expected_input_info[idx]
self.input_to_dtype[name] = info[2] # dtype
self.input_to_device[name] = info[3] # device
self._policy_cached = False
self.input_to_dtype[name] = info[2]
self.input_to_device[name] = info[3]
self.inputs[name] = Tensor.zeros(*self.input_shapes[name], dtype=info[2], device=info[3]).realize()
@property
def vision_input_names(self) -> list[str]:
@@ -62,22 +61,23 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny
def prepare_policy_inputs(self, numpy_inputs: NumpyDict):
if not self._policy_cached:
for key, value in numpy_inputs.items():
self.inputs[key] = Tensor(value, device='NPY').realize()
self._policy_cached = True
for key, value in numpy_inputs.items():
if key in self.inputs:
self.inputs[key].assign(Tensor(value, device=self.inputs[key].device))
def prepare_inputs(self, numpy_inputs: NumpyDict) -> dict:
"""Prepares all vision and policy inputs for the model."""
self.prepare_policy_inputs(numpy_inputs)
for key in self.vision_input_names:
if key in self.inputs:
self.inputs[key] = self.inputs[key].cast(self.input_to_dtype[key])
return self.inputs
def update_vision_inputs(self, vision_inputs: dict[str, Tensor]):
for name, tensor in vision_inputs.items():
if name in self.inputs:
self.inputs[name].assign(tensor)
def _run_model(self) -> NumpyDict:
"""Runs the Tinygrad model inference and parses the outputs."""
outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy().flatten()
outputs = self.model_run(**self.inputs).numpy().flatten()
return self._parse_outputs(outputs)
def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict:

View File

@@ -38,6 +38,8 @@ 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

View File

@@ -60,7 +60,7 @@ class LongitudinalPlannerSP:
# Speed Limit Assist
has_speed_limit = self.resolver.speed_limit_valid or self.resolver.speed_limit_last_valid
self.sla.update(long_enabled, long_override, v_ego, a_ego, v_cruise_cluster, self.resolver.speed_limit,
self.resolver.speed_limit_final_last, has_speed_limit, self.resolver.distance, self.events_sp, CS.gasPressed)
self.resolver.speed_limit_final_last, has_speed_limit, self.resolver.distance, self.events_sp)
targets = {
LongitudinalPlanSource.cruise: (v_cruise, a_ego),
@@ -132,9 +132,6 @@ class LongitudinalPlannerSP:
assist.active = self.sla.is_active
assist.vTarget = float(self.sla.output_v_target)
assist.aTarget = float(self.sla.output_a_target)
assist.capDelta = float(self.sla.cap_delta)
assist.targetCap = float(self.sla._target_cap)
assist.disableReason = self.sla.disable_reason
# E2E Alerts
e2eAlerts = longitudinalPlanSP.e2eAlerts

View File

@@ -17,8 +17,3 @@ CONFIRM_SPEED_THRESHOLD = {
True: 80, # km/h
False: 50, # mph
}
MIN_CAP_FLOOR_MAX = {
True: 64, # km/h
False: 40, # mph
}

View File

@@ -27,8 +27,3 @@ class Mode(IntEnumBase):
information = 1
warning = 2
assist = 3
class UpshiftAccept(IntEnumBase):
NEVER_RAISE = 0
ACCEL_PEDAL = 1

View File

@@ -9,7 +9,6 @@ from cereal import custom, car
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode as SpeedLimitMode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import MIN_CAP_FLOOR_MAX
def compare_cluster_target(v_cruise_cluster: float, target_set_speed: float, is_metric: bool) -> tuple[bool, bool]:
@@ -43,9 +42,3 @@ def set_speed_limit_assist_availability(CP: car.CarParams, CP_SP: custom.CarPara
params.put("SpeedLimitMode", int(SpeedLimitMode.warning))
return allowed
def get_min_cap_floor(params: Params, is_metric: bool) -> float:
value = params.get("SpeedLimitMinCapFloor", return_default=True)
value = max(0, min(value, MIN_CAP_FLOOR_MAX[is_metric]))
return value * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)

View File

@@ -15,20 +15,16 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit import PCM_LONG_REQUIRED_MAX_SET_SPEED, CONFIRM_SPEED_THRESHOLD
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode, UpshiftAccept
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability, \
get_min_cap_floor
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import compare_cluster_target, set_speed_limit_assist_availability
ButtonType = car.CarState.ButtonEvent.Type
EventNameSP = custom.OnroadEventSP.EventName
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
AssistDisableReason = custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason
SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimit.Source
ACTIVE_STATES = (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting)
ENABLED_STATES = (SpeedLimitAssistState.preActive, SpeedLimitAssistState.pending, *ACTIVE_STATES)
CAP_ACTIVE_STATES = (SpeedLimitAssistState.capping,)
CAP_ENABLED_STATES = CAP_ACTIVE_STATES # cap mode has no partially-engaged state
DISABLED_GUARD_PERIOD = 0.5 # secs.
# secs. Time to wait after activation before considering temp deactivation signal.
@@ -37,10 +33,6 @@ PRE_ACTIVE_GUARD_PERIOD = {
False: 5,
}
SPEED_LIMIT_CHANGED_HOLD_PERIOD = 1 # secs. Time to wait after speed limit change before switching to preActive.
CAP_RAISE_HOLD_PERIOD = 0.2 # secs. Time to confirm limit raise before upshifting.
CAP_SUSPEND_GUARD_PERIOD = 1.0 # secs. Time to hold cap disabled after long_override release.
USER_PAUSE_TIMEOUT_TICKS = 6000 # 5 min / DT_MDL (0.05 s) = 6000 ticks
RESUME_CLUSTER_DELTA_THRESHOLD = 1 # integer display-unit delta (kph or mph)
LIMIT_MIN_ACC = -1.5 # m/s^2 Maximum deceleration allowed for limit controllers to provide.
LIMIT_MAX_ACC = 1.0 # m/s^2 Maximum acceleration allowed for limit controllers to provide while active.
@@ -59,7 +51,6 @@ class SpeedLimitAssist:
v_ego: float
a_ego: float
v_offset: float
cap_delta: float
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP):
self.params = Params()
@@ -73,12 +64,10 @@ class SpeedLimitAssist:
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
self.long_enabled = False
self.long_enabled_prev = False
self.long_override = False
self.is_enabled = False
self.is_active = False
self.output_v_target = V_CRUISE_UNSET
self.output_a_target = 0.
self.cap_delta = 0.0
self.v_ego = 0.
self.a_ego = 0.
self.v_offset = 0.
@@ -103,28 +92,8 @@ class SpeedLimitAssist:
self._minus_hold = 0.
self._last_carstate_ts = 0.
self._cap_change_timer = 0
self._cap_suspended_timer = 0
self._cap_below_floor = False
self._target_cap = 0.0
self._cap_upshift_pressed = False
self._cap_upshift_release_timer = 0
self._cap_audio_cue_fired = False
self._cap_raise_accepted = False
self._accel_pressed = False
self._was_cap_suspended = False
self._override_active_last = False
self._min_cap_floor = get_min_cap_floor(self.params, self.is_metric)
self._cap_upshift_accept = self.params.get("SpeedLimitUpshiftAccept", return_default=True)
self._cap_audio_cue_enabled = bool(self.params.get("SpeedLimitCapAudioCue", return_default=True))
self._user_paused: bool = False
self._user_paused_timer: int = 0
self._disable_reason = AssistDisableReason.none
self._speed_limit_final_last_at_pause = 0.
self.tempPaused_count = 0 # diagnostic counter for tests
# TODO-SP: SLA's own output_a_target for planner
# Solution functions mapped to respective states
self.acceleration_solutions = {
SpeedLimitAssistState.disabled: self.get_current_acceleration_as_target,
SpeedLimitAssistState.inactive: self.get_current_acceleration_as_target,
@@ -132,26 +101,8 @@ class SpeedLimitAssist:
SpeedLimitAssistState.pending: self.get_current_acceleration_as_target,
SpeedLimitAssistState.adapting: self.get_adapting_state_target_acceleration,
SpeedLimitAssistState.active: self.get_active_state_target_acceleration,
SpeedLimitAssistState.capping: self.get_current_acceleration_as_target,
SpeedLimitAssistState.tempPaused: self.get_current_acceleration_as_target,
}
@property
def disable_reason(self):
return self._disable_reason
@property
def _gates_pass(self) -> bool:
return self.long_enabled and self.enabled
@property
def _cap_gates_pass(self) -> bool:
return self._gates_pass and not self.long_override and self._cap_suspended_timer <= 0
@property
def _cap_entry_ready(self) -> bool:
return self._has_speed_limit and not self._cap_below_floor
@property
def speed_limit_changed(self) -> bool:
return self._has_speed_limit and bool(self._speed_limit != self.speed_limit_prev)
@@ -175,13 +126,13 @@ class SpeedLimitAssist:
events_sp.add(EventNameSP.speedLimitActive)
def get_v_target_from_control(self) -> float:
if self.pcm_op_long:
if self.state == SpeedLimitAssistState.capping:
return min(self.v_cruise_cluster, self._target_cap)
else:
if self._has_speed_limit and self.is_active:
if self._has_speed_limit:
if self.pcm_op_long and self.is_enabled:
return self._speed_limit_final_last
if not self.pcm_op_long and self.is_active:
return self._speed_limit_final_last
# Fallback
return V_CRUISE_UNSET
# TODO-SP: SLA's own output_a_target for planner
@@ -193,9 +144,6 @@ class SpeedLimitAssist:
self.is_metric = self.params.get_bool("IsMetric")
set_speed_limit_assist_availability(self.CP, self.CP_SP, self.params)
self.enabled = self.params.get("SpeedLimitMode", return_default=True) == Mode.assist
self._min_cap_floor = get_min_cap_floor(self.params, self.is_metric)
self._cap_upshift_accept = self.params.get("SpeedLimitUpshiftAccept", return_default=True)
self._cap_audio_cue_enabled = bool(self.params.get("SpeedLimitCapAudioCue", return_default=True))
def update_car_state(self, CS: car.CarState) -> None:
now = time.monotonic()
@@ -239,12 +187,7 @@ class SpeedLimitAssist:
cst_high
pcm_long_required_max_set_speed_conv = round(pcm_long_required_max * speed_conv)
if self.pcm_op_long and self.state not in CAP_ACTIVE_STATES:
self.target_set_speed_conv = pcm_long_required_max_set_speed_conv
elif not self.pcm_op_long:
self.target_set_speed_conv = self.speed_limit_final_last_conv
else:
self.target_set_speed_conv = self.v_cruise_cluster_conv
self.target_set_speed_conv = pcm_long_required_max_set_speed_conv if self.pcm_op_long else self.speed_limit_final_last_conv
@property
def apply_confirm_speed_threshold(self) -> bool:
@@ -289,142 +232,74 @@ class SpeedLimitAssist:
return self._get_button_release(req_plus, req_minus)
def _cap_limit_change_held(self) -> bool:
"""Return True when limit-change hold period has elapsed."""
return self._cap_change_timer >= int(SPEED_LIMIT_CHANGED_HOLD_PERIOD / DT_MDL)
def update_state_machine_pcm_op_long(self):
self.long_engaged_timer = max(0, self.long_engaged_timer - 1)
self.pre_active_timer = max(0, self.pre_active_timer - 1)
def _cap_upshift_release_edge(self) -> bool:
"""Return True when limit-raise hold period elapsed after gas release edge."""
if self._cap_upshift_pressed and not self._accel_pressed:
self._cap_upshift_release_timer = int(CAP_RAISE_HOLD_PERIOD / DT_MDL)
self._cap_upshift_pressed = self._accel_pressed
if self._cap_upshift_release_timer > 0:
self._cap_upshift_release_timer = max(0, self._cap_upshift_release_timer - 1)
if self._cap_upshift_release_timer <= 0:
return True
return False
def _go_disabled(self, reason: 'AssistDisableReason') -> None:
"""Transition to disabled state with given reason."""
self._cap_raise_accepted = False
self.state = SpeedLimitAssistState.disabled
self._disable_reason = reason
def _should_exit_temp_pause(self) -> bool:
"""Check if conditions warrant exiting temp pause state."""
limit_changed = self._speed_limit_final_last != self._speed_limit_final_last_at_pause
timer_expired = self._user_paused_timer <= 0
cluster_realigned = abs(self.v_cruise_cluster_conv - self.speed_limit_final_last_conv) <= RESUME_CLUSTER_DELTA_THRESHOLD
return limit_changed or timer_expired or cluster_realigned
def update_state_machine_cap(self, events_sp: EventsSP) -> tuple[bool, bool]:
"""Cap mode FSM for pcm_op_long cars. Returns (enabled, active)."""
# Bookkeeping: timers, override flags (unchanged)
self._cap_change_timer = min(self._cap_change_timer + 1,
int((SPEED_LIMIT_CHANGED_HOLD_PERIOD + 1) / DT_MDL))
self._cap_upshift_release_timer = max(0, self._cap_upshift_release_timer - 1)
self._user_paused_timer = max(0, self._user_paused_timer - 1)
if self._override_active_last and not self.long_override and self._was_cap_suspended:
self._cap_suspended_timer = int(CAP_SUSPEND_GUARD_PERIOD / DT_MDL)
elif not self.long_override:
self._cap_suspended_timer = max(0, self._cap_suspended_timer - 1)
self._override_active_last = self.long_override
self._cap_below_floor = self._has_speed_limit and self._speed_limit_final_last < self._min_cap_floor
# Gate checks FIRST: apply to all non-disabled states (including tempPaused)
# ACTIVE, ADAPTING, PENDING, PRE_ACTIVE, INACTIVE
if self.state != SpeedLimitAssistState.disabled:
if not self._gates_pass:
self._go_disabled(AssistDisableReason.gateDisabled)
self._was_cap_suspended = False
self._cap_suspended_timer = 0
elif self.long_override:
self._go_disabled(AssistDisableReason.longOverride)
self._was_cap_suspended = True
if not self.long_enabled or not self.enabled:
self.state = SpeedLimitAssistState.disabled
# Sub-state dispatch (only if gates passed)
elif self.state == SpeedLimitAssistState.tempPaused:
# Exit conditions: speed limit changed, timer expired, or cluster delta returns
if self._should_exit_temp_pause():
self._user_paused = False
self._user_paused_timer = 0
self._go_disabled(AssistDisableReason.autoResume)
else:
# ACTIVE
if self.state == SpeedLimitAssistState.active:
if self.v_cruise_cluster_changed:
self.state = SpeedLimitAssistState.inactive
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
elif self._has_speed_limit and self.v_offset < LIMIT_SPEED_OFFSET_TH:
self.state = SpeedLimitAssistState.adapting
elif self.state == SpeedLimitAssistState.capping:
# Cluster delta entry: user nudged cruise control away from limit
if self.v_cruise_cluster_changed:
self._user_paused = True
self._user_paused_timer = USER_PAUSE_TIMEOUT_TICKS
self._speed_limit_final_last_at_pause = self._speed_limit_final_last
self.state = SpeedLimitAssistState.tempPaused
self._disable_reason = AssistDisableReason.userTempPause
elif not self._has_speed_limit:
self._cap_raise_accepted = False
self.state = SpeedLimitAssistState.pending
self._disable_reason = AssistDisableReason.mapGap
elif self._cap_below_floor:
self._cap_raise_accepted = False
self.state = SpeedLimitAssistState.pending
self._disable_reason = AssistDisableReason.belowFloor
elif self._speed_limit_final_last != self._target_cap and self._cap_limit_change_held():
old_cap = self._target_cap
self._target_cap = self._speed_limit_final_last
self._cap_change_timer = 0
self._cap_raise_accepted = False
if self._target_cap > old_cap:
if self._cap_upshift_accept == UpshiftAccept.NEVER_RAISE:
self._target_cap = old_cap
elif self._cap_upshift_accept == UpshiftAccept.ACCEL_PEDAL:
if not self._accel_pressed:
self._cap_raise_accepted = True
else:
self._target_cap = old_cap
else:
self._cap_upshift_release_edge()
# ADAPTING
elif self.state == SpeedLimitAssistState.adapting:
if self.v_cruise_cluster_changed:
self.state = SpeedLimitAssistState.inactive
elif self.speed_limit_changed and self.apply_confirm_speed_threshold:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
elif self.v_offset >= LIMIT_SPEED_OFFSET_TH:
self.state = SpeedLimitAssistState.active
# PENDING
elif self.state == SpeedLimitAssistState.pending:
if self.target_set_speed_confirmed:
self._update_confirmed_state()
elif self.speed_limit_changed:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
# PRE_ACTIVE
elif self.state == SpeedLimitAssistState.preActive:
if self.target_set_speed_confirmed:
self._update_confirmed_state()
elif self.pre_active_timer <= 0:
# Timeout - session ended
self.state = SpeedLimitAssistState.inactive
# INACTIVE
elif self.state == SpeedLimitAssistState.inactive:
pass
# DISABLED
elif self.state == SpeedLimitAssistState.disabled:
if self.long_enabled and self.enabled:
# start or reset preActive timer if initially enabled or manual set speed change detected
if not self.long_enabled_prev or self.v_cruise_cluster_changed:
self.long_engaged_timer = int(DISABLED_GUARD_PERIOD / DT_MDL)
elif self.long_engaged_timer <= 0:
if self.target_set_speed_confirmed:
self._update_confirmed_state()
elif self._has_speed_limit:
self.state = SpeedLimitAssistState.preActive
self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.pcm_op_long] / DT_MDL)
else:
self._cap_upshift_release_edge()
else:
self._cap_upshift_release_edge()
self.state = SpeedLimitAssistState.pending
elif self.state == SpeedLimitAssistState.pending:
if self._cap_entry_ready:
if not self._was_cap_suspended:
self._target_cap = self._speed_limit_final_last
self._cap_change_timer = 0
self._cap_audio_cue_fired = False
self._cap_raise_accepted = False
self._disable_reason = AssistDisableReason.none
self.state = SpeedLimitAssistState.capping
else:
# Disabled-entry logic: if gates pass + cap_suspended_timer clear, enter capping/pending
if self._cap_gates_pass:
if self._cap_entry_ready:
if not self._was_cap_suspended:
self._target_cap = self._speed_limit_final_last
self._cap_change_timer = 0
self._cap_audio_cue_fired = False
self._disable_reason = AssistDisableReason.none
self.state = SpeedLimitAssistState.capping
else:
self._disable_reason = AssistDisableReason.mapGap if not self._has_speed_limit else AssistDisableReason.belowFloor
self.state = SpeedLimitAssistState.pending
# Audio cue on capping entry
if self.state == SpeedLimitAssistState.capping and self._state_prev != SpeedLimitAssistState.capping:
# suppress audio cue on override-release re-entry
if self._cap_audio_cue_enabled and not self._was_cap_suspended:
events_sp.add(EventNameSP.speedLimitCapActive)
self._cap_audio_cue_fired = True
self._was_cap_suspended = False
enabled = self.state in CAP_ENABLED_STATES
active = self.state in CAP_ACTIVE_STATES
enabled = self.state in ENABLED_STATES
active = self.state in ACTIVE_STATES
return enabled, active
@@ -485,13 +360,13 @@ class SpeedLimitAssist:
return enabled, active
def update_events(self, events_sp: EventsSP) -> None:
if not self.pcm_op_long and self.state == SpeedLimitAssistState.preActive:
if self.state == SpeedLimitAssistState.preActive:
events_sp.add(EventNameSP.speedLimitPreActive)
if self.state == SpeedLimitAssistState.pending and self._state_prev != SpeedLimitAssistState.pending:
events_sp.add(EventNameSP.speedLimitPending)
if not self.pcm_op_long and self.is_active:
if self.is_active:
if self._state_prev not in ACTIVE_STATES:
self.update_active_event(events_sp)
@@ -504,9 +379,8 @@ class SpeedLimitAssist:
self.update_active_event(events_sp)
def update(self, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float, v_cruise_cluster: float, speed_limit: float,
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP, accel_pressed: bool = False) -> None:
speed_limit_final_last: float, has_speed_limit: bool, distance: float, events_sp: EventsSP) -> None:
self.long_enabled = long_enabled
self.long_override = long_override
self.v_ego = v_ego
self.a_ego = a_ego
@@ -514,14 +388,13 @@ class SpeedLimitAssist:
self._speed_limit = speed_limit
self._speed_limit_final_last = speed_limit_final_last
self._distance = distance
self._accel_pressed = accel_pressed
self.update_params()
self.update_calculations(v_cruise_cluster)
self._state_prev = self.state
if self.pcm_op_long:
self.is_enabled, self.is_active = self.update_state_machine_cap(events_sp)
self.is_enabled, self.is_active = self.update_state_machine_pcm_op_long()
else:
self.is_enabled, self.is_active = self.update_state_machine_non_pcm_long()
@@ -538,9 +411,4 @@ class SpeedLimitAssist:
self.output_v_target = self.get_v_target_from_control()
self.output_a_target = self.get_a_target_from_control()
if self.pcm_op_long and self.state == SpeedLimitAssistState.capping:
self.cap_delta = max(0.0, self.v_cruise_cluster - self._target_cap)
else:
self.cap_delta = 0.0
self.frame += 1

View File

@@ -1,136 +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 pytest
from cereal import custom, car
from opendbc.car.car_helpers import interfaces
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.constants import CV
from openpilot.common.params import Params
from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import Mode
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers import get_min_cap_floor
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
class CarParamsFactory:
@staticmethod
def create_car_interface(car_name: str = TOYOTA.TOYOTA_RAV4_TSS2) -> tuple[car.CarParams, custom.CarParamsSP, object]:
params = Params()
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP)
CI.CP.openpilotLongitudinalControl = True
sunnypilot_interfaces.setup_interfaces(CI, params)
return CI.CP, CI.CP_SP, CI
class SpeedLimitAssistScenario:
def __init__(self, CP: car.CarParams, CP_SP: custom.CarParamsSP, params: Params = None):
if params is None:
params = Params()
self.params = params
self.CP = CP
self.CP_SP = CP_SP
self.params.put("SpeedLimitMode", int(Mode.assist))
self.sla = SpeedLimitAssist(CP, CP_SP)
self.sla.update_params()
self.events_sp = EventsSP()
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
def set_state(self, state: int) -> "SpeedLimitAssistScenario":
self.sla.state = state
return self
def set_speed_limits(self, speed_limit: float, distance: float = 0, speed_limit_final_last: float = 0,
speed_limit_prev: float = 0) -> "SpeedLimitAssistScenario":
self.sla._speed_limit = speed_limit
self.sla._distance = distance
self.sla.speed_limit_prev = speed_limit_prev
return self
def set_cruise_speeds(self, v_cruise_cluster: float, v_cruise_cluster_prev: float = None) -> "SpeedLimitAssistScenario":
self.sla.v_cruise_cluster = v_cruise_cluster
if v_cruise_cluster_prev is None:
v_cruise_cluster_prev = v_cruise_cluster
self.sla.v_cruise_cluster_prev = v_cruise_cluster_prev
self.sla.prev_v_cruise_cluster_conv = round(v_cruise_cluster_prev * self.speed_conv)
return self
def set_engaged(self, op_engaged: bool) -> "SpeedLimitAssistScenario":
self.sla.op_engaged = op_engaged
return self
def set_param(self, key: str, value) -> "SpeedLimitAssistScenario":
# IntEnum instances carry a .value the Params API does not accept directly
if hasattr(value, 'value'):
value = value.value
if isinstance(value, bool):
self.params.put_bool(key, value)
elif isinstance(value, int):
self.params.put(key, value)
else:
self.params.put(key, str(value) if not isinstance(value, str) else value)
# Runtime caches these behind PARAMS_UPDATE_PERIOD; force-sync for tests
if key == "SpeedLimitMinCapFloor":
self.sla._min_cap_floor = get_min_cap_floor(self.sla.params, self.sla.is_metric)
elif key == "SpeedLimitUpshiftAccept":
self.sla._cap_upshift_accept = self.sla.params.get("SpeedLimitUpshiftAccept", return_default=True)
elif key == "SpeedLimitCapAudioCue":
self.sla._cap_audio_cue_enabled = bool(self.sla.params.get("SpeedLimitCapAudioCue", return_default=True))
elif key == "SpeedLimitMode":
self.sla.enabled = self.sla.params.get("SpeedLimitMode", return_default=True) == Mode.assist
elif key == "IsMetric":
self.sla.is_metric = self.sla.params.get_bool("IsMetric")
return self
def clear_events(self) -> "SpeedLimitAssistScenario":
self.events_sp.clear()
return self
def reset_state(self) -> "SpeedLimitAssistScenario":
self.sla.state = SpeedLimitAssistState.disabled
self.sla.frame = -1
self.sla.long_enabled = False
self.sla.long_enabled_prev = False
self.sla._speed_limit = 0.0
self.sla.speed_limit_prev = 0.0
self.sla._speed_limit_final_last = 0.0
self.sla._distance = 0.0
self.sla.long_engaged_timer = 0
self.sla.pre_active_timer = 0
self.events_sp.clear()
return self
def build(self) -> "SpeedLimitAssistScenario":
return self
@pytest.fixture
def params():
p = Params()
yield p
@pytest.fixture
def car_params_factory():
return CarParamsFactory()
@pytest.fixture
def scenario_builder(params):
def builder(car_name: str = TOYOTA.TOYOTA_RAV4_TSS2) -> SpeedLimitAssistScenario:
CP, CP_SP, _ = CarParamsFactory.create_car_interface(car_name)
return SpeedLimitAssistScenario(CP, CP_SP, params)
return builder

View File

@@ -1,362 +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 pytest
from cereal import car, custom
from openpilot.common.realtime import DT_MDL
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
SpeedLimitAssistState
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
def make_mock_car_params(mocker, pcm_op_long: bool = True) -> car.CarParams:
"""Create a minimal CarParams mock with pcm_op_long setting."""
cp = mocker.MagicMock(spec=car.CarParams)
cp.openpilotLongitudinalControl = pcm_op_long
cp.pcmCruise = pcm_op_long
cp.brand = "generic"
return cp
def make_mock_car_params_sp(mocker) -> custom.CarParamsSP:
"""Create a minimal CarParamsSP mock."""
cp_sp = mocker.MagicMock(spec=custom.CarParamsSP)
return cp_sp
def get_event_name_sp():
"""Get EventNameSP enum."""
return custom.OnroadEventSP.EventName
def make_sla_factory(mocker, pcm_op_long: bool = True) -> SpeedLimitAssist:
"""Factory: create a SpeedLimitAssist instance with mocked params."""
cp = make_mock_car_params(mocker, pcm_op_long)
cp_sp = make_mock_car_params_sp(mocker)
mock_params = mocker.MagicMock()
mock_params.get_bool.return_value = True
mock_params.get.return_value = True
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist.Params",
return_value=mock_params)
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers.set_speed_limit_assist_availability")
mocker.patch("openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.helpers.get_min_cap_floor", return_value=5.0)
sla = SpeedLimitAssist(cp, cp_sp)
return sla
class TestBug1OverrideNoOscillation:
"""Override falling edge arms suspend timer, preventing oscillation."""
def test_sustained_override_stays_disabled(self, mocker):
"""5s sustained override: state never enters capping."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.capping
sla._state_prev = SpeedLimitAssistState.capping
sla._target_cap = 25.0
events_sp = EventsSP()
num_ticks = int(5.0 / DT_MDL)
for _ in range(num_ticks):
sla.long_override = True
sla.update_state_machine_cap(events_sp)
assert (sla.state != SpeedLimitAssistState.capping), "State entered capping during sustained override"
assert sla.state == SpeedLimitAssistState.disabled
def test_override_release_enters_capping_after_guard(self, mocker):
"""Release override: state enters capping after guard period."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.disabled
sla._state_prev = SpeedLimitAssistState.disabled
sla._target_cap = 25.0
sla._was_cap_suspended = True
sla._override_active_last = True
sla.long_override = True
events_sp = EventsSP()
sla.update_state_machine_cap(events_sp)
sla.long_override = False
guard_ticks = int(1.0 / DT_MDL)
for i in range(guard_ticks + 1):
sla.update_state_machine_cap(events_sp)
if i < guard_ticks:
assert sla.state == SpeedLimitAssistState.disabled
assert sla.state == SpeedLimitAssistState.capping
class TestBug2TargetCapPreserved:
"""Target cap preserved across override-release cycles."""
def test_target_cap_preserved_on_override_suspension(self, mocker):
"""Override pulse: _target_cap preserved, not reset to new limit."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.capping
sla._state_prev = SpeedLimitAssistState.capping
sla._target_cap = 25.0
sla._cap_change_timer = 0
events_sp = EventsSP()
sla._speed_limit_final_last = 30.0
sla.long_override = True
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.disabled
assert sla._was_cap_suspended is True
sla.long_override = False
guard_ticks = int(1.0 / DT_MDL)
for _ in range(guard_ticks + 1):
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.capping
assert sla._target_cap == 25.0, f"Expected 25.0, got {sla._target_cap}"
class TestBug3AccelPressedWired:
"""Accel_pressed parameter wired to sla.update()."""
def test_accel_pressed_parameter_received(self, mocker):
"""sla.update() receives accel_pressed=True."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla.v_ego = 20.0
sla.a_ego = 0.0
sla.v_cruise_cluster = 30.0
events_sp = EventsSP()
sla.update(
long_enabled=True,
long_override=False,
v_ego=20.0,
a_ego=0.0,
v_cruise_cluster=30.0,
speed_limit=30.0,
speed_limit_final_last=30.0,
has_speed_limit=True,
distance=100.0,
events_sp=events_sp,
accel_pressed=True,
)
assert sla._accel_pressed is True
def test_accel_pressed_false_by_default(self, mocker):
"""sla.update() with accel_pressed=False (default)."""
sla = make_sla_factory(mocker, pcm_op_long=True)
events_sp = EventsSP()
sla.update(
long_enabled=True,
long_override=False,
v_ego=20.0,
a_ego=0.0,
v_cruise_cluster=30.0,
speed_limit=30.0,
speed_limit_final_last=30.0,
has_speed_limit=True,
distance=100.0,
events_sp=events_sp,
)
assert sla._accel_pressed is False
class TestBug4NoAudioCueOnOverrideReentry:
"""Audio cue suppressed on override-release re-entry."""
def test_cue_fires_on_cold_entry(self, mocker):
"""Fresh engagement: audio cue fires exactly once."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.disabled
sla._state_prev = SpeedLimitAssistState.disabled
sla._was_cap_suspended = False
sla._cap_audio_cue_enabled = True
events_sp = EventsSP()
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.capping
assert sla._cap_audio_cue_fired is True
event_name_sp = get_event_name_sp()
assert event_name_sp.speedLimitCapActive in events_sp.events, "Audio cue event not fired"
def test_no_cue_on_override_reentry(self, mocker):
"""Override-release re-entry: audio cue NOT fired."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.disabled
sla._state_prev = SpeedLimitAssistState.capping
sla._target_cap = 25.0
sla._was_cap_suspended = True
sla._override_active_last = False
sla._cap_audio_cue_enabled = True
sla._cap_audio_cue_fired = True
events_sp = EventsSP()
sla._cap_suspended_timer = int(1.0 / DT_MDL)
guard_ticks = int(1.0 / DT_MDL) + 1
for _ in range(guard_ticks):
sla.update_state_machine_cap(events_sp)
sla._state_prev = sla.state
assert sla.state == SpeedLimitAssistState.capping
event_name_sp = get_event_name_sp()
assert (event_name_sp.speedLimitCapActive not in events_sp.events), "Audio cue should not fire on override re-entry"
class TestEdgeACases:
"""Edge A: No spurious timer on non-capping override."""
def test_edge_a_no_spurious_timer_on_disabled_override(self, mocker):
"""Override during disabled state should not arm timer."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = False
sla._was_cap_suspended = False
sla.state = SpeedLimitAssistState.disabled
sla._state_prev = SpeedLimitAssistState.disabled
events_sp = EventsSP()
sla.long_override = True
sla.update_state_machine_cap(events_sp)
sla.long_override = False
sla.update_state_machine_cap(events_sp)
assert sla._cap_suspended_timer == 0
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.capping
class TestEdgeBCases:
"""Edge B: _target_cap preserved via pending."""
def test_edge_b_target_cap_preserved_via_pending(self, mocker):
"""Target cap preserved when transiting pending->capping after suspension."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla.state = SpeedLimitAssistState.capping
sla._state_prev = SpeedLimitAssistState.capping
sla._target_cap = 25.0
sla._was_cap_suspended = True
events_sp = EventsSP()
sla._has_speed_limit = False
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.pending
sla._has_speed_limit = True
sla._speed_limit_final_last = 30.0
sla.update_state_machine_cap(events_sp)
assert sla.state == SpeedLimitAssistState.capping
assert sla._target_cap == 25.0
class TestEdgeCCases:
"""Edge C: Timer cleared on disengage."""
def test_edge_c_timer_cleared_on_disengage(self, mocker):
"""Timer cleared when long_enabled=False."""
sla = make_sla_factory(mocker, pcm_op_long=True)
sla.long_enabled = True
sla.enabled = True
sla._has_speed_limit = True
sla._speed_limit_final_last = 25.0
sla._min_cap_floor = 5.0
sla._cap_suspended_timer = 50
sla.state = SpeedLimitAssistState.capping
sla._state_prev = SpeedLimitAssistState.capping
events_sp = EventsSP()
sla.long_enabled = False
sla.update_state_machine_cap(events_sp)
assert sla._cap_suspended_timer == 0
assert sla.state == SpeedLimitAssistState.disabled
class TestTargetCapPublished:
"""Target cap published in cereal message."""
def test_target_cap_written_to_assist(self, mocker):
"""_write_assist_fields writes sla._target_cap to assist.targetCap."""
from openpilot.sunnypilot.selfdrive.controls.lib.longitudinal_planner import LongitudinalPlannerSP
sla = make_sla_factory(mocker, pcm_op_long=True)
sla._target_cap = 22.35
sla.output_v_target = 10.0
sla.output_a_target = 0.0
sla.cap_delta = 0.0
sla.is_enabled = True
sla.is_active = False
sla.state = SpeedLimitAssistState.disabled
planner = mocker.MagicMock()
planner.sla = sla
msg = custom.LongitudinalPlanSP.new_message()
assist = msg.speedLimit.assist
LongitudinalPlannerSP._write_assist_fields(planner, assist)
assert assist.targetCap == pytest.approx(22.35)
assert assist.enabled is True
assert assist.active is False
assert assist.vTarget == pytest.approx(10.0)
assert assist.aTarget == pytest.approx(0.0)
assert assist.capDelta == pytest.approx(0.0)

View File

@@ -7,7 +7,7 @@ See the LICENSE.md file in the root directory for more details.
import pytest
from cereal import custom, car
from cereal import custom
from opendbc.car.car_helpers import interfaces
from opendbc.car.rivian.values import CAR as RIVIAN
from opendbc.car.tesla.values import CAR as TESLA
@@ -25,7 +25,6 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist
from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
ButtonType = car.CarState.ButtonEvent.Type
ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values())
@@ -72,7 +71,6 @@ class TestSpeedLimitAssist:
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP)
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
CI.CP.pcmCruise = False # test non-PCM FSM path (preActive, pending, adapting, active)
sunnypilot_interfaces.setup_interfaces(CI, self.params)
return CI
@@ -85,17 +83,13 @@ class TestSpeedLimitAssist:
def reset_state(self):
self.sla.state = SpeedLimitAssistState.disabled
self.sla._state_prev = SpeedLimitAssistState.disabled
self.sla.frame = -1
self.sla.long_enabled = False
self.sla.long_enabled_prev = False
self.sla.long_engaged_timer = 0
self.sla.pre_active_timer = 0
self.sla.last_op_engaged_frame = 0
self.sla.op_engaged = False
self.sla.op_engaged_prev = False
self.sla._speed_limit = 0.
self.sla._speed_limit_final_last = 0.
self.sla.speed_limit_prev = 0.
self.sla.v_cruise_cluster = 0.
self.sla.v_cruise_cluster_prev = 0.
self.sla.last_valid_speed_limit_offsetted = 0.
self.sla._distance = 0.
self.events_sp.clear()
@@ -139,6 +133,20 @@ class TestSpeedLimitAssist:
assert self.sla.state == SpeedLimitAssistState.preActive
assert self.sla.is_enabled and not self.sla.is_active
def test_transition_disabled_to_pending_no_speed_limit_not_max_initial_set_speed(self):
for _ in range(int(3. / DT_MDL)):
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, SPEED_LIMITS['city'], 0, 0, False, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.pending
assert self.sla.is_enabled and not self.sla.is_active
def test_preactive_to_active_with_max_speed_confirmation(self):
self.sla.state = SpeedLimitAssistState.preActive
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
SPEED_LIMITS['highway'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.active
assert self.sla.is_enabled and self.sla.is_active
assert self.sla.output_v_target == SPEED_LIMITS['highway']
def test_preactive_timeout_to_inactive(self):
self.sla.state = SpeedLimitAssistState.preActive
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
@@ -147,6 +155,47 @@ class TestSpeedLimitAssist:
self.sla.update(True, False, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.inactive
def test_preactive_to_pending_no_speed_limit(self):
self.sla.state = SpeedLimitAssistState.preActive
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, self.pcm_long_max_set_speed, 0, 0, False, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.pending
assert self.sla.is_enabled and not self.sla.is_active
def test_pending_to_active_when_speed_limit_available(self):
self.sla.state = SpeedLimitAssistState.pending
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
self.sla.update(True, False, SPEED_LIMITS['highway'], 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['highway'], SPEED_LIMITS['highway'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.active
def test_pending_to_adapting_when_below_speed_limit(self):
self.sla.state = SpeedLimitAssistState.pending
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
self.sla.update(True, False, SPEED_LIMITS['highway'] + 5, 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['highway'], SPEED_LIMITS['highway'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.adapting
assert self.sla.is_enabled and self.sla.is_active
def test_active_to_adapting_transition(self):
self.initialize_active_state(self.pcm_long_max_set_speed)
self.sla.update(True, False, SPEED_LIMITS['highway'] + 2, 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
SPEED_LIMITS['highway'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.adapting
def test_adapting_to_active_transition(self):
self.sla.state = SpeedLimitAssistState.adapting
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed, SPEED_LIMITS['highway'],
SPEED_LIMITS['highway'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.active
def test_manual_cruise_change_detection(self):
self.sla.state = SpeedLimitAssistState.active
expected_cruise = SPEED_LIMITS['highway']
@@ -154,7 +203,6 @@ class TestSpeedLimitAssist:
different_cruise = SPEED_LIMITS['highway'] + 5
self.sla.update(True, False, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
# In non-pcm mode, manual cruise change transitions to inactive (not tempPaused)
assert self.sla.state == SpeedLimitAssistState.inactive
# TODO-SP: test lower CST cases
@@ -228,236 +276,3 @@ class TestSpeedLimitAssist:
assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active]
elif initial_state in ACTIVE_STATES:
assert self.sla.state in ACTIVE_STATES
def test_non_pcm_regression_method_exists(self):
"""Regression: update_state_machine_non_pcm_long() method exists unchanged."""
assert hasattr(self.sla, "update_state_machine_non_pcm_long")
# Verify it is callable
assert callable(self.sla.update_state_machine_non_pcm_long)
# Verify method is not affected by cap mode refactor (signature check)
import inspect
inspect.signature(self.sla.update_state_machine_non_pcm_long)
# Non-PCM method should have expected parameters (varies by impl, just verify it's present)
class TestSpeedLimitAssistTempPaused:
"""Tests for tempPaused state functionality (cap mode)."""
def setup_method(self, method):
self.params = Params()
self.reset_custom_params()
self.events_sp = EventsSP()
CI = self._setup_platform(DEFAULT_CAR)
self.sla = SpeedLimitAssist(CI.CP, CI.CP_SP)
self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD[self.sla.pcm_op_long] / DT_MDL)
self.pcm_long_max_set_speed = PCM_LONG_REQUIRED_MAX_SET_SPEED[self.sla.is_metric][1] # use 80 MPH for now
self.speed_conv = CV.MS_TO_KPH if self.sla.is_metric else CV.MS_TO_MPH
# For temp paused tests, use pcm_op_long = True
self.sla.pcm_op_long = True
self.sla.enabled = True
def teardown_method(self, method):
self.reset_state()
def _setup_platform(self, car_name):
CarInterface = interfaces[car_name]
CP = CarInterface.get_non_essential_params(car_name)
CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name)
CI = CarInterface(CP, CP_SP)
CI.CP.openpilotLongitudinalControl = True # always assume it's openpilot longitudinal
CI.CP.pcmCruise = False # test non-PCM FSM path (preActive, pending, adapting, active)
sunnypilot_interfaces.setup_interfaces(CI, self.params)
return CI
def reset_custom_params(self):
self.params.put("IsReleaseSpBranch", True)
self.params.put("SpeedLimitMode", int(Mode.assist))
self.params.put_bool("IsMetric", False)
self.params.put("SpeedLimitOffsetType", 0)
self.params.put("SpeedLimitValueOffset", 0)
def reset_state(self):
self.sla.state = SpeedLimitAssistState.disabled
self.sla._state_prev = SpeedLimitAssistState.disabled
self.sla.frame = -1
self.sla.long_enabled = False
self.sla.long_enabled_prev = False
self.sla.long_engaged_timer = 0
self.sla.pre_active_timer = 0
self.sla._speed_limit = 0.
self.sla._speed_limit_final_last = 0.
self.sla.speed_limit_prev = 0.
self.sla.v_cruise_cluster = 0.
self.sla.v_cruise_cluster_prev = 0.
self.sla._distance = 0.
self.events_sp.clear()
def test_temp_paused_entry_capping_state(self):
"""Test that tempPaused entry occurs during state machine when user paused."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla._user_paused = True
self.sla._user_paused_timer = 1000
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
def test_temp_paused_exit_on_speed_limit_change(self):
"""Test exiting tempPaused when speed limit changes."""
self.sla.state = SpeedLimitAssistState.tempPaused
self.sla._user_paused = True
self.sla._user_paused_timer = 1000
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla.long_enabled = True
self.sla.enabled = True
# Change speed limit
new_limit = SPEED_LIMITS['highway']
self.sla.update(True, False, new_limit, 0, self.pcm_long_max_set_speed,
new_limit, new_limit, True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.disabled
assert self.sla._user_paused == False
def test_temp_paused_exit_on_timer_expiry(self):
"""Test exiting tempPaused when 5-minute timer expires."""
self.sla.state = SpeedLimitAssistState.tempPaused
self.sla._user_paused = True
self.sla._user_paused_timer = 1
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
# Trigger update with timer expiry
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.disabled
assert self.sla._user_paused_timer <= 0
def test_disable_reason_user_cancel(self):
"""Test disable_reason set to userCancel on pause."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._user_paused = True
self.sla._user_paused_timer = 1000
self.sla.update(True, False, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
def test_disable_reason_long_override(self):
"""Test disable_reason set to longOverride."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._target_cap = SPEED_LIMITS['city']
self.sla.update(True, True, SPEED_LIMITS['city'], 0, self.pcm_long_max_set_speed,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.disabled
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.longOverride
def test_disable_reason_below_floor(self):
"""Test disable_reason set to belowFloor."""
min_floor = self.sla._min_cap_floor
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._speed_limit_final_last = min_floor - 1
self.sla.long_enabled = True
self.sla.enabled = True
self.sla.v_cruise_cluster = self.pcm_long_max_set_speed
self.sla.v_cruise_cluster_prev = self.pcm_long_max_set_speed
self.sla.prev_v_cruise_cluster_conv = round(self.pcm_long_max_set_speed * self.speed_conv)
self.sla.update(True, False, min_floor - 1, 0, self.pcm_long_max_set_speed,
min_floor - 1, min_floor - 1, True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.pending
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.belowFloor
def test_temp_paused_entry_cap_cluster_nudge_plus(self):
"""Test tempPaused entry in capping state when cluster nudged above limit."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla._target_cap = SPEED_LIMITS['city']
self.sla.long_enabled = True
self.sla.enabled = True
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
# Nudge cluster up (simulate user pressing accel)
nudged_cruise = SPEED_LIMITS['city'] + 1.0
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
assert self.sla._user_paused == True
assert self.sla._user_paused_timer > 0
assert self.sla._disable_reason == custom.LongitudinalPlanSP.SpeedLimit.AssistDisableReason.userTempPause
def test_temp_paused_entry_cap_cluster_nudge_minus(self):
"""Test tempPaused entry in capping state when cluster nudged below limit."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla._target_cap = SPEED_LIMITS['city']
self.sla.long_enabled = True
self.sla.enabled = True
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
# Nudge cluster down (simulate user pressing brake/decel)
nudged_cruise = SPEED_LIMITS['city'] - 1.0
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
assert self.sla._user_paused == True
assert self.sla._user_paused_timer > 0
def test_temp_paused_exit_cluster_returns_to_limit(self):
"""Test exiting tempPaused when cluster returns within ±1 of limit."""
self.sla.state = SpeedLimitAssistState.tempPaused
self.sla._user_paused = True
self.sla._user_paused_timer = 1000
self.sla._speed_limit_final_last_at_pause = SPEED_LIMITS['city']
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla.long_enabled = True
self.sla.enabled = True
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city'] + 5
self.sla.prev_v_cruise_cluster_conv = round((SPEED_LIMITS['city'] + 5) * self.speed_conv)
# Return cluster to within ±1 of limit
returned_cruise = SPEED_LIMITS['city'] + 0.5
self.sla.update(True, False, SPEED_LIMITS['city'], 0, returned_cruise,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.disabled
assert self.sla._user_paused == False
def test_temp_paused_sticky_double_nudge(self):
"""Test that multiple nudges keep state in tempPaused."""
self.sla.state = SpeedLimitAssistState.capping
self.sla._has_speed_limit = True
self.sla._speed_limit_final_last = SPEED_LIMITS['city']
self.sla._target_cap = SPEED_LIMITS['city']
self.sla.long_enabled = True
self.sla.enabled = True
self.sla.v_cruise_cluster = SPEED_LIMITS['city']
self.sla.v_cruise_cluster_prev = SPEED_LIMITS['city']
self.sla.prev_v_cruise_cluster_conv = round(SPEED_LIMITS['city'] * self.speed_conv)
# First nudge
nudged_cruise_1 = SPEED_LIMITS['city'] + 2.0
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise_1,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
assert self.sla._user_paused_timer > 0
saved_timer_1 = self.sla._user_paused_timer
# Second nudge while in tempPaused (shouldn't trigger another entry)
nudged_cruise_2 = SPEED_LIMITS['city'] + 3.0
self.sla.update(True, False, SPEED_LIMITS['city'], 0, nudged_cruise_2,
SPEED_LIMITS['city'], SPEED_LIMITS['city'], True, 0, self.events_sp)
assert self.sla.state == SpeedLimitAssistState.tempPaused
# Timer should have been decremented by one update call
assert self.sla._user_paused_timer == saved_timer_1 - 1

View File

@@ -1,585 +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.
Test suite for Speed Limit Assist cap mode (pcm_op_long only).
Covers 20 edge cases for FSM, debounce, upshift, pedal release, and audio cue.
"""
import pytest
from cereal import custom
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.common.constants import CV
from openpilot.common.realtime import DT_MDL
from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.common import UpshiftAccept
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.speed_limit_assist import SpeedLimitAssist, \
CAP_ACTIVE_STATES
SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimit.AssistState
class TestSpeedLimitCapMode:
def test_cap_mode_applies_to_pcm_op_long(self, scenario_builder):
"""Cap mode applies to pcm_op_long cars (pcmCruise=True + openpilotLongitudinalControl=True)."""
scenario = scenario_builder(TOYOTA.TOYOTA_RAV4_TSS2)
scenario.set_state(SpeedLimitAssistState.disabled)
assert scenario.sla.pcm_op_long is True
assert scenario.sla.state == SpeedLimitAssistState.disabled
def test_disabled_to_capping_transition(self, scenario_builder):
"""FSM: disabled -> capping when speed limit available and engaged."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.disabled)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Update to enter capping
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
assert scenario.sla.is_active
def test_capping_to_disabled_on_disengagement(self, scenario_builder):
"""FSM: capping -> disabled when user disengages (manual override)."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Simulate manual override (long_override=True)
scenario.sla.update(
True, True, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
assert scenario.sla.state == SpeedLimitAssistState.disabled
assert scenario.sla.output_v_target == V_CRUISE_UNSET
def test_below_floor_pause_transition(self, scenario_builder):
"""FSM: capping exits to pending (no cap emitted) when posted limit is below min cap floor."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitMinCapFloor", 25) # 25 mph floor
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
# Posted limit 20 mph (below 25 mph floor) -> cap must pause
low_limit_ms = 20 * CV.MPH_TO_MS
scenario.set_speed_limits(low_limit_ms, 0)
scenario.set_cruise_speeds(30 * CV.MPH_TO_MS)
for _ in range(int(0.5 / DT_MDL)):
scenario.sla.update(
True, False, 30 * CV.MPH_TO_MS, 0,
30 * CV.MPH_TO_MS, low_limit_ms, low_limit_ms,
True, 0, scenario.events_sp
)
# Below-floor condition pauses cap: state is not capping, v_target is unset (no cap)
assert scenario.sla.state != SpeedLimitAssistState.capping
assert scenario.sla.output_v_target == V_CRUISE_UNSET
def test_resume_from_pause_above_floor(self, scenario_builder):
"""FSM: pending (below-floor pause) -> capping when vehicle speed rises above min cap floor."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitMinCapFloor", 25) # 25 mph
scenario.set_state(SpeedLimitAssistState.pending)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Update with speed above floor
scenario.sla.update(
True, False, 30 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Should resume capping if conditions allow
assert scenario.sla.state in CAP_ACTIVE_STATES or scenario.sla.state == SpeedLimitAssistState.pending
def test_change_debounce_hold_new_limit(self, scenario_builder):
"""FSM: New speed limit held for 1s before accepting (debounce)."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Start in capping with initial cap at 25 mph
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
scenario.sla._has_speed_limit = True
# First: establish baseline with 25 mph and pressed pedal (to establish state for edge detection)
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=True
)
# Then: change limit to 35 mph, press pedal briefly, then release and wait for debounce to expire
# Press pedal for first 0.5s (speed_limit=25 but speed_limit_final_last=35 simulates detection)
for _ in range(int(0.5 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=True
)
# Release pedal and wait for cap debounce + accel debounce to complete (0.7s more = 1.2s total)
for _ in range(int(0.7 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=False
)
# Cap should now be 35 mph after debounce and pedal release edge
assert abs(scenario.sla._target_cap - 35 * CV.MPH_TO_MS) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_upshift_never_raise_keeps_old_cap(self, scenario_builder):
"""FSM: NEVER_RAISE mode keeps cap unchanged when limit increases."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.NEVER_RAISE)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Start with cap at 25 mph
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Increase limit to 35 mph, wait for debounce to expire
for _ in range(int(1.2 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# In NEVER_RAISE mode, cap stays at 25 mph (old value)
assert abs(scenario.sla._target_cap - 25 * CV.MPH_TO_MS) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_upshift_accel_pedal_requires_release(self, scenario_builder):
"""FSM: ACCEL_PEDAL mode accepts new cap only on pedal release."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Start with cap at 25 mph
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Pedal pressed with new limit 35 mph, wait 1.1s (exceeds cap debounce) but pedal still pressed
for _ in range(int(1.1 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=True
)
# Cap should still be 25 mph (pedal pressed, upshift rejected despite debounce)
assert abs(scenario.sla._target_cap - 25 * CV.MPH_TO_MS) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_pedal_release_debounce_200ms(self, scenario_builder):
"""FSM: Accel pedal release edge requires 200ms debounce."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitUpshiftAccept", UpshiftAccept.ACCEL_PEDAL)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# First establish the limit change and wait for cap change debounce to start
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=True
)
# Pedal pressed, wait 0.6s, then release and wait 0.5s (total 1.1s > 1.0s cap debounce + 0.2s accel debounce)
for _ in range(int(0.6 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=True
)
# Release and wait for both debounces to complete
for _ in range(int(0.5 / DT_MDL)):
scenario.sla.update(
True, False, 35 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 35 * CV.MPH_TO_MS,
True, 0, scenario.events_sp, accel_pressed=False
)
# Now cap should be updated to 35 mph (cap debounce complete + accel release edge detected and debounced)
assert abs(scenario.sla._target_cap - 35 * CV.MPH_TO_MS) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_suspended_timer_after_long_override(self, scenario_builder):
"""FSM: 1s suspension window after manual override release."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
scenario.sla.update(
True, True, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
assert scenario.sla.state == SpeedLimitAssistState.disabled
assert scenario.sla._cap_suspended_timer > 0 # frame counter
# Attempt to re-engage within suspension window (0.5s)
for _ in range(int(0.5 / DT_MDL)):
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Should still be disabled (suspension active)
assert scenario.sla.state == SpeedLimitAssistState.disabled
# Wait for suspension to expire (1.1s total)
for _ in range(int(0.7 / DT_MDL)):
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Now cap can re-engage and transition to capping
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_audio_cue_fires_once_on_capping_entry(self, scenario_builder):
"""Event: speedLimitCapActive fires once on entry to capping state."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitCapAudioCue", True)
scenario.set_state(SpeedLimitAssistState.disabled)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Enter capping state
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Verify we transitioned to capping
assert scenario.sla.state == SpeedLimitAssistState.capping
# Audio cue flag should be set
assert scenario.sla._cap_audio_cue_fired is True
def test_audio_cue_disabled_no_fire(self, scenario_builder):
"""Event: speedLimitCapActive suppressed when audio cue disabled."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitCapAudioCue", False)
scenario.set_state(SpeedLimitAssistState.disabled)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
# Enter capping state
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Verify we transitioned to capping
assert scenario.sla.state == SpeedLimitAssistState.capping
# Audio cue flag should NOT be set (disabled)
assert scenario.sla._cap_audio_cue_fired is False
def test_cap_delta_ui_feedback(self, scenario_builder):
"""UI: target_cap tracks posted limit, delta = v_cruise_cluster - target_cap (positive when cap below driver intent)."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
limit_ms = 25 * CV.MPH_TO_MS
cruise_ms = 35 * CV.MPH_TO_MS
scenario.set_speed_limits(limit_ms, 0)
scenario.set_cruise_speeds(cruise_ms)
scenario.sla._target_cap = limit_ms
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
cruise_ms, limit_ms, limit_ms,
True, 0, scenario.events_sp
)
assert abs(scenario.sla.output_v_target - limit_ms) < 0.1
assert abs(scenario.sla._target_cap - limit_ms) < 0.1
cap_delta = max(0., cruise_ms - scenario.sla._target_cap)
assert cap_delta > 0
assert abs(cap_delta - 10 * CV.MPH_TO_MS) < 0.1
assert abs(scenario.sla.cap_delta - cap_delta) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_min_cap_floor_zero_disables_pause(self, scenario_builder):
"""FSM: min cap floor 0 disables pause-on-low-speed behavior."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitMinCapFloor", 0)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(5 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Very low speed (5 mph), but floor is 0
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
5 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Should continue capping (floor 0 means no pause even at low speed)
assert scenario.sla.state == SpeedLimitAssistState.capping
assert scenario.sla._cap_below_floor is False
def test_min_cap_floor_max_value_40_mph(self, scenario_builder):
"""FSM: min cap floor clamped to 40 mph (reasonable max)."""
scenario = scenario_builder()
scenario.set_param("IsMetric", False)
scenario.set_param("SpeedLimitMinCapFloor", 40)
scenario.sla.is_metric = False
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(30 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Speed 25 mph (limit) is below floor 40 mph, should pause
for _ in range(int(0.5 / DT_MDL)):
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
30 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# Should transition to pending (below floor)
assert scenario.sla.state == SpeedLimitAssistState.pending
assert scenario.sla._cap_below_floor is True
def test_v_target_clamped_to_cruise_when_capping(self, scenario_builder):
"""Output: v_target = min(v_cruise, cap) when actively capping."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
# v_target should be min(20, 25) = 20 m/s (driver cruise is limiting factor)
assert abs(scenario.sla.output_v_target - 20 * CV.MPH_TO_MS) < 0.1
assert scenario.sla.state == SpeedLimitAssistState.capping
# Missing edge cases from matrix (Issues #1, #2, #5, #7, #16, #18, #19)
def test_school_zone_false_positive_25_mph_floor(self, scenario_builder):
"""Edge case #1: OSM school zone 25 mph at inactive hours blocked by floor."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitMinCapFloor", 25)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(11.18, 0) # 25 mph -> m/s
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 11.18
# Speed limit 25 mph = 11.18 m/s is at floor (not strictly above)
scenario.sla.update(
True, False, 11.18, 0,
20 * CV.MPH_TO_MS, 11.18, 11.18,
True, 0, scenario.events_sp
)
# Should not pause at exactly floor (must be strictly above)
assert scenario.sla.state == SpeedLimitAssistState.capping
def test_construction_stale_25_mph_pauses(self, scenario_builder):
"""Edge case #2: Construction temp speed limit 25 mph pauses on stale data."""
scenario = scenario_builder()
scenario.set_param("SpeedLimitMinCapFloor", 25)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
# Stale construction limit (25 mph, below floor by resolver check)
scenario.set_speed_limits(11.18, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 11.18
# Simulate old data with has_speed_limit=False (resolver cleared it)
scenario.sla.update(
True, False, 11.18, 0,
20 * CV.MPH_TO_MS, 11.18, 11.18,
False, 0, scenario.events_sp
)
# Should transition to pending (lost limit)
assert scenario.sla.state == SpeedLimitAssistState.pending
@pytest.mark.parametrize("is_metric", [True, False])
def test_kmh_mph_unit_conversion_border(self, scenario_builder, is_metric):
"""Edge case #5: km/h ↔ mph border crossing unit conversion."""
scenario = scenario_builder()
scenario.params.put_bool("IsMetric", is_metric)
# Reload SLA to pick up metric setting
from opendbc.car.toyota.values import CAR as TOYOTA
from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit.tests.conftest import CarParamsFactory
CP, CP_SP, _ = CarParamsFactory.create_car_interface(TOYOTA.TOYOTA_RAV4_TSS2)
scenario.sla = SpeedLimitAssist(CP, CP_SP)
scenario.sla.params.put_bool("IsMetric", is_metric)
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
if is_metric:
# 65 km/h ≈ 40.4 mph
scenario.set_speed_limits(65 * CV.KPH_TO_MS, 0)
scenario.set_cruise_speeds(60 * CV.KPH_TO_MS)
scenario.sla._target_cap = 65 * CV.KPH_TO_MS
else:
# 40 mph ≈ 64.4 km/h
scenario.set_speed_limits(40 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(35 * CV.MPH_TO_MS)
scenario.sla._target_cap = 40 * CV.MPH_TO_MS
scenario.sla.update(
True, False, scenario.sla._target_cap, 0,
scenario.sla.v_cruise_cluster, scenario.sla._speed_limit,
scenario.sla._speed_limit, True, 0, scenario.events_sp
)
# Should remain in capping with correct unit-aware cap
assert scenario.sla.state == SpeedLimitAssistState.capping
assert scenario.sla._has_speed_limit is True
def test_gps_tunnel_fade_10s_age_limit(self, scenario_builder):
"""Edge case #7: GPS tunnel fade stale limit via LIMIT_MAX_MAP_DATA_AGE."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Simulate has_speed_limit=False (resolver expired data after 10s)
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
False, 0, scenario.events_sp
)
# Should transition to pending (lost limit)
assert scenario.sla.state == SpeedLimitAssistState.pending
assert scenario.sla.output_v_target == V_CRUISE_UNSET
def test_lost_speed_limit_mid_cap_to_pending(self, scenario_builder):
"""Edge case #16: Lost speed limit mid-cap transitions to pending."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# Simulate limit suddenly unavailable
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
False, 0, scenario.events_sp
)
# Should transition to pending, emit V_CRUISE_UNSET
assert scenario.sla.state == SpeedLimitAssistState.pending
assert scenario.sla.output_v_target == V_CRUISE_UNSET
def test_mode_param_off_disables_sla_on_pcm_op_long(self, scenario_builder):
"""Edge case #18: SpeedLimitMode controls engagement on pcm_op_long cars."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.disabled)
scenario.set_engaged(True)
scenario.set_speed_limits(25 * CV.MPH_TO_MS, 0)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.set_param("SpeedLimitMode", 0)
scenario.sla.update(
True, False, 25 * CV.MPH_TO_MS, 0,
20 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS, 25 * CV.MPH_TO_MS,
True, 0, scenario.events_sp
)
assert scenario.sla.state == SpeedLimitAssistState.disabled
def test_speed_limit_final_zero_with_has_limit_edge(self, scenario_builder):
"""Edge case #19: has_speed_limit=true but speed_limit_final=0 -> pending."""
scenario = scenario_builder()
scenario.set_state(SpeedLimitAssistState.capping)
scenario.set_engaged(True)
scenario.set_speed_limits(0, 0) # Zero speed limit (bad data)
scenario.set_cruise_speeds(20 * CV.MPH_TO_MS)
scenario.sla._target_cap = 25 * CV.MPH_TO_MS
# has_speed_limit=True but speed_limit_final_last=0
scenario.sla.update(
True, False, 0, 0,
20 * CV.MPH_TO_MS, 0, 0,
True, 0, scenario.events_sp
)
# Should transition to pending (bad/zero limit treated as no limit)
assert scenario.sla.state == SpeedLimitAssistState.pending
def test_non_pcm_path_unchanged(self, scenario_builder):
"""Regression: Non-PCM update_state_machine_non_pcm_long() unchanged."""
scenario = scenario_builder()
# Verify method exists and has correct signature
assert hasattr(scenario.sla, "update_state_machine_non_pcm_long")
# Method signature check (internal implementation detail)

View File

@@ -4,7 +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 numpy as np
from cereal import car
@@ -18,7 +17,6 @@ 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
@@ -28,6 +26,7 @@ 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
@@ -51,13 +50,14 @@ 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.torque_override_enabled:
if self.custom_torque_params and self.torque_override_enabled:
self.use_params = False
else:
self.use_params = self.use_live_torque_params

View File

@@ -216,14 +216,6 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = {
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
},
EventNameSP.speedLimitCapActive: {
ET.WARNING: Alert(
"Speed Limit Capping",
"",
AlertStatus.normal, AlertSize.small,
Priority.LOW, VisualAlert.none, AudibleAlertSP.promptSingleHigh, 5.),
},
EventNameSP.speedLimitChanged: {
ET.WARNING: Alert(
"Set speed changed",

View File

@@ -30,10 +30,6 @@ 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 = {}

View File

@@ -18,7 +18,7 @@ import time
from jsonrpc import dispatcher
from functools import partial
from openpilot.common.params import Params
from openpilot.common.params import Params, ParamKeyType
from openpilot.common.realtime import set_core_affinity
from openpilot.common.swaglog import cloudlog
from openpilot.system.hardware.hw import Paths
@@ -28,11 +28,14 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce
create_connection, WebSocketConnectionClosedException)
import cereal.messaging as messaging
from openpilot.sunnypilot.selfdrive.car.sync_car_list_param import update_car_list_param
from openpilot.sunnypilot.models.default_model import DEFAULT_MODEL
from openpilot.sunnypilot.selfdrive.car.sync_sunnylink_params 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://ws.stg.api.sunnypilot.ai')
SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://athena.sunnylink.ai')
HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4"))
LOCAL_PORT_WHITELIST = {8022}
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
@@ -44,12 +47,15 @@ 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
}
@@ -64,7 +70,6 @@ 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'),
@@ -147,37 +152,6 @@ 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():
@@ -231,34 +205,19 @@ def getParamsAllKeysV1() -> dict[str, str]:
@dispatcher.add_method
def getParamsMetadata() -> str:
"""Compressed equivalent of getParamsAllKeysV1 — same struct, gzipped + base64."""
"""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.
"""
try:
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')
schema = generate_schema()
schema["capabilities"] = generate_capabilities()
schema["capability_labels"] = CAPABILITY_LABELS
schema["default_model"] = DEFAULT_MODEL
raw = json.dumps(schema, separators=(",", ":")).encode("utf-8")
return base64.b64encode(gzip.compress(raw)).decode("utf-8")
except Exception:
cloudlog.exception("sunnylinkd.getParamsMetadata.exception")
raise
@@ -270,12 +229,25 @@ 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:
continue
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"")
params_dict["params"].append({
"key": key,
@@ -306,6 +278,13 @@ 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")

Some files were not shown because too many files have changed in this diff Show More