Compare commits

...

65 Commits

Author SHA1 Message Date
royjr be4223fe13 visuals-radar-tracks
commit 1e8d692804
Merge: 5d5dc6c50c 097dd9b5f2
Author: royjr <royjr96@gmail.com>
Date:   Tue Jun 9 00:13:12 2026 -0400

    Merge branch 'master' into visuals-radar-tracks

commit 5d5dc6c50c
Author: royjr <royjr96@gmail.com>
Date:   Tue Jun 9 00:12:06 2026 -0400

    yaml

commit 5c64fe0695
Merge: 37eeb121cd 066ba92e77
Author: royjr <royjr96@gmail.com>
Date:   Mon Jun 8 23:19:24 2026 -0400

    Merge branch 'master' into visuals-radar-tracks

commit 37eeb121cd
Merge: 19a5202f4c 1658898498
Author: royjr <royjr96@gmail.com>
Date:   Fri Mar 27 16:27:10 2026 -0700

    Merge branch 'master' into visuals-radar-tracks

commit 19a5202f4c
Merge: 920ef49b32 a17a38d8c3
Author: royjr <royjr96@gmail.com>
Date:   Mon Mar 2 02:44:58 2026 -0500

    Merge branch 'master' into visuals-radar-tracks

commit 920ef49b32
Author: royjr <royjr96@gmail.com>
Date:   Thu Feb 5 01:01:07 2026 -0500

    no return

commit c7a37ca89e
Merge: 45ca076aef 5c12a7cfc3
Author: royjr <royjr96@gmail.com>
Date:   Thu Feb 5 00:52:12 2026 -0500

    Merge branch 'master' into visuals-radar-tracks

commit 45ca076aef
Merge: 70cf5e6d52 19a7d1d5d7
Author: royjr <royjr96@gmail.com>
Date:   Wed Dec 31 15:26:12 2025 -0500

    Merge branch 'master' into visuals-radar-tracks

commit 70cf5e6d52
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 12:03:57 2025 -0500

    Update params_metadata.json

commit f4c71e4918
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 10:05:17 2025 -0500

    lint

commit 8408241495
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 02:23:10 2025 -0500

    Update toggles.py

commit 640988673e
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 02:22:16 2025 -0500

    mici toggle

commit 1429975f1e
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 02:22:01 2025 -0500

    Revert "force enable for now"

    This reverts commit efd499448a.

commit efd499448a
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:42:38 2025 -0500

    force enable for now

commit f7e636f089
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:42:19 2025 -0500

    use params

commit 00187dc59e
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:37:19 2025 -0500

    use checks

commit cde6368cf7
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:30:15 2025 -0500

    cleaner

commit 08b56f9b33
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:28:12 2025 -0500

    better sp

commit 78efe3a476
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:21:08 2025 -0500

    init sp

commit 54578675c2
Author: royjr <royjr96@gmail.com>
Date:   Mon Dec 29 00:19:17 2025 -0500

    init
2026-06-09 00:15:59 -04:00
royjr 2b492098e9 Update opendbc_repo 2026-06-09 00:14:58 -04:00
royjr 00fee3ee88 Merge branch 'master' into hyundai-radar-tracks 2026-06-09 00:14:36 -04:00
Jason Wen 097dd9b5f2 sunnylink: deprecate legacy params metadata (#1862) 2026-06-08 23:50:43 -04:00
github-actions[bot] 122ac986de [bot] Update Python packages (#1861)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-08 23:35:54 -04:00
MVL bc27262a92 Revert "deprecate carState.brake" for Honda Gas Interceptor (#1860)
* Return Honda exception removed in commaai#37857

Added handling for brakePressed state with conditions for Honda models.

* Add CAR back to imports

* match

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-06-08 23:20:51 -04:00
royjr 930ab1a84d fixes 2026-06-08 22:37:07 -04:00
royjr 0c77f1b41b auto_radar 2026-06-08 22:26:08 -04:00
royjr 9fd4e5b9f8 Merge branch 'master' into hyundai-radar-tracks 2026-06-08 22:20:43 -04:00
royjr 818dd7c4e3 Update opendbc_repo 2026-06-08 22:20:39 -04:00
James Vecellio-Grant 066ba92e77 modeld_v2: safe model validation (#1855)
* modeld_v2: safe model validation

* fix string

* numpy

* dumb

* god use full attribute names please

---------

Co-authored-by: Jason Wen <haibin.wen3@gmail.com>
2026-06-07 23:07:59 -04:00
github-actions[bot] dfb21bd53e [bot] Update Python packages (#1859)
Update Python packages

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-07 22:56:23 -04:00
royjr 9fe6d20249 Merge branch 'master' into hyundai-radar-tracks 2026-03-27 16:11:57 -07:00
royjr e31b28d0b3 Update opendbc_repo 2026-03-27 16:11:51 -07:00
royjr 06498600bf Update opendbc_repo 2026-03-11 23:28:33 -04:00
royjr 05649045b7 Update opendbc_repo 2026-03-01 23:02:25 -05:00
royjr 0ee21708ac Merge branch 'master' into hyundai-radar-tracks 2026-03-01 22:39:12 -05:00
royjr 1492941483 Update opendbc_repo 2026-03-01 22:39:07 -05:00
royjr 8794178ccf Merge branch 'master' into hyundai-radar-tracks 2026-02-28 17:02:32 -05:00
royjr 86303e7a3c Update opendbc_repo 2026-02-28 17:01:42 -05:00
royjr e189678033 Update opendbc_repo 2026-02-25 21:25:16 -05:00
royjr 99bae4f475 Merge branch 'master' into hyundai-radar-tracks 2026-02-25 21:24:58 -05:00
royjr ef7046c2ed Merge branch 'master' into hyundai-radar-toggle 2026-02-13 22:28:03 -05:00
royjr 734121b517 Update opendbc_repo 2026-02-13 22:27:58 -05:00
royjr b738769c22 Update opendbc_repo 2026-02-05 22:59:22 -05:00
royjr 0bfd9e5cf2 Merge branch 'master' into hyundai-radar-toggle 2026-02-05 22:50:16 -05:00
royjr 20fadbcaa8 Update opendbc_repo 2026-02-05 22:50:08 -05:00
royjr 0eeb052241 hyundai_radar toggle mici 2025-12-29 02:20:27 -05:00
royjr edc2d16ae4 Update params_metadata.json 2025-12-28 17:47:10 -05:00
royjr e72fa9453d Merge branch 'master' into hyundai-radar-toggle 2025-12-28 17:20:20 -05:00
royjr 1c56f0b0a3 Update opendbc_repo 2025-12-28 17:19:47 -05:00
royjr 80d15212e2 Merge branch 'master' into hyundai-radar-toggle 2025-12-20 11:47:50 -05:00
royjr ab2863a859 Update opendbc_repo 2025-12-20 11:45:59 -05:00
royjr 743aa8e736 Merge branch 'master' into hyundai-radar-toggle 2025-10-10 00:09:54 -04:00
royjr 5847256371 Update opendbc_repo 2025-10-10 00:09:37 -04:00
royjr c13f909390 Update opendbc_repo 2025-10-09 01:30:54 -04:00
royjr f1de835d17 Update opendbc_repo 2025-10-09 01:11:07 -04:00
royjr 76ddb20cd5 Merge branch 'master' into hyundai-radar-toggle 2025-10-09 01:10:40 -04:00
royjr ede7f70ddc Merge branch 'master' into hyundai-radar-toggle 2025-10-08 23:04:21 -04:00
royjr 46c3047fa7 Update opendbc_repo 2025-10-08 23:04:16 -04:00
royjr cf5b5c666d Merge branch 'master' into hyundai-radar-toggle 2025-09-30 13:57:57 -04:00
royjr 03b56d6f09 Update opendbc_repo 2025-09-30 13:57:52 -04:00
royjr ab180ce1e5 Update opendbc_repo 2025-09-22 23:13:42 -04:00
royjr e0d8bc88b2 Update opendbc_repo 2025-09-22 00:33:12 -04:00
royjr 1dc45adb1c Merge branch 'master' into hyundai-radar-toggle 2025-09-22 00:28:40 -04:00
royjr c3bd613885 Merge branch 'master' into hyundai-radar-toggle 2025-09-20 01:56:28 -04:00
royjr 55edb4efcb Update opendbc_repo 2025-09-20 01:56:23 -04:00
royjr 7ff9b28c94 Merge branch 'master' into hyundai-radar-toggle 2025-08-24 16:57:46 -04:00
royjr 5f9d340d7b Update opendbc_repo 2025-08-24 16:57:37 -04:00
royjr e82b47cb70 Update opendbc_repo 2025-08-22 12:03:48 -04:00
royjr 10b5e58558 Merge branch 'master' into hyundai-radar-toggle 2025-08-22 11:47:43 -04:00
royjr 116936341f Update opendbc_repo 2025-08-22 11:41:48 -04:00
royjr 8d5bd92d51 Update opendbc_repo 2025-08-14 22:13:26 -04:00
royjr 4453dba6ed Update opendbc_repo 2025-08-13 23:03:43 -04:00
royjr cc20313815 fix param 2025-08-13 20:38:33 -04:00
royjr 3b30b93bbb Merge branch 'master' into hyundai-radar-toggle 2025-08-13 18:34:52 -04:00
royjr 5d9de36d40 Update opendbc_repo 2025-08-13 18:31:10 -04:00
royjr a91b97b89a Set RADAR_OFF flag for Hyundai when radar is off
Adds logic to set the RADAR_OFF flag in CP_SP.flags when the Hyundai radar type is set to OFF. This ensures the correct flag is applied for vehicles with radar disabled.
2025-07-01 02:09:53 -04:00
royjr ec4e2ec3c1 Update opendbc_repo 2025-07-01 02:09:48 -04:00
royjr 1f49367380 Merge branch 'master-new' into hyundai-radar-toggle 2025-07-01 01:10:33 -04:00
royjr 040f81fc91 Update opendbc_repo 2025-07-01 01:10:02 -04:00
royjr 5253b33b7a buttonClicked 2025-06-03 01:34:18 -04:00
royjr fc46a0ed7f Update opendbc_repo 2025-06-03 01:18:21 -04:00
royjr 8056a797c7 Merge branch 'master-new' into hyundai-radar-toggle 2025-06-03 01:17:26 -04:00
royjr a492625927 init 2025-05-20 01:34:50 -04:00
37 changed files with 1950 additions and 2566 deletions
+2
View File
@@ -178,6 +178,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
{"OnroadUploads", {PERSISTENT | BACKUP, BOOL, "1"}},
{"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}},
{"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RadarTracks", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}},
{"RocketFuel", {PERSISTENT | BACKUP, BOOL, "0"}},
{"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}},
@@ -219,6 +220,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
// sunnypilot car specific params
{"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}},
{"HyundaiRadar", {PERSISTENT | BACKUP, INT, "0"}},
{"SubaruStopAndGo", {PERSISTENT | BACKUP, BOOL, "0"}},
{"SubaruStopAndGoManualParkingBrake", {PERSISTENT | BACKUP, BOOL, "0"}},
{"TeslaCoopSteering", {PERSISTENT | BACKUP, BOOL, "0"}},
+119 -117
View File
@@ -4,24 +4,24 @@
A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified.
# 340 Supported Cars
# 341 Supported Cars
|Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|<a href="##"><img width=2000></a>Hardware Needed<br>&nbsp;|Video|Setup Video|
|---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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=Acura ILX 2016-18">Buy Here</a></sub></details>|||
|Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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=Acura ILX 2019">Buy Here</a></sub></details>|||
|Acura|MDX 2022-24|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-full.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=Acura MDX 2022-24">Buy Here</a></sub></details>|||
|Acura|MDX 2022-24|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-full.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=Acura MDX 2022-24">Buy Here</a></sub></details>|||
|Acura|MDX 2025-26|All except Type S|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=Acura MDX 2025-26">Buy Here</a></sub></details>|||
|Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|<details><summary>Parts</summary><sub>- 1 Honda Nidec connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura RDX 2016-18">Buy Here</a></sub></details>|||
|Acura|RDX 2019-21|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|3 mph|[![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=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|Acura|TLX 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 Honda Bosch A connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Acura TLX 2021-22">Buy Here</a></sub></details>|||
|Acura|RDX 2019-21|All|openpilot available[<sup>1,5</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=Acura RDX 2019-21">Buy Here</a></sub></details>|||
|Acura|TLX 2021-22|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.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=Acura TLX 2021-22">Buy Here</a></sub></details>|||
|Acura|TLX 2025|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=Acura TLX 2025">Buy Here</a></sub></details>|||
|Audi[<sup>11</sup>](#footnotes)|A3 2014-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=Audi A3 2014-19">Buy Here</a></sub></details>|||
|Audi[<sup>11</sup>](#footnotes)|A3 Sportback e-tron 2017-18|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 A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|Audi[<sup>11</sup>](#footnotes)|Q2 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 Q2 2018">Buy Here</a></sub></details>|||
|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>|||
|Audi[<sup>12</sup>](#footnotes)|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 A3 2014-19">Buy Here</a></sub></details>|||
|Audi[<sup>12</sup>](#footnotes)|A3 Sportback e-tron 2017-18|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 A3 Sportback e-tron 2017-18">Buy Here</a></sub></details>|||
|Audi[<sup>12</sup>](#footnotes)|Q2 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Q2 2018">Buy Here</a></sub></details>|||
|Audi[<sup>12</sup>](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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" /></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>|||
@@ -33,7 +33,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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" /></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>|||
|CUPRA[<sup>12</sup>](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>|||
@@ -47,8 +47,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<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" /></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|Focus 2018-22[<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-22">Buy Here</a></sub></details>|||
|Ford|Focus Hybrid 2018-22[<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-22">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" /></a>|
@@ -75,35 +75,35 @@ A supported vehicle is one that just works when you install a comma device. All
|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" /></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 2018-22|All|openpilot available[<sup>1,5</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 2018-22|All|openpilot available[<sup>1,5</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|City (Brazil only) 2023|All|openpilot available[<sup>1,5</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" /></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" /></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>|||
|Honda|Civic 2019-21|All|openpilot available[<sup>1,5</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,5</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,5</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,5</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,5</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,5</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,5</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,5</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>|||
|Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 CR-V 2015-16">Buy Here</a></sub></details>|||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1</sup>](#footnotes)|0 mph|15 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 CR-V 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|15 mph|[![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 CR-V 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V 2023-26|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 CR-V 2023-26">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2017-22|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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|12 mph|[![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 CR-V Hybrid 2017-22">Buy Here</a></sub></details>|||
|Honda|CR-V Hybrid 2023-26|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 CR-V Hybrid 2023-26">Buy Here</a></sub></details>|||
|Honda|e 2020|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 e 2020">Buy Here</a></sub></details>|||
|Honda|e 2020|All|openpilot available[<sup>1,5</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 e 2020">Buy Here</a></sub></details>|||
|Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Fit 2018-20">Buy Here</a></sub></details>|||
|Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Freed 2020">Buy Here</a></sub></details>|||
|Honda|HR-V 2019-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 HR-V 2019-22">Buy Here</a></sub></details>|||
|Honda|HR-V 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 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 HR-V 2023-25">Buy Here</a></sub></details>|||
|Honda|Insight 2019-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 Insight 2019-22">Buy Here</a></sub></details>|||
|Honda|Inspire 2018|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 Inspire 2018">Buy Here</a></sub></details>|||
|Honda|N-Box 2018|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.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 N-Box 2018">Buy Here</a></sub></details>|||
|Honda|HR-V 2023-25|All|openpilot available[<sup>1,5</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 HR-V 2023-25">Buy Here</a></sub></details>|||
|Honda|Insight 2019-22|All|openpilot available[<sup>1,5</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 Insight 2019-22">Buy Here</a></sub></details>|||
|Honda|Inspire 2018|All|openpilot available[<sup>1,5</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 Inspire 2018">Buy Here</a></sub></details>|||
|Honda|N-Box 2018|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.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 N-Box 2018">Buy Here</a></sub></details>|||
|Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Odyssey 2018-20">Buy Here</a></sub></details>|||
|Honda|Odyssey 2021-26|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|43 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 Odyssey 2021-26">Buy Here</a></sub></details>|||
|Honda|Odyssey 2021-26|All|openpilot available[<sup>1,5</sup>](#footnotes)|0 mph|43 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 Odyssey 2021-26">Buy Here</a></sub></details>|||
|Honda|Odyssey (Singapore) 2021|Honda Sensing|openpilot|19 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Odyssey (Singapore) 2021">Buy Here</a></sub></details>|||
|Honda|Odyssey (Taiwan) 2018-19|Honda Sensing|openpilot|19 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Odyssey (Taiwan) 2018-19">Buy Here</a></sub></details>|||
|Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.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 Passport 2019-25">Buy Here</a></sub></details>|||
@@ -126,6 +126,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Ioniq 5 (with HDA II) 2022-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=Hyundai Ioniq 5 (with HDA II) 2022-24">Buy Here</a></sub></details>|||
|Hyundai|Ioniq 5 (without HDA II) 2022-24|Highway Driving 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 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 Ioniq 5 (without HDA II) 2022-24">Buy Here</a></sub></details>|||
|Hyundai|Ioniq 6 (with HDA II) 2023-24|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|<details><summary>Parts</summary><sub>- 1 Hyundai P connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq 6 (with HDA II) 2023-24">Buy Here</a></sub></details>|||
|Hyundai|Ioniq 6 (without HDA II) 2023-24|Highway Driving Assist|Stock|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 Ioniq 6 (without HDA II) 2023-24">Buy Here</a></sub></details>|||
|Hyundai|Ioniq Electric 2019|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 C connector<br>- 1 OBD-C cable (2 ft)<br>- 1 comma four<br>- 1 comma power v3<br>- 1 harness box<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Hyundai Ioniq Electric 2019">Buy Here</a></sub></details>|||
|Hyundai|Ioniq Electric 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 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 Ioniq Electric 2020">Buy Here</a></sub></details>|||
|Hyundai|Ioniq Hybrid 2017-19|Smart Cruise Control (SCC)|openpilot available[<sup>1</sup>](#footnotes)|0 mph|32 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=Hyundai Ioniq Hybrid 2017-19">Buy Here</a></sub></details>|||
@@ -223,14 +224,14 @@ 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" /></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>||
|MAN[<sup>12</sup>](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|31 mph|[![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>12</sup>](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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" /></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" /></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>|||
|Nissan[<sup>6</sup>](#footnotes)|Altima 2019-24|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-24">Buy Here</a></sub></details>|||
|Nissan[<sup>6</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>6</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>6</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>|||
@@ -238,35 +239,35 @@ A supported vehicle is one that just works when you install a comma device. All
|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" /></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" /></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>|||
|Subaru|Impreza 2017-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 Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|openpilot available[<sup>1,7</sup>](#footnotes)|0 mph|0 mph|[![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 Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[![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 Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2020-22|All[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[![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 Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>6</sup>](#footnotes)|Stock|0 mph|0 mph|[![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" /></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)|||
|Škoda[<sup>11</sup>](#footnotes)|Karoq 2019-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 Karoq 2019-23">Buy Here</a></sub></details>|||
|Škoda[<sup>11</sup>](#footnotes)|Kodiaq 2017-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 Kodiaq 2017-23">Buy Here</a></sub></details>|||
|Škoda[<sup>11</sup>](#footnotes)|Octavia 2015-19[<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 Octavia 2015-19">Buy Here</a></sub></details>|||
|Škoda[<sup>11</sup>](#footnotes)|Octavia RS 2016[<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 Octavia RS 2016">Buy Here</a></sub></details>|||
|Škoda[<sup>11</sup>](#footnotes)|Octavia Scout 2017-19[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![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 Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|Škoda|Scala 2020-23[<sup>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![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 Scala 2020-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|Škoda[<sup>11</sup>](#footnotes)|Superb 2015-22[<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 Superb 2015-22">Buy Here</a></sub></details>|||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|Tesla[<sup>9</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>8</sup>](#footnotes)|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 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>8</sup>](#footnotes)|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 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|Tesla[<sup>9</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>8</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|SEAT[<sup>12</sup>](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>|||
|Subaru|Impreza 2017-19|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</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 Impreza 2017-19">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Impreza 2020-22|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|openpilot available[<sup>1,8</sup>](#footnotes)|0 mph|0 mph|[![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 Impreza 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2015-18|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![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 Legacy 2015-18">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Legacy 2020-22|All[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![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 Legacy 2020-22">Buy Here</a></sub></details><details><summary>Tools</summary><sub>- 1 Pry Tool<br>- 1 Socket Wrench 8mm or 5/16" (deep)</sub></details>|||
|Subaru|Outback 2015-17|EyeSight Driver Assistance[<sup>7</sup>](#footnotes)|Stock|0 mph|0 mph|[![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>7</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>7</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>7</sup>](#footnotes)|openpilot available[<sup>1,8</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>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>17</sup>](#footnotes)|||
|Škoda|Kamiq 2021-23[<sup>13,15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>17</sup>](#footnotes)|||
|Škoda[<sup>12</sup>](#footnotes)|Karoq 2019-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Karoq 2019-23">Buy Here</a></sub></details>|||
|Škoda[<sup>12</sup>](#footnotes)|Kodiaq 2017-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Kodiaq 2017-23">Buy Here</a></sub></details>|||
|Škoda[<sup>12</sup>](#footnotes)|Octavia 2015-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Octavia 2015-19">Buy Here</a></sub></details>|||
|Škoda[<sup>12</sup>](#footnotes)|Octavia RS 2016[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Octavia RS 2016">Buy Here</a></sub></details>|||
|Škoda[<sup>12</sup>](#footnotes)|Octavia Scout 2017-19[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Octavia Scout 2017-19">Buy Here</a></sub></details>|||
|Škoda|Scala 2020-23[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Scala 2020-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Škoda[<sup>12</sup>](#footnotes)|Superb 2015-22[<sup>15</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Superb 2015-22">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW3) 2019-23[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW3) 2019-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model 3 (with HW4) 2024-25[<sup>9</sup>](#footnotes)|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 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model 3 (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW3) 2020-23[<sup>9</sup>](#footnotes)|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 Tesla A connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW3) 2020-23">Buy Here</a></sub></details>|||
|Tesla[<sup>10</sup>](#footnotes)|Model Y (with HW4) 2024-25[<sup>9</sup>](#footnotes)|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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 Tesla B connector<br>- 1 comma four<br>- 1 harness box<br>- 1 long OBD-C cable (9.5 ft)<br>- 1 mount<br><a href="https://comma.ai/shop/comma-3x?harness=Tesla Model Y (with HW4) 2024-25">Buy Here</a></sub></details>|||
|Toyota|Alphard 2019-20|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 Alphard 2019-20">Buy Here</a></sub></details>|||
|Toyota|Alphard Hybrid 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 Alphard Hybrid 2021">Buy Here</a></sub></details>|||
|Toyota|Avalon 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 Avalon 2016">Buy Here</a></sub></details>|||
@@ -279,8 +280,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|C-HR 2021|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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" /></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 2018-20|All|Stock|0 mph[<sup>11</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>11</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" /></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>|||
@@ -312,60 +313,61 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|RAV4 Hybrid 2022|All|openpilot available[<sup>1</sup>](#footnotes)|0 mph|0 mph|[![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" /></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>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTD 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 GTD 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Golf GTE 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 GTE 2015-20">Buy Here</a></sub></details>|||
|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" /></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>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Passat Alltrack 2015-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 Passat Alltrack 2015-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Passat GTE 2015-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 Passat GTE 2015-22">Buy Here</a></sub></details>|||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![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 Polo 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![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 Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,15</sup>](#footnotes)|0 mph|0 mph|[![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 T-Cross 2021">Buy Here</a></sub></details>[<sup>16</sup>](#footnotes)|||
|Volkswagen[<sup>11</sup>](#footnotes)|T-Roc 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 T-Roc 2018-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Taos 2022-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=Volkswagen Taos 2022-24">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont 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 Teramont 2018-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont Cross Sport 2021-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 Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Teramont X 2021-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 Teramont X 2021-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Tiguan 2018-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=Volkswagen Tiguan 2018-24">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Tiguan eHybrid 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 Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>11</sup>](#footnotes)|Touran 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=Volkswagen Touran 2016-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTD 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 GTD 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTE 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 GTE 2015-20">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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>12</sup>](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>12</sup>](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>14</sup>](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat Alltrack 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Alltrack 2015-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Passat GTE 2015-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 GTE 2015-22">Buy Here</a></sub></details>|||
|Volkswagen|Polo 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Polo 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Volkswagen|Polo GTI 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 Polo GTI 2018-23">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Volkswagen|T-Cross 2021|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</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 T-Cross 2021">Buy Here</a></sub></details>[<sup>17</sup>](#footnotes)|||
|Volkswagen[<sup>12</sup>](#footnotes)|T-Roc 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 T-Roc 2018-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Taos 2022-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Taos 2022-24">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Teramont 2018-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont Cross Sport 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Teramont Cross Sport 2021-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Teramont X 2021-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Teramont X 2021-22">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Tiguan 2018-24">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Tiguan eHybrid 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Tiguan eHybrid 2021-23">Buy Here</a></sub></details>|||
|Volkswagen[<sup>12</sup>](#footnotes)|Touran 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[<sup>1,16</sup>](#footnotes)|0 mph|0 mph|[![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 Touran 2016-23">Buy Here</a></sub></details>|||
### Footnotes
<sup>1</sup>openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `nightly-dev`. <br />
<sup>2</sup>Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in North and South America/Southeast Asia. <br />
<sup>3</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/gm" target="_blank">GM</a>. <br />
<sup>4</sup>2019 Honda Civic 1.6L Diesel Sedan does not have ALC below 12mph. <br />
<sup>5</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
<sup>6</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
<sup>7</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
<sup>8</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
<sup>9</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
<sup>10</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
<sup>11</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
<sup>12</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
<sup>13</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
<sup>14</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
<sup>15</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
<sup>16</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
<sup>5</sup>Enabling longitudinal control (alpha) will disable all CMBS functionality, including AEB and FCW. <br />
<sup>6</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/nissan" target="_blank">Nissan</a>. <br />
<sup>7</sup>In the non-US market, openpilot requires the car to come equipped with EyeSight with Lane Keep Assistance. <br />
<sup>8</sup>Enabling longitudinal control (alpha) will disable all EyeSight functionality, including AEB, LDW, and RAB. <br />
<sup>9</sup>Some 2023 model years have HW4. To check which hardware type your vehicle has, look for <b>Autopilot computer</b> under <b>Software -> Additional Vehicle Information</b> on your vehicle's touchscreen. See <a href="https://www.notateslaapp.com/news/2173/how-to-check-if-your-tesla-has-hardware-4-ai4-or-hardware-3">this page</a> for more information. <br />
<sup>10</sup>See more setup details for <a href="https://github.com/commaai/openpilot/wiki/tesla" target="_blank">Tesla</a>. <br />
<sup>11</sup>openpilot operates above 28mph for Camry 4CYL L, 4CYL LE and 4CYL SE which don't have Full-Speed Range Dynamic Radar Cruise Control. <br />
<sup>12</sup>The J533 harness plugs in at the CAN gateway under the dashboard, just above the steering column. More information can be found at <a href="https://docs.howtocomma.com/docs/j533-harness-install" target="_blank">this guide</a>. <br />
<sup>13</sup>Not including the China market Kamiq, which is based on the (currently) unsupported PQ34 platform. <br />
<sup>14</sup>Refers only to the MQB-based European B8 Passat, not the NMS Passat in the USA/China/Mideast markets. <br />
<sup>15</sup>Some Škoda vehicles are equipped with heated windshields, which are known to block GPS signal needed for some comma four functionality. <br />
<sup>16</sup>Only available for vehicles using a gateway (J533) harness. At this time, vehicles using a camera harness are limited to using stock ACC. <br />
<sup>17</sup>Model-years 2022 and beyond may have a combined CAN gateway and BCM, which is supported by openpilot in software, but doesn't yet have a harness available from the comma store. <br />
## Community Maintained Cars
Although they're not upstream, the community has openpilot running on other makes and models. See the 'Community Supported Models' section of each make [on our wiki](https://wiki.comma.ai/).
+14 -3
View File
@@ -13,7 +13,7 @@ from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs
from opendbc.car.can_definitions import CanData
from opendbc.car.car_helpers import FRAME_FINGERPRINT, interfaces
from opendbc.car.fingerprints import MIGRATION
from opendbc.car.honda.values import HondaFlags
from opendbc.car.honda.values import CAR as HONDA, HondaFlags
from opendbc.car.structs import car
from opendbc.car.tests.routes import non_tested_cars, routes, CarTestRoute
from opendbc.car.values import Platform, PLATFORMS
@@ -358,7 +358,13 @@ class TestCarModelBase(unittest.TestCase):
self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev())
if self.safety.get_brake_pressed_prev() != prev_panda_brake:
self.assertEqual(CS.brakePressed, self.safety.get_brake_pressed_prev())
# TODO: remove this exception once this mismatch is resolved
brake_pressed = CS.brakePressed
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brake > 0.05:
brake_pressed = False
self.assertEqual(brake_pressed, self.safety.get_brake_pressed_prev())
if self.safety.get_regen_braking_prev() != prev_panda_regen_braking:
self.assertEqual(CS.regenBraking, self.safety.get_regen_braking_prev())
@@ -442,7 +448,12 @@ class TestCarModelBase(unittest.TestCase):
checks['steeringAngleDeg'] += (angle_can > (self.safety.get_angle_meas_max() + 1) or
angle_can < (self.safety.get_angle_meas_min() - 1))
checks['brakePressed'] += CS.brakePressed != self.safety.get_brake_pressed_prev()
# TODO: remove this exception once this mismatch is resolved
brake_pressed = CS.brakePressed
if CS.brakePressed and not self.safety.get_brake_pressed_prev():
if self.CP.carFingerprint in (HONDA.HONDA_PILOT, HONDA.HONDA_RIDGELINE) and CS.brakeDEPRECATED > 0.05:
brake_pressed = False
checks['brakePressed'] += brake_pressed != self.safety.get_brake_pressed_prev()
checks['regenBraking'] += CS.regenBraking != self.safety.get_regen_braking_prev()
checks['steeringDisengage'] += CS.steeringDisengage != self.safety.get_steering_disengage_prev()
@@ -21,8 +21,11 @@ class TogglesLayoutMici(NavScroller):
record_front = BigParamControl("record & upload driver camera", "RecordFront", toggle_callback=restart_needed_callback)
record_mic = BigParamControl("record & upload mic audio", "RecordAudio", toggle_callback=restart_needed_callback)
enable_openpilot = BigParamControl("enable sunnypilot", "OpenpilotEnabledToggle", toggle_callback=restart_needed_callback)
hyundai_radar = BigMultiParamToggle("hyundai radar", "HyundaiRadar", ["off", "lead only", "full radar"])
radar_tracks = BigParamControl("radar tracks", "RadarTracks")
self._scroller.add_widgets([
radar_tracks,
self._personality_toggle,
self._experimental_btn,
is_metric_toggle,
@@ -35,6 +38,8 @@ class TogglesLayoutMici(NavScroller):
# Toggle lists
self._refresh_toggles = (
("HyundaiRadar", hyundai_radar),
("RadarTracks", radar_tracks),
("ExperimentalMode", self._experimental_btn),
("IsMetric", is_metric_toggle),
("IsLdwEnabled", ldw_toggle),
@@ -153,6 +153,9 @@ class ModelRenderer(Widget, ModelRendererSP):
self._draw_lane_lines()
self._draw_path(sm)
if ui_state.radar_tracks and sm.valid['liveTracks'] and sm.recv_frame['liveTracks'] >= ui_state.started_frame:
self.radar_tracks.draw_radar_tracks(sm['liveTracks'], self._map_to_screen, self._path_offset_z, track_size=3)
# if render_lead_indicator and radar_state:
# self._draw_lead_indicator()
+3
View File
@@ -135,6 +135,9 @@ class ModelRenderer(Widget, ChevronMetrics, ModelRendererSP):
self._draw_lane_lines()
self._draw_path(sm)
if ui_state.radar_tracks and sm.valid['liveTracks'] and sm.recv_frame['liveTracks'] >= ui_state.started_frame:
self.radar_tracks.draw_radar_tracks(sm['liveTracks'], self._map_to_screen, self._path_offset_z)
if render_lead_indicator and radar_state:
self._draw_lead_indicator()
self.chevron_metrics.draw_lead_status(sm, radar_state, self._rect, self._lead_vehicles)
@@ -6,9 +6,12 @@ See the LICENSE.md file in the root directory for more details.
"""
from openpilot.selfdrive.ui.sunnypilot.onroad.chevron_metrics import ChevronMetrics
from openpilot.selfdrive.ui.sunnypilot.onroad.rainbow_path import RainbowPath
from openpilot.selfdrive.ui.sunnypilot.onroad.radar_tracks import RadarTracks
class ModelRendererSP:
def __init__(self):
self.rainbow_path = RainbowPath()
self.chevron_metrics = ChevronMetrics()
self.radar_tracks = RadarTracks()
@@ -0,0 +1,23 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import math
import pyray as rl
class RadarTracks:
def draw_radar_tracks(self, live_tracks, map_to_screen, path_offset_z, track_size=6):
for track in live_tracks.points:
d_rel, y_rel, v_rel, a_rel = track.dRel, track.yRel, track.vRel, track.aRel
if not (math.isfinite(d_rel) and math.isfinite(y_rel) and math.isfinite(v_rel) and math.isfinite(a_rel)):
continue
pt = map_to_screen(d_rel, -y_rel, path_offset_z)
if pt is None:
continue
x, y = pt
rl.draw_circle(int(x), int(y), track_size, rl.Color(0, 255, 64, 255))
+1
View File
@@ -169,6 +169,7 @@ class UIStateSP:
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")
self.radar_tracks = self.params.get_bool("RadarTracks")
if not self._sp_initialized:
self._sp_initialized = True
+1
View File
@@ -63,6 +63,7 @@ class UIState(UIStateSP):
"liveParameters",
"testJoystick",
"rawAudioData",
"liveTracks",
] + self.sm_services_ext
)
+10 -10
View File
@@ -188,7 +188,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
n_frames = img_shape[1] // 6
img_buf_shape = (frame_skip * (n_frames - 1) + 1, 6, img_shape[2], img_shape[3])
npy_keys = {}
numpy_keys = {}
queue_keys = {}
for key, shape in input_shapes.items():
@@ -196,7 +196,7 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
continue
if len(shape) == 3 and shape[1] > 1:
if key.startswith('desire'):
npy_keys[key] = np.zeros(shape[2], dtype=np.float32)
numpy_keys[key] = np.zeros(shape[2], dtype=np.float32)
queue_keys[f'{key}_q'] = Tensor(
np.zeros((frame_skip * shape[1], shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
@@ -205,24 +205,24 @@ def make_supercombo_input_queues(input_shapes, frame_skip, device):
np.zeros((frame_skip * (shape[1] - 1) + 1, shape[0], shape[2]), dtype=np.float32),
device=device).contiguous().realize()
else:
npy_keys[key] = np.zeros(shape, dtype=np.float32)
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
elif len(shape) == 2:
npy_keys[key] = np.zeros(shape, dtype=np.float32)
numpy_keys[key] = np.zeros(shape, dtype=np.float32)
if 'traffic_convention' not in npy_keys:
if 'traffic_convention' not in numpy_keys:
tc_shape = input_shapes.get('traffic_convention', (1, 2))
npy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
numpy_keys['traffic_convention'] = np.zeros(tc_shape, dtype=np.float32)
npy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
npy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
numpy_keys['tfm'] = np.zeros((3, 3), dtype=np.float32)
numpy_keys['big_tfm'] = np.zeros((3, 3), dtype=np.float32)
input_queues = {
'img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
'big_img_q': Tensor(np.zeros(img_buf_shape, dtype=np.uint8), device=device).contiguous().realize(),
**queue_keys,
**{k: Tensor(v, device='NPY').realize() for k, v in npy_keys.items()},
**{k: Tensor(v, device='NPY').realize() for k, v in numpy_keys.items()},
}
return input_queues, npy_keys
return input_queues, numpy_keys
def make_run_supercombo(model_runner, nv12: NV12Frame, model_w, model_h,
+11 -16
View File
@@ -62,11 +62,6 @@ def _find_driving_pkl(bundle):
if _pkl_exists(pkl_path):
return pkl_path
fallback = os.path.join(model_root, 'driving_tinygrad.pkl')
if _pkl_exists(fallback):
return fallback
return None
class FrameMeta:
frame_id: int = 0
@@ -125,7 +120,7 @@ class ModelState(ModelStateBase):
self._vision_input_names = [k for k in model_metadata['input_shapes'] if 'img' in k]
from openpilot.sunnypilot.modeld_v2.compile_modeld import make_supercombo_input_queues
frame_skip = derive_frame_skip({}, model_metadata['input_shapes'])
self.input_queues, self.npy = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
self.input_queues, self.numpy_inputs = make_supercombo_input_queues(model_metadata['input_shapes'], frame_skip, device=self.DEV)
else:
vision_metadata = metadata['vision']
policy_keys = [k for k in metadata if k != 'vision']
@@ -143,7 +138,7 @@ class ModelState(ModelStateBase):
policy_input_shapes = first_policy_metadata['input_shapes']
self._vision_input_names = [k for k in vision_input_shapes if 'img' in k]
frame_skip = derive_frame_skip(vision_input_shapes, policy_input_shapes)
self.input_queues, self.npy = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
self.input_queues, self.numpy_inputs = make_split_input_queues(vision_input_shapes, policy_input_shapes, frame_skip, device=self.DEV)
from openpilot.sunnypilot.modeld_v2.parse_model_outputs_split import Parser as SplitParser
from openpilot.sunnypilot.modeld_v2.parse_model_outputs import Parser as CombinedParser
@@ -183,7 +178,7 @@ class ModelState(ModelStateBase):
@property
def desire_key(self) -> str:
return next(k for k in self.npy if k.startswith('desire'))
return next(k for k in self.numpy_inputs if k.startswith('desire'))
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:
@@ -199,16 +194,16 @@ class ModelState(ModelStateBase):
desire_key = self.desire_key
inputs[desire_key][0] = 0
self.npy[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
self.numpy_inputs[desire_key][:] = np.where(inputs[desire_key] - self.prev_desire > .99, inputs[desire_key], 0)
self.prev_desire[:] = inputs[desire_key]
for key in ('traffic_convention', 'lateral_control_params'):
if key in self.npy and key in inputs:
self.npy[key][:] = inputs[key]
if key in self.numpy_inputs and key in inputs:
self.numpy_inputs[key][:] = inputs[key]
road_key = next(n for n in bufs if 'big' not in n)
wide_key = next(n for n in bufs if 'big' in n)
self.npy['tfm'][:, :] = transforms[road_key].reshape(3, 3)
self.npy['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
self.numpy_inputs['tfm'][:, :] = transforms[road_key].reshape(3, 3)
self.numpy_inputs['big_tfm'][:, :] = transforms[wide_key].reshape(3, 3)
if prepare_only:
self._warp_enqueue(**self.input_queues, frame=self.full_frames[road_key], big_frame=self.full_frames[wide_key])
@@ -236,8 +231,8 @@ class ModelState(ModelStateBase):
if 'planplus' in outputs and 'plan' in outputs:
outputs['plan'] = outputs['plan'] + outputs['planplus']
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.npy:
buf = self.npy['prev_desired_curv']
if 'desired_curvature' in outputs and 'prev_desired_curv' in self.numpy_inputs:
buf = self.numpy_inputs['prev_desired_curv']
buf[0, :-1] = buf[0, 1:]
buf[0, -1, :] = outputs['desired_curvature'][0, :] if not self.mlsim else 0
@@ -409,7 +404,7 @@ def main(demo=False):
'traffic_convention': traffic_convention,
}
if 'lateral_control_params' in model.npy:
if 'lateral_control_params' in model.numpy_inputs:
inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32)
mt1 = time.perf_counter()
-62
View File
@@ -1,62 +0,0 @@
## Neural networks in openpilot
To view the architecture of the ONNX networks, you can use [netron](https://netron.app/)
## Supercombo
### Supercombo input format (Full size: 799906 x float32)
* **image stream**
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
* Channel 4 represents the half-res U channel
* Channel 5 represents the half-res V channel
* **wide image stream**
* Two consecutive images (256 * 512 * 3 in RGB) recorded at 20 Hz : 393216 = 2 * 6 * 128 * 256
* Each 256 * 512 image is represented in YUV420 with 6 channels : 6 * 128 * 256
* Channels 0,1,2,3 represent the full-res Y channel and are represented in numpy as Y[::2, ::2], Y[::2, 1::2], Y[1::2, ::2], and Y[1::2, 1::2]
* Channel 4 represents the half-res U channel
* Channel 5 represents the half-res V channel
* **desire**
* one-hot encoded buffer to command model to execute certain actions, bit needs to be sent for the past 5 seconds (at 20FPS) : 100 * 8
* **traffic convention**
* one-hot encoded vector to tell model whether traffic is right-hand or left-hand traffic : 2
* **feature buffer**
* A buffer of intermediate features that gets appended to the current feature to form a 5 seconds temporal context (at 20FPS) : 99 * 512
### Supercombo output format (Full size: XXX x float32)
Read [here](https://github.com/commaai/openpilot/blob/90af436a121164a51da9fa48d093c29f738adf6a/selfdrive/modeld/models/driving.h#L236) for more.
## Driver Monitoring Model
* .onnx model can be run with onnx runtimes
* .dlc file is a pre-quantized model and only runs on qualcomm DSPs
### input format
* single image W = 1440 H = 960 luminance channel (Y) from the planar YUV420 format:
* full input size is 1440 * 960 = 1382400
* normalized ranging from 0.0 to 1.0 in float32 (onnx runner) or ranging from 0 to 255 in uint8 (snpe runner)
* camera calibration angles (roll, pitch, yaw) from liveCalibration: 3 x float32 inputs
### output format
* 84 x float32 outputs = 2 + 41 * 2 ([parsing example](https://github.com/commaai/openpilot/blob/22ce4e17ba0d3bfcf37f8255a4dd1dc683fe0c38/selfdrive/modeld/models/dmonitoring.cc#L33))
* for each person in the front seats (2 * 41)
* face pose: 12 = 6 + 6
* face orientation [pitch, yaw, roll] in camera frame: 3
* face position [dx, dy] relative to image center: 2
* normalized face size: 1
* standard deviations for above outputs: 6
* face visible probability: 1
* eyes: 20 = (8 + 1) + (8 + 1) + 1 + 1
* eye position and size, and their standard deviations: 8
* eye visible probability: 1
* eye closed probability: 1
* wearing sunglasses probability: 1
* face occluded probability: 1
* touching wheel probability: 1
* paying attention probability: 1
* (deprecated) distracted probabilities: 2
* using phone probability: 1
* distracted probability: 1
* common outputs 2
* poor camera vision probability: 1
* left hand drive probability: 1
@@ -1,101 +0,0 @@
// clang++ -O2 repro.cc && ./a.out
#include <sched.h>
#include <sys/types.h>
#include <unistd.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
static inline double millis_since_boot() {
struct timespec t;
clock_gettime(CLOCK_BOOTTIME, &t);
return t.tv_sec * 1000.0 + t.tv_nsec * 1e-6;
}
#define MODEL_WIDTH 320
#define MODEL_HEIGHT 640
// null function still breaks it
#define input_lambda(x) x
// this is copied from models/dmonitoring.cc, and is the code that triggers the issue
void inner(uint8_t *resized_buf, float *net_input_buf) {
int resized_width = MODEL_WIDTH;
int resized_height = MODEL_HEIGHT;
// one shot conversion, O(n) anyway
// yuvframe2tensor, normalize
for (int r = 0; r < MODEL_HEIGHT/2; r++) {
for (int c = 0; c < MODEL_WIDTH/2; c++) {
// Y_ul
net_input_buf[(c*MODEL_HEIGHT/2) + r] = input_lambda(resized_buf[(2*r*resized_width) + (2*c)]);
// Y_ur
net_input_buf[(c*MODEL_HEIGHT/2) + r + (2*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width) + (2*c+1)]);
// Y_dl
net_input_buf[(c*MODEL_HEIGHT/2) + r + ((MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c)]);
// Y_dr
net_input_buf[(c*MODEL_HEIGHT/2) + r + (3*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(2*r*resized_width+1) + (2*c+1)]);
// U
net_input_buf[(c*MODEL_HEIGHT/2) + r + (4*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + (r*resized_width/2) + c]);
// V
net_input_buf[(c*MODEL_HEIGHT/2) + r + (5*(MODEL_WIDTH/2)*(MODEL_HEIGHT/2))] = input_lambda(resized_buf[(resized_width*resized_height) + ((resized_width/2)*(resized_height/2)) + (r*resized_width/2) + c]);
}
}
}
float trial() {
int resized_width = MODEL_WIDTH;
int resized_height = MODEL_HEIGHT;
int yuv_buf_len = (MODEL_WIDTH/2) * (MODEL_HEIGHT/2) * 6; // Y|u|v -> y|y|y|y|u|v
// allocate the buffers
uint8_t *resized_buf = (uint8_t*)malloc(resized_width*resized_height*3/2);
float *net_input_buf = (float*)malloc(yuv_buf_len*sizeof(float));
printf("allocate -- %p 0x%x -- %p 0x%lx\n", resized_buf, resized_width*resized_height*3/2, net_input_buf, yuv_buf_len*sizeof(float));
// test for bad buffers
static int CNT = 20;
float avg = 0.0;
for (int i = 0; i < CNT; i++) {
double s4 = millis_since_boot();
inner(resized_buf, net_input_buf);
double s5 = millis_since_boot();
avg += s5-s4;
}
avg /= CNT;
// once it's bad, it's reliably bad
if (avg > 10) {
printf("HIT %f\n", avg);
printf("BAD\n");
for (int i = 0; i < 200; i++) {
double s4 = millis_since_boot();
inner(resized_buf, net_input_buf);
double s5 = millis_since_boot();
printf("%.2f ", s5-s4);
}
printf("\n");
exit(0);
}
// don't free so we get a different buffer each time
//free(resized_buf);
//free(net_input_buf);
return avg;
}
int main() {
while (true) {
float ret = trial();
printf("got %f\n", ret);
}
}
@@ -46,16 +46,6 @@ class TestFindDrivingPkl:
assert result is not None
assert 'driving_fof_tinygrad.pkl' in result
def test_finds_fallback_driving_tinygrad(self, tmp_path, monkeypatch):
(tmp_path / 'driving_tinygrad.pkl').write_bytes(b'fake')
from openpilot.system.hardware import hw
monkeypatch.setattr(hw.Paths, 'model_root', staticmethod(lambda: str(tmp_path)))
bundle = DummyBundle(models=[DummyModel('vision', 'nonexistent.pkl')])
result = _find_driving_pkl(bundle)
assert result is not None
assert 'driving_tinygrad.pkl' in result
# Init — assertion guard
@@ -84,8 +74,8 @@ class TestStockEquivalence:
skip_keys = {'action_t'}
assert set(state.input_queues.keys()) == set(stock_queues.keys()) - skip_keys, \
f"Queue keys differ: v2={set(state.input_queues.keys())}, stock={set(stock_queues.keys())}"
assert set(state.npy.keys()) == set(stock_npy.keys()) - skip_keys, \
f"Npy keys differ: v2={set(state.npy.keys())}, stock={set(stock_npy.keys())}"
assert set(state.numpy_inputs.keys()) == set(stock_npy.keys()) - skip_keys, \
f"Npy keys differ: v2={set(state.numpy_inputs.keys())}, stock={set(stock_npy.keys())}"
def test_split_queue_keys_work_with_desire_key(self, model_state_factory):
from openpilot.sunnypilot.modeld_v2.compile_modeld import derive_frame_skip, make_split_input_queues
@@ -188,16 +178,16 @@ class TestInputQueueCreation:
def test_npy_contains_transforms(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert 'tfm' in state.npy, f"{arch.name}: 'tfm' missing from npy"
assert 'big_tfm' in state.npy, f"{arch.name}: 'big_tfm' missing from npy"
assert state.npy['tfm'].shape == (3, 3)
assert state.npy['big_tfm'].shape == (3, 3)
assert 'tfm' in state.numpy_inputs, f"{arch.name}: 'tfm' missing from npy"
assert 'big_tfm' in state.numpy_inputs, f"{arch.name}: 'big_tfm' missing from npy"
assert state.numpy_inputs['tfm'].shape == (3, 3)
assert state.numpy_inputs['big_tfm'].shape == (3, 3)
@pytest.mark.parametrize("archetype_name", ARCHETYPE_NAMES)
def test_npy_contains_desire(self, archetype_name, model_state_factory):
arch = ARCHETYPES[archetype_name]
state = model_state_factory(arch)
assert arch.expected_desire_key in state.npy, \
assert arch.expected_desire_key in state.numpy_inputs, \
f"{arch.name}: '{arch.expected_desire_key}' missing from npy"
@@ -1,2 +0,0 @@
#!/usr/bin/env bash
clang++ -I /home/batman/one/external/tensorflow/include/ -L /home/batman/one/external/tensorflow/lib -Wl,-rpath=/home/batman/one/external/tensorflow/lib main.cc -ltensorflow
@@ -1,69 +0,0 @@
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include "tensorflow/c/c_api.h"
void* read_file(const char* path, size_t* out_len) {
FILE* f = fopen(path, "r");
if (!f) {
return NULL;
}
fseek(f, 0, SEEK_END);
long f_len = ftell(f);
rewind(f);
char* buf = (char*)calloc(f_len, 1);
assert(buf);
size_t num_read = fread(buf, f_len, 1, f);
fclose(f);
if (num_read != 1) {
free(buf);
return NULL;
}
if (out_len) {
*out_len = f_len;
}
return buf;
}
static void DeallocateBuffer(void* data, size_t) {
free(data);
}
int main(int argc, char* argv[]) {
TF_Buffer* buf;
TF_Graph* graph;
TF_Status* status;
char *path = argv[1];
// load model
{
size_t model_size;
char tmp[1024];
snprintf(tmp, sizeof(tmp), "%s.pb", path);
printf("loading model %s\n", tmp);
uint8_t *model_data = (uint8_t *)read_file(tmp, &model_size);
buf = TF_NewBuffer();
buf->data = model_data;
buf->length = model_size;
buf->data_deallocator = DeallocateBuffer;
printf("loaded model of size %d\n", model_size);
}
// import graph
status = TF_NewStatus();
graph = TF_NewGraph();
TF_ImportGraphDefOptions *opts = TF_NewImportGraphDefOptions();
TF_GraphImportGraphDef(graph, buf, opts, status);
TF_DeleteImportGraphDefOptions(opts);
TF_DeleteBuffer(buf);
if (TF_GetCode(status) != TF_OK) {
printf("FAIL: %s\n", TF_Message(status));
} else {
printf("SUCCESS\n");
}
}
@@ -1,8 +0,0 @@
#!/usr/bin/env python3
import sys
import tensorflow as tf
with open(sys.argv[1], "rb") as f:
graph_def = tf.compat.v1.GraphDef()
graph_def.ParseFromString(f.read())
#tf.io.write_graph(graph_def, '', sys.argv[1]+".try")
@@ -1,38 +0,0 @@
#!/usr/bin/env python3
import os
import time
import numpy as np
import cereal.messaging as messaging
from openpilot.system.manager.process_config import managed_processes
N = int(os.getenv("N", "5"))
TIME = int(os.getenv("TIME", "30"))
if __name__ == "__main__":
sock = messaging.sub_sock('modelV2', conflate=False, timeout=1000)
execution_times = []
for _ in range(N):
os.environ['LOGPRINT'] = 'debug'
managed_processes['modeld'].start()
time.sleep(5)
t = []
start = time.monotonic()
while time.monotonic() - start < TIME:
msgs = messaging.drain_sock(sock, wait_for_one=True)
for m in msgs:
t.append(m.modelV2.modelExecutionTime)
execution_times.append(np.array(t[10:]) * 1000)
managed_processes['modeld'].stop()
print("\n\n")
print(f"ran modeld {N} times for {TIME}s each")
for _, t in enumerate(execution_times):
print(f"\tavg: {sum(t)/len(t):0.2f}ms, min: {min(t):0.2f}ms, max: {max(t):0.2f}ms")
print("\n\n")
+117 -85
View File
@@ -6,80 +6,138 @@ See the LICENSE.md file in the root directory for more details.
"""
import hashlib
import os
import pickle
from pathlib import Path
import numpy as np
from openpilot.common.params import Params
from cereal import custom
from openpilot.sunnypilot.models.constants import Meta, MetaTombRaider, MetaSimPose
from openpilot.common.params import Params
from openpilot.common.swaglog import cloudlog
from openpilot.sunnypilot.models.constants import Meta, MetaSimPose, MetaTombRaider
from openpilot.system.hardware.hw import Paths
from pathlib import Path
# see the README.md for more details on the model selector versioning
CURRENT_SELECTOR_VERSION = 15
REQUIRED_MIN_SELECTOR_VERSION = 14
# SET ME TO THE EXACT JSON VERSION WE SET IN SUNNYPILOT_MODELS REPO
REQUIRED_JSON_VERSION = 15
CUSTOM_MODEL_PATH = Paths.model_root()
METADATA_PATH = Path(__file__).parent / '../models/supercombo_metadata.pkl'
ModelManager = custom.ModelManagerSP
_LAST_VALIDATED_RAW = None
def _compute_hash(file_path: str) -> str | None:
from openpilot.common.file_chunker import read_file_chunked
try:
return hashlib.sha256(read_file_chunked(file_path)).hexdigest().lower()
except FileNotFoundError:
return None
async def verify_file(file_path: str, expected_hash: str) -> bool:
from openpilot.common.file_chunker import read_file_chunked
try:
data = read_file_chunked(file_path)
except FileNotFoundError:
return False
return hashlib.sha256(data).hexdigest().lower() == expected_hash.lower()
file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False
def _verify_file(file_path: str, expected_hash: str) -> bool:
file_hash = _compute_hash(file_path)
return file_hash == expected_hash.lower() if file_hash else False
def is_bundle_version_compatible(bundle: dict) -> bool:
"""
Checks whether the model bundle is compatible with the current selector version constraints.
The bundle specifies a `minimum_selector_version`, which defines the minimum selector version
The bundle parsed from the json specifies a `minimum_selector_version`, which defines the minimum selector version
required to load the model. This function ensures that:
1. The model is not too old: the bundle must require at least `REQUIRED_MIN_SELECTOR_VERSION`.
2. The model is not too new: it must support the current selector version (`CURRENT_SELECTOR_VERSION`).
This allows the selector to enforce both a minimum and maximum range of supported models,
even if a model would otherwise be compatible.
:param bundle: Dictionary containing `minimum_selector_version`, as defined by the model bundle.
:type bundle: Dict
:return: True if the selector version is within the accepted range for the bundle; otherwise False.
:rtype: Bool
the bundle MUST match the `REQUIRED_JSON_VERSION` set here in helpers.
"""
return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION)
return bundle.get("minimumSelectorVersion", 0) == REQUIRED_JSON_VERSION
def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle:
"""Gets the active model bundle from cache"""
if params is None:
params = Params()
def _bundle_artifacts(bundle: custom.ModelManagerSP.ModelBundle) -> list[tuple[str, str]]:
artifacts = []
for model in getattr(bundle, 'models', []) or []:
for artifact in (getattr(model, 'artifact', None), getattr(model, 'metadata', None)):
if artifact and getattr(artifact, 'fileName', None) and getattr(artifact, 'downloadUri', None):
sha256 = getattr(artifact.downloadUri, 'sha256', None)
if sha256:
artifacts.append((artifact.fileName, sha256))
return artifacts
def _bundle_is_valid_locally(bundle: custom.ModelManagerSP.ModelBundle) -> bool:
model_root = Paths.model_root()
return all(_verify_file(os.path.join(model_root, file_name), expected_hash)
for file_name, expected_hash in _bundle_artifacts(bundle))
def _bundle_needs_reset(active_bundle: custom.ModelManagerSP.ModelBundle, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None) -> bool:
if active_bundle is None:
return False
if available_bundles is not None:
matching_bundle = None
for bundle in available_bundles:
if getattr(active_bundle, 'ref', None) and getattr(bundle, 'ref', None):
if active_bundle.ref == bundle.ref:
matching_bundle = bundle
break
elif getattr(active_bundle, 'internalName', None) == getattr(bundle, 'internalName', None):
matching_bundle = bundle
break
if matching_bundle is None:
return True
if active_bundle.minimumSelectorVersion != matching_bundle.minimumSelectorVersion:
return True
active_runner = getattr(active_bundle, 'runner', None)
matching_runner = getattr(matching_bundle, 'runner', None)
if active_runner is not None and matching_runner is not None:
if getattr(active_runner, 'raw', active_runner) != getattr(matching_runner, 'raw', matching_runner):
return True
if set(_bundle_artifacts(active_bundle)) != set(_bundle_artifacts(matching_bundle)):
return True
return not _bundle_is_valid_locally(active_bundle)
def validate_active_bundle(params: Params, available_bundles: list[custom.ModelManagerSP.ModelBundle] | None = None) -> None:
global _LAST_VALIDATED_RAW
raw_bundle = params.get("ModelManager_ActiveBundle")
if not raw_bundle:
return
if raw_bundle == _LAST_VALIDATED_RAW:
return
active_bundle = get_active_bundle(params, raw_bundle_dict=raw_bundle)
if active_bundle is None or _bundle_needs_reset(active_bundle, available_bundles):
cloudlog.warning("Active model bundle invalid; resetting to default")
params.remove("ModelManager_ActiveBundle")
params.put("ModelRunnerTypeCache", int(custom.ModelManagerSP.Runner.stock), block=True)
_LAST_VALIDATED_RAW = None
else:
_LAST_VALIDATED_RAW = raw_bundle
def get_active_bundle(params: Params | None = None, raw_bundle_dict: dict | bytes | None = None) -> "custom.ModelManagerSP.ModelBundle | None":
params = params or Params()
try:
if (active_bundle := params.get("ModelManager_ActiveBundle") or {}) and is_bundle_version_compatible(active_bundle):
return custom.ModelManagerSP.ModelBundle(**active_bundle)
active_bundle_dict = raw_bundle_dict if raw_bundle_dict is not None else (params.get("ModelManager_ActiveBundle") or {})
if active_bundle_dict and is_bundle_version_compatible(active_bundle_dict):
return custom.ModelManagerSP.ModelBundle(**active_bundle_dict)
except Exception:
pass
return None
def get_active_model_runner(params: Params = None, force_check=False) -> int:
if params is None:
params = Params()
def get_active_model_runner(params: Params | None = None, force_check: bool = False) -> int:
params = params or Params()
cached_runner_type = params.get("ModelRunnerTypeCache")
if cached_runner_type is not None and not force_check:
return cached_runner_type
runner_type = custom.ModelManagerSP.Runner.stock
if active_bundle := get_active_bundle(params):
runner_type = active_bundle.runner.raw
@@ -88,66 +146,40 @@ def get_active_model_runner(params: Params = None, force_check=False) -> int:
return runner_type
def _get_model():
if bundle := get_active_bundle():
drive_model = next(model for model in bundle.models if model.type == ModelManager.Model.Type.supercombo)
return drive_model
return None
def load_metadata():
metadata_path = METADATA_PATH
if model := _get_model():
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}"
def load_metadata():
model = _get_model()
metadata_path = f"{CUSTOM_MODEL_PATH}/{model.metadata.fileName}" if model else METADATA_PATH
with open(metadata_path, 'rb') as f:
return pickle.load(f)
def prepare_inputs(model_metadata) -> dict[str, np.ndarray]:
# img buffers are managed in openCL transform code so we don't pass them as inputs
inputs = {
k: np.zeros(v, dtype=np.float32).flatten()
for k, v in model_metadata['input_shapes'].items()
if 'img' not in k
def prepare_inputs(model_metadata: dict) -> dict[str, np.ndarray]:
return {
key: np.zeros(shape, dtype=np.float32).flatten()
for key, shape in model_metadata['input_shapes'].items()
if 'img' not in key
}
return inputs
def load_meta_constants(model_metadata: dict):
""" Loads the appropriate meta model class based on key shapes"""
if 'sim_pose' in model_metadata['input_shapes']:
return MetaSimPose
def load_meta_constants(model_metadata):
"""
Determines and loads the appropriate meta model class based on the metadata provided. The function checks
specific keys and conditions within the provided metadata dictionary to identify the corresponding meta
model class to return.
meta_slice = model_metadata['output_slices']['meta']
if (meta_slice.start, meta_slice.stop, meta_slice.step) == (5868, 5921, None):
return MetaTombRaider
:param model_metadata: Dictionary containing metadata about the model. It includes
details such as input shapes, output slices, and other configurations for identifying
metadata-dependent meta model classes.
:type model_metadata: dict
:return: The appropriate meta model class (Meta, MetaSimPose, or MetaTombRaider)
based on the conditions and metadata provided.
:rtype: type
"""
meta = Meta # Default Meta
if 'sim_pose' in model_metadata['input_shapes'].keys():
# Meta for models with sim_pose input
meta = MetaSimPose
else:
# Meta for Tomb Raider, it does not include sim_pose input but has the same meta slice as previous models
meta_slice = model_metadata['output_slices']['meta']
meta_tf_slice = slice(5868, 5921, None)
if (
meta_slice.start == meta_tf_slice.start and
meta_slice.stop == meta_tf_slice.stop and
meta_slice.step == meta_tf_slice.step
):
meta = MetaTombRaider
return meta
return Meta
# The following method(s) are modeld helper methods
+4 -3
View File
@@ -17,7 +17,7 @@ from openpilot.system.hardware.hw import Paths
from cereal import messaging, custom
from openpilot.sunnypilot.models.fetcher import ModelFetcher
from openpilot.sunnypilot.models.helpers import verify_file, get_active_bundle
from openpilot.sunnypilot.models.helpers import get_active_bundle, validate_active_bundle, verify_file
class ModelManagerSP:
@@ -239,6 +239,7 @@ class ModelManagerSP:
while True:
try:
self.available_models = self.model_fetcher.get_available_bundles()
validate_active_bundle(self.params, self.available_models)
self.active_bundle = get_active_bundle(self.params)
if (index_to_download := self.params.get("ModelManager_DownloadIndex")) is not None:
@@ -252,8 +253,8 @@ class ModelManagerSP:
self.selected_bundle = None
if self.params.get("ModelManager_ClearCache"):
self.clear_model_cache()
self.params.remove("ModelManager_ClearCache")
self.clear_model_cache()
self.params.remove("ModelManager_ClearCache")
self._report_status()
rk.keep_time()
+1
View File
@@ -112,6 +112,7 @@ def initialize_params(params) -> list[dict[str, Any]]:
# hyundai
keys.extend([
"HyundaiLongitudinalTuning",
"HyundaiRadar",
])
# subaru
-34
View File
@@ -41,7 +41,6 @@ LOCAL_PORT_WHITELIST = {8022}
SUNNYLINK_LOG_ATTR_NAME = "user.sunny.upload"
SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require a change on sidebar.cc
DISALLOW_LOG_UPLOAD = threading.Event()
METADATA_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "params_metadata.json")
params = Params()
@@ -170,39 +169,6 @@ def getParamsAllKeys() -> list[str]:
return keys
@dispatcher.add_method
def getParamsAllKeysV1() -> dict[str, str]:
try:
with open(METADATA_PATH) as f:
metadata = json.load(f)
except Exception:
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.metadata.exception")
metadata = {}
try:
available_keys: list[str] = [k.decode('utf-8') for k in Params().all_keys()]
params_dict: dict[str, list[dict[str, str | bool | int | object | dict | None]]] = {"params": []}
for key in available_keys:
value = get_param_as_byte(key, get_default=True)
param_entry = {
"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:
meta_copy = metadata[key].copy()
param_entry["_extra"] = meta_copy
params_dict["params"].append(param_entry)
return {"keys": json.dumps(params_dict.get("params", []))}
except Exception:
cloudlog.exception("sunnylinkd.getParamsAllKeysV1.exception")
raise
@dispatcher.add_method
def getParamsMetadata() -> str:
"""Return settings_ui.json + live capabilities as gzip-compressed, base64-encoded string.
+5 -6
View File
@@ -97,12 +97,11 @@ The compiler splices a list-context `$ref` into its parent list. Macros may refe
```
1. common/params_keys.h — add/remove the C++ param key
2. params_metadata.json — automated via update_params_metadata.py
3. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
4. python sunnypilot/sunnylink/tools/compile_settings_ui.py
5. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
6. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
7. commit
2. settings_ui_src/pages/<page>.yaml — add/edit/remove the item in the right section
3. python sunnypilot/sunnylink/tools/compile_settings_ui.py
4. python sunnypilot/sunnylink/tools/validate_settings_ui.py (or: --check on the compiler)
5. uv run python -m pytest sunnypilot/sunnylink/tests/ # run regression + compiler tests
6. commit
```
CI runs `compile_settings_ui.py --check` to fail on hand-edited `settings_ui.json`.
File diff suppressed because it is too large Load Diff
+6
View File
@@ -1296,6 +1296,12 @@
"title": "Display Turn Signals",
"description": "When enabled, visual turn indicators are drawn on the HUD."
},
{
"key": "RadarTracks",
"widget": "toggle",
"title": "Radar Tracks",
"description": "Show radar tracks"
},
{
"key": "RoadNameToggle",
"widget": "toggle",
@@ -24,6 +24,10 @@ sections:
widget: toggle
title: Display Turn Signals
description: When enabled, visual turn indicators are drawn on the HUD.
- key: RadarTracks
widget: toggle
title: Radar Tracks
description: Show radar tracks
- key: RoadNameToggle
widget: toggle
title: Display Road Name
@@ -1,86 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import getParamsAllKeysV1, METADATA_PATH
def test_get_params_all_keys_v1():
"""
Test the getParamsAllKeysV1 API endpoint.
Why:
This endpoint is used by the UI (and potentially external tools) to fetch the list of
available parameters along with their metadata (titles, descriptions, options, constraints).
We need to ensure it returns the correct structure and that the metadata from
params_metadata.json is correctly merged into the response.
Expected:
- The response should contain a "keys" field which is a JSON string of a list of parameters.
- Each parameter object should have "key", "type", "default_value", and optionally "_extra".
- The "_extra" field should contain the rich metadata (title, options, min/max, etc.) matching
the source of truth (params_metadata.json).
"""
response = getParamsAllKeysV1()
assert "keys" in response
keys_json = response["keys"]
params_list = json.loads(keys_json)
assert isinstance(params_list, list)
assert len(params_list) > 0
# Check structure of first item
first_param = params_list[0]
assert "key" in first_param
assert "type" in first_param
assert "default_value" in first_param
if "_extra" in first_param:
assert isinstance(first_param["_extra"], dict)
assert "default" not in first_param["_extra"]
assert "type" not in first_param["_extra"]
# Load the source of truth
with open(METADATA_PATH) as f:
metadata = json.load(f)
# Verify that the API response matches the metadata file for a few sample keys
# This ensures the plumbing is working without being brittle to content changes
# 1. Check a key that should have metadata
keys_with_metadata = [k for k in params_list if k["key"] in metadata]
assert len(keys_with_metadata) > 0, "No parameters found that match metadata keys"
for param in keys_with_metadata[:5]: # Check first 5 matches
key = param["key"]
expected_meta = metadata[key]
assert "_extra" in param, f"Parameter {key} should have _extra field"
actual_meta = param["_extra"]
# Verify all fields in JSON are present in the API response
for meta_key, meta_val in expected_meta.items():
assert meta_key in actual_meta, f"Missing {meta_key} in API response for {key}"
assert actual_meta[meta_key] == meta_val, f"Mismatch for {key}.{meta_key}: expected {meta_val}, got {actual_meta[meta_key]}"
# 2. Check that we are correctly serving options if they exist
params_with_options = [k for k in keys_with_metadata if "options" in k.get("_extra", {})]
if params_with_options:
param = params_with_options[0]
key = param["key"]
assert isinstance(param["_extra"]["options"], list), f"Options for {key} should be a list"
assert param["_extra"]["options"] == metadata[key]["options"]
# 3. Check that we are correctly serving numeric constraints if they exist
params_with_constraints = [k for k in keys_with_metadata if "min" in k.get("_extra", {})]
if params_with_constraints:
param = params_with_constraints[0]
key = param["key"]
assert param["_extra"]["min"] == metadata[key]["min"]
assert param["_extra"]["max"] == metadata[key]["max"]
assert param["_extra"]["step"] == metadata[key]["step"]
@@ -1,284 +0,0 @@
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import os
import pytest
from openpilot.common.params import Params
from openpilot.sunnypilot.sunnylink.athena.sunnylinkd import METADATA_PATH
def test_metadata_json_exists():
"""
Test that the params_metadata.json file exists at the expected path.
Why:
The metadata file is the source of truth for parameter descriptions, options, and constraints.
If it's missing, the UI will not be able to display rich information for parameters.
Expected:
The file should exist at sunnypilot/sunnylink/params_metadata.json.
"""
assert os.path.exists(METADATA_PATH), f"Metadata file not found at {METADATA_PATH}"
def test_metadata_json_valid():
"""
Test that the params_metadata.json file contains valid JSON.
Why:
Invalid JSON will cause the metadata loading to fail, potentially crashing the UI or
resulting in missing metadata.
Expected:
The file content should be parseable as a JSON object (dictionary).
"""
with open(METADATA_PATH) as f:
try:
data = json.load(f)
except json.JSONDecodeError:
pytest.fail("Metadata file is not valid JSON")
assert isinstance(data, dict), "Metadata root must be a dictionary"
def test_all_params_have_metadata():
"""
Test that every parameter in the codebase has a corresponding entry in params_metadata.json.
Why:
We want to ensure 100% coverage of parameter metadata. Any parameter added to the codebase
should also be documented in the metadata file.
Expected:
There should be no parameters in Params() that are missing from the metadata file.
If this fails, run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py'.
"""
params = Params()
all_keys = [k.decode('utf-8') for k in params.all_keys()]
with open(METADATA_PATH) as f:
metadata = json.load(f)
missing_keys = [key for key in all_keys if key not in metadata]
if missing_keys:
pytest.fail(
f"The following parameters are missing from metadata: {missing_keys}. "
+ "Please run 'python3 sunnypilot/sunnylink/tools/update_params_metadata.py' to update."
)
def test_metadata_keys_exist_in_params():
"""
Test that all keys in params_metadata.json actually exist in the codebase.
Why:
We want to avoid stale metadata for parameters that have been removed or renamed.
This keeps the metadata file clean and relevant.
Expected:
There should be no keys in the metadata file that are not present in Params().
This prints a warning rather than failing, as it's less critical than missing metadata.
"""
params = Params()
all_keys = {k.decode('utf-8') for k in params.all_keys()}
with open(METADATA_PATH) as f:
metadata = json.load(f)
extra_keys = [key for key in metadata.keys() if key not in all_keys]
if extra_keys:
print(f"Warning: The following keys in metadata do not exist in Params: {extra_keys}")
def test_no_default_titles():
"""
Test that no parameter has a title that is identical to its key.
Why:
The default behavior of the update script is to set the title equal to the key.
We want to force developers to provide human-readable, descriptive titles for all parameters.
Expected:
No parameter metadata should have 'title' == 'key'.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
default_title_keys = [key for key, meta in metadata.items() if meta.get("title") == key]
if default_title_keys:
pytest.fail(
f"The following parameters have default titles (title == key): {default_title_keys}. "
+ "Please update 'params_metadata.json' with descriptive titles."
)
def test_options_structure():
"""
Test that the 'options' field in metadata follows the correct structure.
Why:
The UI expects 'options' to be a list of objects with 'value' and 'label' keys.
Incorrect structure will break the UI rendering for dropdowns/toggles.
Expected:
If 'options' is present, it must be a list of dicts, and each dict must have 'value' and 'label'.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
for key, meta in metadata.items():
if "options" in meta:
options = meta["options"]
assert isinstance(options, list), f"Options for {key} must be a list"
for option in options:
assert isinstance(option, dict), f"Option in {key} must be a dictionary"
assert "value" in option, f"Option in {key} must have a 'value' key"
assert "label" in option, f"Option in {key} must have a 'label' key"
def test_numeric_constraints():
"""
Test that numeric parameters have valid 'min', 'max', and 'step' constraints.
Why:
The UI uses these constraints to validate user input and render sliders/steppers.
Missing or invalid constraints can lead to UI bugs or invalid parameter values.
Expected:
If any of min/max/step is present, ALL of them must be present.
They must be numbers (int/float), and min must be less than max.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
for key, meta in metadata.items():
if "min" in meta or "max" in meta or "step" in meta:
assert "min" in meta, f"Numeric param {key} must have 'min'"
assert "max" in meta, f"Numeric param {key} must have 'max'"
assert "step" in meta, f"Numeric param {key} must have 'step'"
assert isinstance(meta["min"], (int, float)), f"Min for {key} must be number"
assert isinstance(meta["max"], (int, float)), f"Max for {key} must be number"
assert isinstance(meta["step"], (int, float)), f"Step for {key} must be number"
assert meta["min"] < meta["max"], f"Min must be less than max for {key}"
def test_known_params_metadata():
"""
Test specific known parameters to ensure they have the expected rich metadata.
Why:
This acts as a spot check to ensure that our rich metadata population logic is working correctly
and that critical parameters (like LongitudinalPersonality) have their options and constraints preserved.
Expected:
'LongitudinalPersonality' should have 3 options (Aggressive, Standard, Relaxed).
'CustomAccLongPressIncrement' should have min=1, max=10, step=1.
"""
with open(METADATA_PATH) as f:
metadata = json.load(f)
# Check an enum-like param
lp = metadata.get("LongitudinalPersonality")
assert lp is not None
assert "options" in lp
assert len(lp["options"]) == 3
assert lp["options"][0]["label"] == "Aggressive"
assert lp["options"][0]["value"] == 0
# Check a numeric param
acc_long = metadata.get("CustomAccLongPressIncrement")
assert acc_long is not None
assert acc_long["min"] == 1
assert acc_long["max"] == 10
assert acc_long["step"] == 1
def test_torque_control_tune_versions_in_sync():
"""
Test that TorqueControlTune options in params_metadata.json match versions in latcontrol_torque_versions.json.
Why:
The TorqueControlTune dropdown in the UI should always reflect the available torque tune versions.
If versions are added/removed from latcontrol_torque_versions.json, the metadata must be updated accordingly.
Expected:
- TorqueControlTune should have a 'Default' option with empty string value
- All versions from latcontrol_torque_versions.json should be present in the options
- The version values and labels should match between both files
"""
from openpilot.common.basedir import BASEDIR
versions_json_path = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
sync_script_path = "python3 sunnypilot/sunnylink/tools/sync_torque_versions.py"
# Load both files
with open(METADATA_PATH) as f:
metadata = json.load(f)
with open(versions_json_path) as f:
versions = json.load(f)
# Get TorqueControlTune metadata
torque_tune = metadata.get("TorqueControlTune")
if torque_tune is None:
pytest.fail(f"TorqueControlTune not found in params_metadata.json. Please run '{sync_script_path}' to sync.")
if "options" not in torque_tune:
pytest.fail(f"TorqueControlTune must have options. Please run '{sync_script_path}' to sync.")
options = torque_tune["options"]
if not isinstance(options, list):
pytest.fail(f"TorqueControlTune options must be a list. Please run '{sync_script_path}' to sync.")
if len(options) == 0:
pytest.fail(f"TorqueControlTune must have at least one option. Please run '{sync_script_path}' to sync.")
# Check that Default option exists
default_option = next((opt for opt in options if opt.get("value") == ""), None)
if default_option is None:
pytest.fail(f"TorqueControlTune must have a 'Default' option with empty string value. Please run '{sync_script_path}' to sync.")
if default_option.get("label") != "Default":
pytest.fail(f"Default option must have label 'Default'. Please run '{sync_script_path}' to sync.")
# Build expected options from versions.json
expected_version_keys = set(versions.keys())
actual_version_keys = set()
for option in options:
if option.get("value") == "":
continue # Skip the default option
label = option.get("label")
value = option.get("value")
# Check that this option corresponds to a version
if label not in versions:
pytest.fail(f"Option label '{label}' not found in latcontrol_torque_versions.json. Please run '{sync_script_path}' to sync.")
# Check that the value matches the version number
expected_value = float(versions[label]["version"])
if value != expected_value:
pytest.fail(f"Option '{label}' has value {value}, expected {expected_value}. Please run '{sync_script_path}' to sync.")
actual_version_keys.add(label)
# Check that all versions are represented
missing_versions = expected_version_keys - actual_version_keys
if missing_versions:
pytest.fail(f"The following versions are missing from TorqueControlTune options: {missing_versions}. " +
f"Please run '{sync_script_path}' to sync.")
extra_versions = actual_version_keys - expected_version_keys
if extra_versions:
pytest.fail("The following versions in TorqueControlTune options are not in latcontrol_torque_versions.json: " +
f"{extra_versions}. Please run '{sync_script_path}' to sync.")
@@ -1,133 +0,0 @@
#!/usr/bin/env python3
"""
Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
This file is part of sunnypilot and is licensed under the MIT License.
See the LICENSE.md file in the root directory for more details.
"""
import json
import os
from openpilot.common.basedir import BASEDIR
from openpilot.common.params import Params
from openpilot.sunnypilot.system.params_migration import ONROAD_BRIGHTNESS_TIMER_VALUES
METADATA_PATH = os.path.join(os.path.dirname(__file__), "../params_metadata.json")
TORQUE_VERSIONS_JSON = os.path.join(BASEDIR, "sunnypilot", "selfdrive", "controls", "lib", "latcontrol_torque_versions.json")
def main():
params = Params()
all_keys = params.all_keys()
if os.path.exists(METADATA_PATH):
with open(METADATA_PATH) as f:
try:
data = json.load(f)
except json.JSONDecodeError:
data = {}
else:
data = {}
# Add new keys
for key in all_keys:
key_str = key.decode("utf-8")
if key_str not in data:
print(f"Adding new key: {key_str}")
data[key_str] = {
"title": key_str,
"description": "",
}
# Remove deleted keys
# keys_to_remove = [k for k in data.keys() if k.encode("utf-8") not in all_keys]
# for k in keys_to_remove:
# print(f"Removing deleted key: {k}")
# del data[k]
# Sort keys
sorted_data = dict(sorted(data.items()))
with open(METADATA_PATH, "w") as f:
json.dump(sorted_data, f, indent=2)
f.write("\n")
print(f"Updated {METADATA_PATH}")
# update onroad screen brightness params
update_onroad_brightness_param()
# update onroad screen brightness timer params
update_onroad_brightness_timer_param()
# update torque versions param
update_torque_versions_param()
def update_onroad_brightness_param():
try:
with open(METADATA_PATH) as f:
params_metadata = json.load(f)
if "OnroadScreenOffBrightness" in params_metadata:
options = [
{"value": 0, "label": "Auto (Default)"},
{"value": 1, "label": "Auto (Dark)"},
{"value": 2, "label": "Screen Off"},
]
for i in range(3, 23):
options.append({"value": i, "label": f"{(i - 2) * 5} %"})
params_metadata["OnroadScreenOffBrightness"]["options"] = options
with open(METADATA_PATH, 'w') as f:
json.dump(params_metadata, f, indent=2)
f.write('\n')
print(f"Updated OnroadScreenOffBrightness options in params_metadata.json with {len(options)} options.")
except Exception as e:
print(f"Failed to update OnroadScreenOffBrightness versions in params_metadata.json: {e}")
def update_onroad_brightness_timer_param():
try:
with open(METADATA_PATH) as f:
params_metadata = json.load(f)
if "OnroadScreenOffTimer" in params_metadata:
options = []
for _index, seconds in sorted(ONROAD_BRIGHTNESS_TIMER_VALUES.items()):
label = f"{seconds}s" if seconds < 60 else f"{seconds // 60}m"
options.append({"value": seconds, "label": label})
params_metadata["OnroadScreenOffTimer"]["options"] = options
with open(METADATA_PATH, 'w') as f:
json.dump(params_metadata, f, indent=2)
f.write('\n')
print(f"Updated OnroadScreenOffTimer options in params_metadata.json with {len(options)} options.")
except Exception as e:
print(f"Failed to update OnroadScreenOffTimer options in params_metadata.json: {e}")
def update_torque_versions_param():
with open(TORQUE_VERSIONS_JSON) as f:
current_versions = json.load(f)
try:
with open(METADATA_PATH) as f:
params_metadata = json.load(f)
options = [{"value": "", "label": "Default"}]
for version_key, version_data in current_versions.items():
version_value = float(version_data["version"])
options.append({"value": version_value, "label": str(version_key)})
if "TorqueControlTune" in params_metadata:
params_metadata["TorqueControlTune"]["options"] = options
with open(METADATA_PATH, 'w') as f:
json.dump(params_metadata, f, indent=2)
f.write('\n')
print(f"Updated TorqueControlTune options in params_metadata.json with {len(options)} options: \n{options}")
except Exception as e:
print(f"Failed to update TorqueControlTune versions in params_metadata.json: {e}")
if __name__ == "__main__":
main()
+343
View File
@@ -0,0 +1,343 @@
#!/usr/bin/env python3
import argparse
import concurrent.futures
import os
import signal
import subprocess
import sys
import time
from collections import Counter, defaultdict
import cereal.messaging as messaging
from msgq.ipc_pyx import IpcError
from opendbc.car.tests.routes import CarTestRoute, routes
from openpilot.tools.replay.custom_routes import CUSTOM_ROUTES
from openpilot.tools.replay.radar_helpers import (
build_seen_address_map,
get_radar_spec,
is_exclusive_full_range_match,
)
REPLAY_PATH = os.path.join(os.path.dirname(__file__), "replay")
REPLAY_PLAYBACK_SPEED = "3.0"
REPLAY_ALLOW_SERVICES = "can"
DETECTION_TIMEOUT_SECONDS = 20.0
DETECTION_MIN_HITS = 10
SOCKET_WAIT_TIMEOUT_SECONDS = 10.0
CAR_MODEL_WIDTH = 38
RADAR_TYPE_WIDTH = 10
ANSI_RESET = "\033[0m"
ANSI_BOLD = "\033[1m"
ANSI_DIM = "\033[2m"
ANSI_RED = "\033[31m"
ANSI_GREEN = "\033[32m"
ANSI_YELLOW = "\033[33m"
ANSI_BLUE = "\033[34m"
ANSI_MAGENTA = "\033[35m"
ANSI_CYAN = "\033[36m"
RADAR_FAMILY_COLORS = {
"RADAR_500_51F": ANSI_CYAN,
"RADAR_210_21F": ANSI_YELLOW,
"RADAR_3A5_3C4": ANSI_GREEN,
"RADAR_602_611": ANSI_BLUE,
}
def build_arg_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Replay Hyundai/Kia/Genesis test routes and auto-detect radar type.",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument("--routes", action="store_true",
help="Use Hyundai/Kia/Genesis routes from opendbc/car/tests/routes.py.")
parser.add_argument("--custom-routes", action="store_true",
help="Use routes from tools/replay/custom_routes.py.")
parser.add_argument("--route", action="append", default=[],
help="Specific route(s) to test. If omitted, use Hyundai/Kia/Genesis routes from opendbc/car/tests/routes.py.")
parser.add_argument("--data-dir", default=None,
help="Optional local directory of route data to pass through to replay.")
parser.add_argument("--timeout", type=float, default=DETECTION_TIMEOUT_SECONDS,
help="Seconds to wait for a radar-family detection before giving up on a route.")
parser.add_argument("--min-hits", type=int, default=DETECTION_MIN_HITS,
help="Minimum CAN hits in a radar-family address range before considering it detected.")
parser.add_argument("--playback", default=REPLAY_PLAYBACK_SPEED,
help="Replay playback speed.")
parser.add_argument("--limit", type=int, default=None,
help="Only process the first N matching routes.")
parser.add_argument("--jobs", type=int, default=1,
help="Number of routes to process in parallel.")
parser.add_argument("--prefix", default="auto-radar-detect",
help="Base OPENPILOT_PREFIX to isolate replay sockets.")
return parser
def get_hkg_routes() -> list[CarTestRoute]:
hkg_prefixes = ("HYUNDAI_", "KIA_", "GENESIS_")
return [
route for route in routes
if route.car_model is not None and getattr(route.car_model, "name", str(route.car_model)).startswith(hkg_prefixes)
]
def get_selected_routes(args: argparse.Namespace) -> list[CarTestRoute]:
selected_sources = int(bool(args.route)) + int(bool(args.routes)) + int(bool(args.custom_routes))
if selected_sources > 1:
raise ValueError("Use only one of --route, --routes, or --custom-routes.")
if args.route:
route_map = {route.route: route for route in get_hkg_routes()}
selected = []
for route_name in args.route:
selected.append(route_map.get(route_name, CarTestRoute(route_name, None)))
return selected[:args.limit] if args.limit is not None else selected
if args.custom_routes:
selected = [CarTestRoute(route.route, route.car_model) for route in CUSTOM_ROUTES]
return selected[:args.limit] if args.limit is not None else selected
selected = get_hkg_routes()
return selected[:args.limit] if args.limit is not None else selected
def terminate_process(proc: subprocess.Popen) -> None:
if proc.poll() is not None:
return
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
proc.kill()
proc.wait(timeout=5)
def start_replay(route: str, prefix: str, args: argparse.Namespace) -> subprocess.Popen:
cmd = [
REPLAY_PATH,
"--no-vipc",
"--no-loop",
"--playback", args.playback,
"--allow", REPLAY_ALLOW_SERVICES,
"--prefix", prefix,
]
if args.data_dir:
cmd.extend(["--data_dir", args.data_dir])
cmd.append(route)
env = os.environ.copy()
env["OPENPILOT_PREFIX"] = prefix
return subprocess.Popen(
cmd,
cwd=os.path.dirname(REPLAY_PATH),
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid if os.name != "nt" else None,
)
def stop_replay(proc: subprocess.Popen) -> None:
if proc.poll() is not None:
return
if os.name != "nt":
try:
os.killpg(proc.pid, signal.SIGTERM)
except ProcessLookupError:
return
else:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
if os.name != "nt":
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
return
else:
proc.kill()
proc.wait(timeout=5)
def wait_for_can_socket(prefix: str, timeout: float) -> messaging.SubSocket:
socket_path = os.path.join("/tmp", f"msgq_{prefix}", "can")
started = time.monotonic()
while True:
if os.path.exists(socket_path):
break
if time.monotonic() - started > timeout:
raise TimeoutError(f"Timed out waiting for CAN socket at {socket_path}")
time.sleep(0.1)
while True:
try:
return messaging.sub_sock("can", conflate=False, timeout=100)
except IpcError:
if time.monotonic() - started > timeout:
raise
time.sleep(0.1)
def detect_radar_family(route: CarTestRoute, route_idx: int, args: argparse.Namespace) -> dict:
prefix = f"{args.prefix}-{os.getpid()}-{route_idx}"
os.environ["OPENPILOT_PREFIX"] = prefix
messaging.reset_context()
proc = start_replay(route.route, prefix, args)
logcan = wait_for_can_socket(prefix, min(args.timeout, SOCKET_WAIT_TIMEOUT_SECONDS))
counts = Counter()
buses = defaultdict(set)
seen_addresses = build_seen_address_map()
started = time.monotonic()
detected = None
try:
while True:
if proc.poll() is not None and (time.monotonic() - started) > 1.0:
break
if time.monotonic() - started > args.timeout:
break
msgs = messaging.drain_sock(logcan, wait_for_one=True)
for msg in msgs:
for can_msg in msg.can:
radar_spec = get_radar_spec(can_msg.address)
if radar_spec is not None:
counts[radar_spec.name] += 1
buses[radar_spec.name].add(can_msg.src)
seen_addresses[radar_spec.name].add(can_msg.address)
if counts[radar_spec.name] >= args.min_hits and is_exclusive_full_range_match(radar_spec, seen_addresses):
detected = radar_spec.name
if detected is not None:
break
if detected is not None:
break
if detected is not None:
break
finally:
stop_replay(proc)
dominant_family = detected
return {
"route": route.route,
"car_model": str(route.car_model) if route.car_model is not None else "UNKNOWN",
"detected": dominant_family or "NONE",
"counts": dict(counts),
"buses": {name: sorted(bus_set) for name, bus_set in buses.items()},
"seen_addresses": {name: sorted(addresses) for name, addresses in seen_addresses.items()},
}
def print_results(results: list[dict]) -> None:
print(f"{'Car Model':<{CAR_MODEL_WIDTH}} {'Detected':<{RADAR_TYPE_WIDTH}} Route")
print("-" * (CAR_MODEL_WIDTH + RADAR_TYPE_WIDTH + 9 + 60))
for result in results:
detected_plain = "" if result["detected"] == "NONE" else result["detected"]
model = format_car_model(result["car_model"])[:CAR_MODEL_WIDTH]
detected = f"{detected_plain:<{RADAR_TYPE_WIDTH}}"
if supports_color() and detected_plain:
detected = f"{colorize_detected(detected_plain)}" + " " * max(0, RADAR_TYPE_WIDTH - len(detected_plain))
print(f"{model:<{CAR_MODEL_WIDTH}} {detected} {result['route']}")
def supports_color() -> bool:
return sys.stdout.isatty() and os.getenv("TERM") not in (None, "dumb")
def format_car_model(car_model: object) -> str:
return str(car_model) if car_model is not None else "UNKNOWN"
def colorize_detected(detected: str) -> str:
if detected == "NONE":
return ""
if not supports_color():
return detected
color = RADAR_FAMILY_COLORS.get(detected, ANSI_MAGENTA)
return f"{color}{detected}{ANSI_RESET}"
def print_progress_line(index: int, total: int, route: CarTestRoute, detected: str) -> None:
left = f"[{index}/{total}]"
model = format_car_model(route.car_model)[:CAR_MODEL_WIDTH]
detected_plain = "" if detected == "NONE" else detected
route_text = route.route
if supports_color():
left = f"{ANSI_DIM}{left}{ANSI_RESET}"
model = f"{ANSI_CYAN}{model:<{CAR_MODEL_WIDTH}}{ANSI_RESET}"
detected_text = colorize_detected(detected_plain).ljust(len(colorize_detected(detected_plain)))
route_text = f"{ANSI_DIM}{route_text}{ANSI_RESET}"
else:
model = f"{model:<{CAR_MODEL_WIDTH}}"
detected_text = detected_plain
detected_text = f"{detected_plain:<{RADAR_TYPE_WIDTH}}" if not supports_color() else (
f"{colorize_detected(detected_plain)}" + " " * max(0, RADAR_TYPE_WIDTH - len(detected_plain))
)
print(f"{left} {model} {detected_text} {route_text}")
def main() -> int:
args = build_arg_parser().parse_args(sys.argv[1:])
try:
selected_routes = get_selected_routes(args)
except ValueError as e:
print(str(e), file=sys.stderr)
return 2
if not selected_routes:
print("No matching routes found.", file=sys.stderr)
return 1
results = []
total = len(selected_routes)
jobs = max(1, min(args.jobs, total))
header_model = "Car Model"
header_detected = "Radar"
if supports_color():
header_model = f"{ANSI_BOLD}{header_model:<{CAR_MODEL_WIDTH}}{ANSI_RESET}"
header_detected = f"{ANSI_BOLD}{header_detected:<{RADAR_TYPE_WIDTH}}{ANSI_RESET}"
else:
header_model = f"{header_model:<{CAR_MODEL_WIDTH}}"
header_detected = f"{header_detected:<{RADAR_TYPE_WIDTH}}"
print(f" {header_model} {header_detected} Route")
if jobs == 1:
for idx, route in enumerate(selected_routes):
result = detect_radar_family(route, idx, args)
print_progress_line(idx + 1, total, route, result['detected'])
results.append((idx, result))
else:
with concurrent.futures.ProcessPoolExecutor(max_workers=jobs) as executor:
future_to_job = {
executor.submit(detect_radar_family, route, idx, args): (idx, route)
for idx, route in enumerate(selected_routes)
}
completed = 0
for future in concurrent.futures.as_completed(future_to_job):
idx, route = future_to_job[future]
result = future.result()
completed += 1
print_progress_line(completed, total, route, result['detected'])
results.append((idx, result))
results = sorted(
(result for _, result in results),
key=lambda result: (result["car_model"], result["route"]),
)
print()
print_results(results)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/env python3
from dataclasses import dataclass
@dataclass(frozen=True)
class CustomRoute:
car_model: str
route: str
CUSTOM_ROUTES = [
CustomRoute("HYUNDAI_ELANTRA_HEV_2024", "07a48901db7b2503|0000012d--61aba9f832"),
CustomRoute("HYUNDAI_IONIQ_5", "c5a5f79df9b6a084/0000005d--24c0a7a7ee"),
CustomRoute("HYUNDAI_IONIQ_5", "90950642f47cf05b/00000681--6a904aaa98"),
CustomRoute("HYUNDAI_IONIQ_5", "102161de13e822d1/0000000d--f74a52a624"),
CustomRoute("HYUNDAI_PALISADE_2023", "17ef028fec0ccf81/00000010--dba8455015"),
CustomRoute("HYUNDAI_PALISADE_2023", "6b8052042ba3ee05/00000199--3ed3fb5799"),
CustomRoute("KIA_EV6", "455a0ab75ce5c1e0/000000d1--098f32028c"),
CustomRoute("KIA_EV6", "6d2092783bf67457/0000008c--5caf525813"),
CustomRoute("KIA_EV6", "4961cb0f7bdd77a2/00000105--b636ed2f65"),
CustomRoute("KIA_K8_HEV_1ST_GEN", "78ad5150de133637|2023-09-13--16-15-57"),
CustomRoute("GENESIS_GV60_EV_1ST_GEN", "b1e441e63f1c99dd|0000009d--14cde5aeaf"),
CustomRoute("HYUNDAI_IONIQ_6", "faa815c21279fef7/00000187--ce36ae7c11"),
CustomRoute("HYUNDAI_IONIQ_6", "faa815c21279fef7/00000191--e5c3ffb55b"),
CustomRoute("HYUNDAI_KONA_2ND_GEN", "32025f26789d8fab/00000022--a499e8ffa3"),
CustomRoute("HYUNDAI_KONA_EV_2ND_GEN", "1618132d68afc876/00000021--bf0f957649"),
CustomRoute("HYUNDAI_KONA_HEV_2ND_GEN", "97ca61196eb73e0d/00000052--4555329470"),
CustomRoute("HYUNDAI_SANTA_FE_HEV_5TH_GEN", "d54302f1d5e7a7cc|00000656--5dae9f54a7"),
CustomRoute("HYUNDAI_SONATA_2024", "4267ea8a353cdb36/00000262--8a427003c7"),
CustomRoute("KIA_CARNIVAL_HEV_4TH_GEN_2026", "7b8cc7bb46000e53/0000000b--2abacaff78"),
CustomRoute("KIA_EV6_2025", "48c27f77f9fd1a9b|00000199--193ee1ba20"),
CustomRoute("KIA_EV9", "ccfd4a1af758ee73/00000091--7fa49719a5"),
CustomRoute("KIA_K4_2025", "baf39eeaba1217ca/00000002--b36e3fa031"),
CustomRoute("KIA_K5_2025", "c4a804b067623789/0000007c--163f831540"),
CustomRoute("KIA_NIRO_EV_2ND_GEN", "80b8e9a6ad0acec3/0000032d--8379197bd3"),
CustomRoute("KIA_NIRO_EV_2ND_GEN", "80b8e9a6ad0acec3/0000032c--0055eeee96"),
CustomRoute("KIA_NIRO_HEV_2ND_GEN", "0d4257d1c6741384/00000067--42230e4b8d"),
CustomRoute("HYUNDAI_KONA_EV_2022", "e174e1a0b92263ed/00000003--d0591c14ec"),
CustomRoute("HYUNDAI_KONA_EV_2022", "e174e1a0b92263ed/00000001--18955a967e"),
CustomRoute("KIA_CEED_PHEV", "26064e18b24ae44c/00000000--4eb95fc137"),
CustomRoute("HYUNDAI_IONIQ", "ac27c9808ac91710/00000002--e0aab81e4d"),
CustomRoute("KIA_K7_2017", "74fbff45aa20fe9e/00000010--6f173d5799"),
CustomRoute("KIA_NIRO_EV", "b27d9eafeb61976e/00000233--03a9c0858c"),
CustomRoute("KIA_CARNIVAL_4TH_GEN", "a0cb448c2ffb9383/00000157--ecaf77a801"),
CustomRoute("KIA_CARNIVAL_4TH_GEN", "a0cb448c2ffb9383/00000158--f7e70e1435"),
CustomRoute("KIA_CARNIVAL_4TH_GEN", "a0cb448c2ffb9383/00000154--3471fc4ab3"),
CustomRoute("KIA_SORENTO_4TH_GEN", "bf42073ef09e0af2/00000017--3edc1f8259"),
CustomRoute("KIA_SORENTO_HEV_4TH_GEN", "833262b0c9e4016a/0000004a--0227a058e3"),
CustomRoute("HYUNDAI_PALISADE", "662bedbf8453b81e/00000124--7dc358f20c"),
CustomRoute("HYUNDAI_SONATA", "8f52823c702cb300/00000177--177d1aa6ff"),
CustomRoute("HYUNDAI_SONATA_HYBRID", "e3ae9e987a2a4e57/0000002b--30a6963e88"),
CustomRoute("HYUNDAI_ELANTRA_HEV_2021", "7bc0de9da607c543/00000031--4cb0d8aca5"),
CustomRoute("HYUNDAI_ELANTRA_HEV_2021", "7bc0de9da607c543/00000042--302b371266"),
CustomRoute("HYUNDAI_IONIQ_HEV_2022", "4a975fc1d9e71801/00000005--50d176008a"),
CustomRoute("HYUNDAI_IONIQ_9", "71e4e67d29034771/00000014--25fc6b216f"),
CustomRoute("HYUNDAI_IONIQ_9", "71e4e67d29034771/0000000e--d3b19ad6f6/10"),
CustomRoute("HYUNDAI_SANTA_CRUZ_2025", "6e7904b03a4aafc2/00000010--31034184b6"),
CustomRoute("HYUNDAI_TUCSON_4TH_GEN", "26da1db30eff4fcc/00000061--a60470e363"),
CustomRoute("HYUNDAI_TUCSON_4TH_GEN", "a6d25e95d936fdc4/000002ed--89d7944c52"),
CustomRoute("HYUNDAI_TUCSON_4TH_GEN", "6722665fbbf2a644/00000534--4ab7b733c5"),
CustomRoute("HYUNDAI_TUCSON_HEV_2025", "5868ec006e2bb61e/00000021--7d10df95e8"),
CustomRoute("KIA_SPORTAGE_5TH_GEN", "ce05e32158479f42/000003f4--9543087719"),
CustomRoute("KIA_SPORTAGE_HEV_2026", "343d5e350abaedcf/00000020--1c864750b6"),
]
+209
View File
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
import math
import os
import tempfile
from dataclasses import dataclass
from opendbc.can.parser import CANParser
RADAR_500_51F_DBC_TEMPLATE = """
BO_ {addr_dec} RADAR_TRACK_{addr_hex}: 8 RADAR
SG_ UNKNOWN_1 : 7|8@0- (1,0) [-128|127] "" XXX
SG_ AZIMUTH : 12|10@0- (0.2,0) [-102.4|102.2] "" XXX
SG_ STATE : 15|3@0+ (1,0) [0|7] "" XXX
SG_ LONG_DIST : 18|11@0+ (0.1,0) [0|204.7] "" XXX
SG_ REL_ACCEL : 33|10@0- (0.02,0) [-10.24|10.22] "" XXX
SG_ ZEROS : 37|4@0+ (1,0) [0|255] "" XXX
SG_ COUNTER : 38|1@0+ (1,0) [0|1] "" XXX
SG_ STATE_3 : 39|1@0+ (1,0) [0|1] "" XXX
SG_ REL_SPEED : 53|14@0- (0.01,0) [-81.92|81.92] "" XXX
SG_ STATE_2 : 55|2@0+ (1,0) [0|3] "" XXX
"""
RADAR_3A5_3C4_DBC_TEMPLATE = """
BO_ {addr_dec} RADAR_TRACK_{addr_hex}: 24 RADAR
SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX
SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_1 : 25|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_3 : 28|2@0+ (1,0) [0|3] "" XXX
SG_ COUNTER_3 : 31|2@0+ (1,0) [0|3] "" XXX
SG_ NEW_SIGNAL_2 : 38|7@0- (1,0) [0|127] "" XXX
SG_ COUNTER_256 : 47|8@0+ (1,0) [0|255] "" XXX
SG_ NEW_SIGNAL_6 : 51|4@0+ (1,0) [0|15] "" XXX
SG_ STATE : 54|3@0+ (1,0) [0|7] "" XXX
SG_ NEW_SIGNAL_8 : 62|7@0- (1,0) [0|127] "" XXX
SG_ LONG_DIST : 63|12@1+ (0.05,0) [0|8191] "m" XXX
SG_ LAT_DIST : 76|12@1- (0.05,0) [0|127] "" XXX
SG_ REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX
SG_ NEW_SIGNAL_4 : 103|2@0+ (1,0) [0|3] "" XXX
SG_ LAT_DIST_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX
SG_ REL_ACCEL : 118|10@1- (0.02,0) [0|1023] "" XXX
SG_ NEW_SIGNAL_5 : 133|4@0+ (1,0) [0|15] "" XXX
"""
RADAR_210_21F_DBC_TEMPLATE = """
BO_ {addr_dec} RADAR_TRACK_{addr_hex}: 32 RADAR
SG_ CHECKSUM : 0|16@1+ (1,0) [0|65535] "" XXX
SG_ COUNTER : 16|8@1+ (1,0) [0|255] "" XXX
SG_ 1_COUNTER_255 : 47|8@0+ (1,0) [0|255] "" XXX
SG_ 1_STATE_ALT : 51|4@0+ (1,0) [0|15] "" XXX
SG_ 1_STATE : 55|4@0+ (1,0) [0|15] "" XXX
SG_ 1_NEW_SIGNAL_3 : 63|8@0- (1,0) [0|255] "" XXX
SG_ 1_LONG_DIST : 64|12@1+ (0.05,0) [0|4095] "" XXX
SG_ 1_LAT_DIST : 76|12@1- (0.05,0) [0|4095] "" XXX
SG_ 1_REL_SPEED : 88|14@1- (0.01,0) [0|16383] "" XXX
SG_ 1_NEW_SIGNAL_1 : 102|2@1+ (1,0) [0|3] "" XXX
SG_ 1_LAT_ACCEL : 104|13@1- (1,0) [0|8191] "" XXX
SG_ 1_REL_ACCEL : 118|10@1- (1,0) [0|1023] "" XXX
SG_ 2_COUNTER_255 : 175|8@0+ (1,0) [0|255] "" XXX
SG_ 2_STATE_ALT : 179|4@0+ (1,0) [0|15] "" XXX
SG_ 2_STATE : 183|4@0+ (1,0) [0|15] "" XXX
SG_ 2_NEW_SIGNAL_3 : 191|8@0- (1,0) [0|255] "" XXX
SG_ 2_LONG_DIST : 192|12@1+ (0.05,0) [0|4095] "" XXX
SG_ 2_LAT_DIST : 204|12@1- (0.05,0) [0|4095] "" XXX
SG_ 2_REL_SPEED : 216|14@1- (0.01,0) [0|65535] "" XXX
SG_ 2_NEW_SIGNAL_1 : 230|2@1+ (1,0) [0|3] "" XXX
SG_ 2_LAT_ACCEL : 232|13@1- (1,0) [0|8191] "" XXX
SG_ 2_REL_ACCEL : 246|10@1- (1,0) [0|1023] "" XXX
"""
RADAR_602_611_DBC_TEMPLATE = """
BO_ {addr_dec} RADAR_TRACK_{addr_hex}: 8 RADAR
SG_ 1_DISTANCE : 0|10@1+ (0.25,0) [0|255.75] "" XXX
SG_ 1_LATERAL : 10|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX
SG_ 1_SPEED : 21|10@1+ (0.25,-128) [-128|127.75] "" XXX
SG_ 2_DISTANCE : 31|10@1+ (0.25,0) [0|255.75] "" XXX
SG_ 2_LATERAL : 41|11@1+ (0.03,-30.705) [-30.705|30.705] "" XXX
SG_ 2_SPEED : 52|10@1+ (0.25,-128) [-128|127.75] "" XXX
SG_ COUNTER : 62|2@1+ (1,0) [0|3] "" XXX
"""
@dataclass(frozen=True)
class RadarSpec:
name: str
start_addr: int
msg_count: int
dbc_template: str
track_prefixes: tuple[str, ...]
@property
def end_addr(self) -> int:
return self.start_addr + self.msg_count - 1
def contains(self, address: int) -> bool:
return self.start_addr <= address <= self.end_addr
RADAR_SPECS = (
RadarSpec("RADAR_500_51F", 0x500, 32, RADAR_500_51F_DBC_TEMPLATE, ("",)),
RadarSpec("RADAR_210_21F", 0x210, 16, RADAR_210_21F_DBC_TEMPLATE, ("1_", "2_")),
RadarSpec("RADAR_3A5_3C4", 0x3A5, 32, RADAR_3A5_3C4_DBC_TEMPLATE, ("",)),
RadarSpec("RADAR_602_611", 0x602, 16, RADAR_602_611_DBC_TEMPLATE, ("1_", "2_")),
)
def build_seen_address_map() -> dict[str, set[int]]:
return {radar_spec.name: set() for radar_spec in RADAR_SPECS}
def get_radar_spec(address: int) -> RadarSpec | None:
for radar_spec in RADAR_SPECS:
if radar_spec.contains(address):
return radar_spec
return None
def is_exclusive_full_range_match(radar_spec: RadarSpec, seen_addresses: dict[str, set[int]]) -> bool:
expected_addresses = set(range(radar_spec.start_addr, radar_spec.end_addr + 1))
if seen_addresses[radar_spec.name] != expected_addresses:
return False
for other_spec in RADAR_SPECS:
if other_spec.name == radar_spec.name:
continue
other_expected_addresses = set(range(other_spec.start_addr, other_spec.end_addr + 1))
if seen_addresses[other_spec.name] == other_expected_addresses:
return False
return True
def get_radar_dbc_path(radar_spec: RadarSpec) -> str:
dbc_path = os.path.join(tempfile.gettempdir(), f"{radar_spec.name.lower()}_radar_ui.dbc")
dbc_content = "\n".join(
radar_spec.dbc_template.format(addr_dec=addr, addr_hex=f"{addr:x}")
for addr in range(radar_spec.start_addr, radar_spec.end_addr + 1)
)
if not os.path.exists(dbc_path) or open(dbc_path).read() != dbc_content:
with open(dbc_path, "w") as f:
f.write(dbc_content)
return dbc_path
def get_radar_can_parser(radar_spec: RadarSpec, bus: int) -> CANParser:
messages = [(f"RADAR_TRACK_{addr:x}", 50) for addr in range(radar_spec.start_addr, radar_spec.end_addr + 1)]
return CANParser(get_radar_dbc_path(radar_spec), messages, bus)
def get_track_storage_key(radar_spec: RadarSpec, bus: int, addr: int, track_prefix: str) -> tuple[str, int, int]:
if radar_spec.name in ("RADAR_500_51F", "RADAR_3A5_3C4"):
return (radar_spec.name, bus, addr)
track_index = int(track_prefix[0]) - 1
return (radar_spec.name, bus, addr * 2 + track_index)
def get_track_ts_nanos(parser: CANParser, msg_name: str, radar_spec: RadarSpec, track_prefix: str) -> int:
if radar_spec.name == "RADAR_602_611":
return parser.ts_nanos[msg_name][f"{track_prefix}DISTANCE"]
if radar_spec.name == "RADAR_210_21F":
return parser.ts_nanos[msg_name][f"{track_prefix}LONG_DIST"]
return parser.ts_nanos[msg_name]["LONG_DIST"]
def decode_radar_track(radar_spec: RadarSpec, track_msg, track_prefix: str) -> tuple[float, float, float, float]:
if radar_spec.name == "RADAR_602_611":
return (
track_msg[f"{track_prefix}DISTANCE"],
track_msg[f"{track_prefix}LATERAL"],
track_msg[f"{track_prefix}SPEED"],
float("nan"),
)
if radar_spec.name == "RADAR_210_21F":
return (
track_msg[f"{track_prefix}LONG_DIST"],
track_msg[f"{track_prefix}LAT_DIST"],
track_msg[f"{track_prefix}REL_SPEED"],
float("nan"),
)
if radar_spec.name == "RADAR_500_51F":
azimuth = math.radians(track_msg["AZIMUTH"])
long_dist = track_msg["LONG_DIST"]
return (
math.cos(azimuth) * long_dist,
0.5 * -math.sin(azimuth) * long_dist,
track_msg["REL_SPEED"],
track_msg["REL_ACCEL"],
)
return (
track_msg["LONG_DIST"],
track_msg["LAT_DIST"],
track_msg["REL_SPEED"],
track_msg["REL_ACCEL"],
)
def is_radar_track_valid(radar_spec: RadarSpec, track_msg, track_prefix: str) -> bool:
if radar_spec.name == "RADAR_602_611":
return track_msg[f"{track_prefix}DISTANCE"] != 255.75
if radar_spec.name == "RADAR_210_21F":
return track_msg[f"{track_prefix}STATE"] in (3, 4)
return track_msg["STATE"] in (3, 4)
+912
View File
@@ -0,0 +1,912 @@
#!/usr/bin/env python3
import argparse
import os
import signal
import subprocess
import sys
import time
from dataclasses import dataclass
import cv2
import numpy as np
import pyray as rl
import cereal.messaging as messaging
from opendbc.car.tests.routes import routes
from openpilot.common.basedir import BASEDIR
from openpilot.common.transformations.camera import DEVICE_CAMERAS
from openpilot.tools.replay.custom_routes import CUSTOM_ROUTES
from openpilot.tools.replay.lib.ui_helpers import (
UP,
BLACK,
GREEN,
Calibration,
get_blank_lid_overlay,
init_plots,
plot_lead,
plot_model,
to_topdown_pt,
)
from openpilot.tools.replay.radar_helpers import (
RADAR_SPECS,
build_seen_address_map,
decode_radar_track,
get_radar_can_parser,
get_radar_spec,
get_track_storage_key,
get_track_ts_nanos,
is_exclusive_full_range_match,
is_radar_track_valid,
)
from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA
from msgq.visionipc import VisionIpcClient, VisionStreamType
os.environ['BASEDIR'] = BASEDIR
ANGLE_SCALE = 5.0
RADAR_TRACK_TIMEOUT_FRAMES = 10
RADAR_FORMAT_SWITCH_MISS_FRAMES = 30
RADAR_TRACK_RADIUS = 4
CAMERA_RADAR_Y_OFFSET = 25
RADAR_HEATMAP_DECAY = 0.975
RADAR_HEATMAP_ALPHA = 0.65
CAMERA_RADAR_HEATMAP_ALPHA = 0.45
REPLAY_PATH = os.path.join(os.path.dirname(__file__), "replay")
REPLAY_SOCKET_WAIT_TIMEOUT_SECONDS = 10.0
REPLAY_SPEEDS = (0.2, 0.5, 1.0, 2.0, 4.0, 8.0)
CAMERA_DRAW_WIDTH = 640
CAMERA_DRAW_HEIGHT = 480
TOP_DOWN_DRAW_WIDTH = 384
PLOT_DRAW_WIDTH = 480
PLOT_DRAW_HEIGHT = 480
RADAR_HEATMAP_MODES = ("OFF", "TOP", "CAMERA", "BOTH")
CYAN = (90, 235, 255)
AMBER = (255, 210, 90)
SOFT_WHITE = (235, 235, 235)
SLATE = (140, 180, 210)
@dataclass
class RadarTrackPoint:
trackId: int
measured: bool = True
dRel: float = 0.0
yRel: float = 0.0
vRel: float = 0.0
aRel: float = 0.0
yvRel: float = float("nan")
def draw_radar_points(tracks, lid_overlay):
for track in tracks:
px, py = to_topdown_pt(track.dRel, -track.yRel)
if px != -1:
cv2.circle(lid_overlay, (py, px), RADAR_TRACK_RADIUS, 255, thickness=-1, lineType=cv2.LINE_AA)
def update_radar_heatmap(tracks, radar_heatmap):
radar_heatmap *= RADAR_HEATMAP_DECAY
for track in tracks:
px, py = to_topdown_pt(track.dRel, -track.yRel)
if px != -1:
cv2.circle(radar_heatmap, (py, px), RADAR_TRACK_RADIUS + 2, 255.0, thickness=-1, lineType=cv2.LINE_AA)
def update_radar_camera_heatmap(tracks, radar_heatmap, calibration, shape):
radar_heatmap *= RADAR_HEATMAP_DECAY
if calibration is None:
return
height, width = shape[:2]
for track in tracks:
if track.dRel <= 0.0:
continue
pt = calibration.car_space_to_bb(
np.asarray([track.dRel - RADAR_TO_CAMERA]),
np.asarray([-track.yRel]),
np.asarray([1.0]),
)
x, y = np.round(pt[0]).astype(int)
y += CAMERA_RADAR_Y_OFFSET
if 0 <= x < width and 0 <= y < height:
cv2.circle(radar_heatmap, (x, y), RADAR_TRACK_RADIUS + 3, 255.0, thickness=-1, lineType=cv2.LINE_AA)
def overlay_heatmap(img, radar_heatmap, alpha_scale):
radar_heat_uint8 = np.ascontiguousarray(np.clip(radar_heatmap, 0, 255).astype(np.uint8))
radar_heat_mask = radar_heat_uint8 > 0
if not np.any(radar_heat_mask):
return
heat_rgb = cv2.cvtColor(cv2.applyColorMap(radar_heat_uint8, cv2.COLORMAP_TURBO), cv2.COLOR_BGR2RGB).astype(np.float32)
img_rgb = img.astype(np.float32)
alpha = ((radar_heat_uint8.astype(np.float32) / 255.0) * alpha_scale)[..., None]
img[:] = np.where(
radar_heat_mask[..., None],
np.clip(img_rgb * (1.0 - alpha) + heat_rgb * alpha, 0, 255).astype(np.uint8),
img,
)
def draw_radar_points_camera(tracks, img, calibration):
if calibration is None:
return
for track in tracks:
if track.dRel <= 0.0:
continue
# Match the road-space projection convention used by other UI overlays.
pt = calibration.car_space_to_bb(
np.asarray([track.dRel - RADAR_TO_CAMERA]),
np.asarray([-track.yRel]),
np.asarray([1.0]),
)
x, y = np.round(pt[0]).astype(int)
y += CAMERA_RADAR_Y_OFFSET
if 0 <= x < img.shape[1] and 0 <= y < img.shape[0]:
cv2.circle(img, (x, y), RADAR_TRACK_RADIUS, (255, 255, 255), thickness=-1, lineType=cv2.LINE_AA)
def draw_loading_overlay(font, lines, camera_texture, top_down_texture, hor_mode, panel_x, panel_y):
rl.draw_texture_pro(
camera_texture,
rl.Rectangle(0, 0, camera_texture.width, camera_texture.height),
rl.Rectangle(0, 0, CAMERA_DRAW_WIDTH, CAMERA_DRAW_HEIGHT),
rl.Vector2(0, 0),
0.0,
rl.WHITE,
)
rl.draw_texture(top_down_texture, CAMERA_DRAW_WIDTH, 0, rl.WHITE) # noqa: TID251
if hor_mode:
panel_width = 620
panel_height = 300
else:
panel_width = 700
panel_height = 320
rl.draw_rectangle(panel_x - 20, panel_y - 30, panel_width, panel_height, rl.Color(0, 0, 0, 140))
rl.draw_rectangle_lines(panel_x - 20, panel_y - 30, panel_width, panel_height, rl.Color(255, 255, 255, 90))
for i, line in enumerate(lines):
if line:
rl.draw_text_ex(font, line, rl.Vector2(panel_x, panel_y + i * 40), 28 if i == 0 else 20, 0, rl.WHITE)
def start_replay(route: str, prefix: str, playback: str, data_dir: str | None, start_seconds: int) -> subprocess.Popen:
cmd = [
REPLAY_PATH,
"--playback", playback,
"--prefix", prefix,
]
if start_seconds > 0:
cmd.extend(["--start", str(start_seconds)])
if data_dir:
cmd.extend(["--data_dir", data_dir])
cmd.append(route)
env = os.environ.copy()
env["OPENPILOT_PREFIX"] = prefix
return subprocess.Popen(
cmd,
cwd=os.path.dirname(REPLAY_PATH),
env=env,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
preexec_fn=os.setsid if os.name != "nt" else None,
)
def stop_replay(proc: subprocess.Popen | None) -> None:
if proc is not None and proc.poll() is None:
if os.name != "nt":
try:
os.killpg(proc.pid, signal.SIGTERM)
except ProcessLookupError:
pass
else:
proc.terminate()
try:
proc.wait(timeout=5)
except subprocess.TimeoutExpired:
if os.name != "nt":
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
else:
proc.kill()
proc.wait(timeout=5)
# `replay` can leave helper processes like `replayd` behind, so do one
# targeted cleanup pass on shutdown as well.
for pattern in ("/tools/replay/replay", "replayd"):
try:
subprocess.run(
["pkill", "-f", pattern],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=False,
)
except FileNotFoundError:
pass
def wait_for_can_socket(prefix: str, timeout: float) -> None:
socket_path = os.path.join("/tmp", f"msgq_{prefix}", "can")
started = time.monotonic()
while not os.path.exists(socket_path):
if time.monotonic() - started > timeout:
raise TimeoutError(f"Timed out waiting for CAN socket at {socket_path}")
time.sleep(0.1)
def get_hkg_routes() -> list[tuple[str, str]]:
hkg_prefixes = ("HYUNDAI_", "KIA_", "GENESIS_")
return [
(route.route, getattr(route.car_model, "name", str(route.car_model)))
for route in routes
if route.car_model is not None and getattr(route.car_model, "name", str(route.car_model)).startswith(hkg_prefixes)
]
def get_custom_routes() -> list[tuple[str, str]]:
return [(route.route, route.car_model) for route in CUSTOM_ROUTES]
def make_submaster(addr):
return messaging.SubMaster(
[
'carState',
'longitudinalPlan',
'carControl',
'radarState',
'liveCalibration',
'controlsState',
'selfdriveState',
'liveTracks',
'modelV2',
'liveParameters',
'roadCameraState',
],
addr=addr,
)
def reset_radar_state():
return (
0,
None,
0,
{radar_spec.name: 0 for radar_spec in RADAR_SPECS},
build_seen_address_map(),
{},
0,
{},
{},
{},
)
def ui_thread(addr, route_entries=None, playback="1.0", data_dir=None, prefix="ui-replay", start_route_idx=0):
cv2.setNumThreads(1)
# Get monitor info before creating window
rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT)
rl.init_window(1, 1, "")
max_height = rl.get_monitor_height(0)
rl.close_window()
hor_mode = os.getenv("HORIZONTAL") is not None
hor_mode = True if max_height < 960 + 300 else hor_mode
if hor_mode:
size = (CAMERA_DRAW_WIDTH + TOP_DOWN_DRAW_WIDTH + PLOT_DRAW_WIDTH, 960)
write_x = 5
write_y = 480
else:
size = (CAMERA_DRAW_WIDTH + TOP_DOWN_DRAW_WIDTH, 960 + 300)
write_x = CAMERA_DRAW_WIDTH + 5
write_y = 970
rl.set_trace_log_level(rl.TraceLogLevel.LOG_ERROR)
rl.set_config_flags(rl.ConfigFlags.FLAG_MSAA_4X_HINT)
rl.init_window(size[0], size[1], "openpilot debug UI")
rl.set_target_fps(60)
# Load font
font_path = os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf")
font = rl.load_font_ex(font_path, 32, None, 0)
# Create textures for camera and top-down view
camera_image = rl.gen_image_color(640, 480, rl.BLACK)
camera_texture = rl.load_texture_from_image(camera_image)
rl.unload_image(camera_image)
# lid_overlay array is (lidar_x, lidar_y) = (384, 960)
# pygame treats first axis as width, so texture is 384 wide x 960 tall
# For raylib, we need to transpose to get (height, width) = (960, 384) for the RGBA array
top_down_image = rl.gen_image_color(UP.lidar_x, UP.lidar_y, rl.BLACK)
top_down_texture = rl.load_texture_from_image(top_down_image)
rl.unload_image(top_down_image)
current_route_idx = start_route_idx
current_route_name = None
current_route_model = None
replay_proc = None
current_start_seconds = 0
paused = False
state_checks_enabled = False
radar_heatmap_mode_idx = 0
current_playback = min(REPLAY_SPEEDS, key=lambda speed: abs(speed - float(playback)))
last_replay_started_at = time.monotonic()
playback_ready = False
loading_status = "Initializing UI"
def current_offset_seconds() -> int:
if paused or not playback_ready or replay_proc is None or replay_proc.poll() is not None:
return current_start_seconds
return max(0, int(current_start_seconds + (time.monotonic() - last_replay_started_at) * current_playback))
def connect_streams():
nonlocal replay_proc, current_route_name, current_route_model, current_route_idx, current_start_seconds, loading_status, paused, \
last_replay_started_at, current_playback, playback_ready
if route_entries:
current_route_name, current_route_model = route_entries[current_route_idx]
loading_status = f"Starting replay for route {current_route_idx + 1}/{len(route_entries)} at {current_start_seconds}s ({current_playback:.1f}x)"
stop_replay(replay_proc)
os.environ["OPENPILOT_PREFIX"] = prefix
messaging.reset_context()
replay_proc = start_replay(current_route_name, prefix, f"{current_playback:.1f}", data_dir, current_start_seconds)
paused = False
playback_ready = False
loading_status = "Waiting for CAN socket"
wait_for_can_socket(prefix, REPLAY_SOCKET_WAIT_TIMEOUT_SECONDS)
loading_status = "Connecting to messaging streams"
sm_local = make_submaster(addr)
logcan_local = messaging.sub_sock("can", addr=addr, conflate=False, timeout=100)
loading_status = "Waiting for road camera frames"
return sm_local, logcan_local
sm, logcan = connect_streams()
img = np.zeros((480, 640, 3), dtype='uint8')
imgff = None
num_px = 0
calibration = None
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
lid_overlay_blank = get_blank_lid_overlay(UP)
radar_heatmap = np.zeros_like(lid_overlay_blank, dtype=np.float32)
camera_radar_heatmap = np.zeros(img.shape[:2], dtype=np.float32)
# plots
name_to_arr_idx = {
"gas": 0,
"computer_gas": 1,
"user_brake": 2,
"computer_brake": 3,
"v_ego": 4,
"v_pid": 5,
"angle_steers_des": 6,
"angle_steers": 7,
"angle_steers_k": 8,
"steer_torque": 9,
"v_override": 10,
"v_cruise": 11,
"a_ego": 12,
"a_target": 13,
}
plot_arr = np.zeros((100, len(name_to_arr_idx.values())))
plot_xlims = [(0, plot_arr.shape[0]), (0, plot_arr.shape[0]), (0, plot_arr.shape[0]), (0, plot_arr.shape[0])]
plot_ylims = [(-0.1, 1.1), (-ANGLE_SCALE, ANGLE_SCALE), (0.0, 75.0), (-3.0, 2.0)]
plot_names = [
["gas", "computer_gas", "user_brake", "computer_brake"],
["angle_steers", "angle_steers_des", "angle_steers_k", "steer_torque"],
["v_ego", "v_override", "v_pid", "v_cruise"],
["a_ego", "a_target"],
]
plot_colors = [["b", "b", "g", "r", "y"], ["b", "g", "y", "r"], ["b", "g", "r", "y"], ["b", "r"]]
plot_styles = [["-", "-", "-", "-", "-"], ["-", "-", "-", "-"], ["-", "-", "-", "-"], ["-", "-"]]
draw_plots = init_plots(plot_arr, name_to_arr_idx, plot_xlims, plot_ylims, plot_names, plot_colors, plot_styles)
# Palette for converting lid_overlay grayscale indices to RGBA colors
palette = np.zeros((256, 4), dtype=np.uint8)
palette[:, 3] = 255 # alpha
palette[1] = [255, 0, 0, 255] # RED
palette[2] = [0, 255, 0, 255] # GREEN
palette[3] = [0, 0, 255, 255] # BLUE
palette[4] = [255, 255, 0, 255] # YELLOW
palette[110] = [110, 110, 110, 255] # car_color (gray)
palette[255] = [255, 255, 255, 255] # WHITE
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
while not rl.window_should_close():
# ***** frame *****
if not vipc_client.is_connected():
vipc_client.connect(False)
rl.begin_drawing()
rl.clear_background(rl.Color(64, 64, 64, 255))
if rl.is_key_released(rl.KeyboardKey.KEY_Q):
rl.end_drawing()
stop_replay(replay_proc)
replay_proc = None
break
shift_down = rl.is_key_down(rl.KeyboardKey.KEY_LEFT_SHIFT) or rl.is_key_down(rl.KeyboardKey.KEY_RIGHT_SHIFT)
if route_entries and rl.is_key_released(rl.KeyboardKey.KEY_SPACE):
if paused:
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
else:
current_start_seconds = current_offset_seconds()
paused = True
loading_status = "Paused"
stop_replay(replay_proc)
replay_proc = None
if route_entries and rl.is_key_released(rl.KeyboardKey.KEY_RIGHT):
current_route_idx = (current_route_idx + 1) % len(route_entries)
current_start_seconds = 0
paused = False
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if route_entries and rl.is_key_released(rl.KeyboardKey.KEY_LEFT):
current_route_idx = (current_route_idx - 1) % len(route_entries)
current_start_seconds = 0
paused = False
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if route_entries and rl.is_key_released(rl.KeyboardKey.KEY_M):
current_start_seconds = max(0, current_offset_seconds() + (-60 if shift_down else 60))
paused = False
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if route_entries and rl.is_key_released(rl.KeyboardKey.KEY_S):
current_start_seconds = max(0, current_offset_seconds() + (-10 if shift_down else 10))
paused = False
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if route_entries and (rl.is_key_released(rl.KeyboardKey.KEY_EQUAL) or rl.is_key_released(rl.KeyboardKey.KEY_KP_ADD)):
for speed in REPLAY_SPEEDS:
if speed > current_playback:
current_playback = speed
break
if not paused:
current_start_seconds = current_offset_seconds()
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if route_entries and (rl.is_key_released(rl.KeyboardKey.KEY_MINUS) or rl.is_key_released(rl.KeyboardKey.KEY_KP_SUBTRACT)):
for speed in reversed(REPLAY_SPEEDS):
if speed < current_playback:
current_playback = speed
break
if not paused:
current_start_seconds = current_offset_seconds()
(can_range_msg_count,
active_radar_format_name,
active_radar_format_miss_count,
radar_format_total_counts,
radar_format_seen_addresses,
radar_track_ids,
next_radar_track_id,
radar_tracks,
radar_track_last_seen,
radar_parsers) = reset_radar_state()
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
sm, logcan = connect_streams()
vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True)
if rl.is_key_released(rl.KeyboardKey.KEY_C):
state_checks_enabled = not state_checks_enabled
if rl.is_key_released(rl.KeyboardKey.KEY_H):
radar_heatmap_mode_idx = (radar_heatmap_mode_idx + 1) % len(RADAR_HEATMAP_MODES)
if RADAR_HEATMAP_MODES[radar_heatmap_mode_idx] == "OFF":
radar_heatmap.fill(0)
camera_radar_heatmap.fill(0)
yuv_img_raw = vipc_client.recv()
if yuv_img_raw is None or not yuv_img_raw.data.any():
if replay_proc is not None and replay_proc.poll() is not None:
loading_status = f"Replay exited with code {replay_proc.poll()}"
loading_lines = [
f"{loading_status}{' (paused)' if paused else ''}",
f"Route {current_route_idx + 1}/{len(route_entries)}" if route_entries else "Connected to external replay",
f"Platform: {current_route_model}" if current_route_model is not None else "",
f"Route: {current_route_name}" if current_route_name is not None else "",
f"Offset: {current_offset_seconds()}s" if route_entries else "",
f"Playback: {current_playback:.1f}x" if route_entries else "",
f"Radar state checks: {'ON' if state_checks_enabled else 'OFF'}",
f"Radar heatmap: {RADAR_HEATMAP_MODES[radar_heatmap_mode_idx]}",
"Keys: SPACE play/pause, RIGHT next, LEFT prev, M +/-60s, S +/-10s, +/- speed, C checks, H heatmap, Q quit" \
if route_entries else "Keys: C checks, H heatmap, Q quit",
]
draw_loading_overlay(font, loading_lines, camera_texture, top_down_texture, hor_mode, 80, 160)
rl.end_drawing()
continue
if not playback_ready:
playback_ready = True
last_replay_started_at = time.monotonic()
loading_status = "Streaming"
lid_overlay = lid_overlay_blank.copy()
top_down = top_down_texture, lid_overlay
sm.update(0)
camera = DEVICE_CAMERAS[("tici", str(sm['roadCameraState'].sensor))]
# Use received buffer dimensions (full HEVC can have stride != buffer_len/rows due to VENUS padding)
h, w, stride = yuv_img_raw.height, yuv_img_raw.width, yuv_img_raw.stride
nv12_size = h * 3 // 2 * stride
imgff = np.frombuffer(yuv_img_raw.data, dtype=np.uint8, count=nv12_size).reshape((h * 3 // 2, stride))
num_px = w * h
rgb = cv2.cvtColor(imgff[: h * 3 // 2, : w], cv2.COLOR_YUV2RGB_NV12)
qcam = "QCAM" in os.environ
bb_scale = 0.825 if qcam else 0.8
calib_scale = camera.fcam.width / 640.0
zoom_matrix = np.asarray([[bb_scale, 0.0, 0.0], [0.0, bb_scale, 0.0], [0.0, 0.0, 1.0]])
cv2.warpAffine(rgb, zoom_matrix[:2], (img.shape[1], img.shape[0]), dst=img, flags=cv2.WARP_INVERSE_MAP)
intrinsic_matrix = camera.fcam.intrinsics
w = sm['controlsState'].lateralControlState.which()
if w == 'lqrStateDEPRECATED':
angle_steers_k = sm['controlsState'].lateralControlState.lqrStateDEPRECATED.steeringAngleDeg
elif w == 'indiState':
angle_steers_k = sm['controlsState'].lateralControlState.indiState.steeringAngleDeg
else:
angle_steers_k = np.inf
plot_arr[:-1] = plot_arr[1:]
plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg
plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg
plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k
plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED
# TODO gas is deprecated
plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel / 4.0, 0.0, 1.0)
plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brake
plot_arr[-1, name_to_arr_idx['steer_torque']] = sm['carControl'].actuators.torque * ANGLE_SCALE
# TODO brake is deprecated
plot_arr[-1, name_to_arr_idx['computer_brake']] = np.clip(-sm['carControl'].actuators.accel / 4.0, 0.0, 1.0)
plot_arr[-1, name_to_arr_idx['v_ego']] = sm['carState'].vEgo
plot_arr[-1, name_to_arr_idx['v_cruise']] = sm['carState'].cruiseState.speed
plot_arr[-1, name_to_arr_idx['a_ego']] = sm['carState'].aEgo
if len(sm['longitudinalPlan'].accels):
plot_arr[-1, name_to_arr_idx['a_target']] = sm['longitudinalPlan'].accels[0]
if sm.recv_frame['modelV2']:
plot_model(sm['modelV2'], img, calibration, top_down)
if sm.recv_frame['radarState']:
plot_lead(sm['radarState'], top_down)
if sm.updated['liveCalibration'] and num_px:
rpyCalib = np.asarray(sm['liveCalibration'].rpyCalib)
calibration = Calibration(num_px, rpyCalib, intrinsic_matrix, calib_scale)
can_packets = messaging.drain_sock(logcan)
if can_packets:
can_strings = [
(can_packet.logMonoTime, [(msg.address, msg.dat, msg.src) for msg in can_packet.can])
for can_packet in can_packets
]
detected_format_counts = {radar_spec.name: 0 for radar_spec in RADAR_SPECS}
for can_packet in can_packets:
for msg in can_packet.can:
radar_spec = get_radar_spec(msg.address)
if radar_spec is not None:
can_range_msg_count += 1
detected_format_counts[radar_spec.name] += 1
radar_format_total_counts[radar_spec.name] += 1
radar_format_seen_addresses[radar_spec.name].add(msg.address)
if radar_spec.name not in radar_parsers:
radar_parsers[radar_spec.name] = {}
if msg.src not in radar_parsers[radar_spec.name]:
radar_parsers[radar_spec.name][msg.src] = get_radar_can_parser(radar_spec, msg.src)
matching_formats = [
radar_spec.name
for radar_spec in RADAR_SPECS
if is_exclusive_full_range_match(radar_spec, radar_format_seen_addresses)
]
if len(matching_formats) == 1:
if active_radar_format_name == matching_formats[0]:
active_radar_format_miss_count = 0
elif active_radar_format_name is None:
active_radar_format_name = matching_formats[0]
active_radar_format_miss_count = 0
else:
active_radar_format_miss_count += 1
if active_radar_format_miss_count >= RADAR_FORMAT_SWITCH_MISS_FRAMES:
active_radar_format_name = matching_formats[0]
active_radar_format_miss_count = 0
elif len(matching_formats) == 0 and active_radar_format_name is not None:
active_radar_format_miss_count += 1
if active_radar_format_miss_count >= RADAR_FORMAT_SWITCH_MISS_FRAMES:
active_radar_format_name = None
active_radar_format_miss_count = 0
active_radar_spec = next((spec for spec in RADAR_SPECS if spec.name == active_radar_format_name), None)
if active_radar_spec is not None:
for bus, parser in radar_parsers.get(active_radar_spec.name, {}).items():
updated_addrs = parser.update(can_strings)
relevant_updated_addrs = {
track_addr for track_addr in updated_addrs
if active_radar_spec.start_addr <= track_addr <= active_radar_spec.end_addr
}
if not relevant_updated_addrs:
continue
for track_addr in relevant_updated_addrs:
msg_name = f"RADAR_TRACK_{track_addr:x}"
track_msg = parser.vl[msg_name]
for track_prefix in active_radar_spec.track_prefixes:
track_key = get_track_storage_key(active_radar_spec, bus, track_addr, track_prefix)
ts_nanos = get_track_ts_nanos(parser, msg_name, active_radar_spec, track_prefix)
if ts_nanos == 0:
continue
if state_checks_enabled and not is_radar_track_valid(active_radar_spec, track_msg, track_prefix):
radar_tracks.pop(track_key, None)
radar_track_last_seen.pop(track_key, None)
continue
d_rel, y_rel, v_rel, a_rel = decode_radar_track(active_radar_spec, track_msg, track_prefix)
if track_key not in radar_track_ids:
radar_track_ids[track_key] = next_radar_track_id
next_radar_track_id += 1
radar_tracks[track_key] = RadarTrackPoint(
trackId=radar_track_ids[track_key],
dRel=d_rel,
yRel=y_rel,
vRel=v_rel,
aRel=a_rel,
)
radar_track_last_seen[track_key] = sm.frame
stale_tracks = [
track_key for track_key, last_seen in radar_track_last_seen.items()
if (sm.frame - last_seen) > RADAR_TRACK_TIMEOUT_FRAMES
]
for track_key in stale_tracks:
radar_track_last_seen.pop(track_key, None)
radar_tracks.pop(track_key, None)
active_radar_tracks = [
track for track_key, track in radar_tracks.items()
if active_radar_format_name is not None and track_key[0] == active_radar_format_name
]
active_radar_buses = sorted({
track_key[1] for track_key in radar_tracks
if active_radar_format_name is not None and track_key[0] == active_radar_format_name
})
if len(active_radar_tracks) == 0:
active_radar_tracks = sm['liveTracks'].points
active_radar_buses = []
radar_heatmap_mode = RADAR_HEATMAP_MODES[radar_heatmap_mode_idx]
if radar_heatmap_mode in ("TOP", "BOTH"):
update_radar_heatmap(active_radar_tracks, radar_heatmap)
if radar_heatmap_mode in ("CAMERA", "BOTH"):
update_radar_camera_heatmap(active_radar_tracks, camera_radar_heatmap, calibration, img.shape)
overlay_heatmap(img, camera_radar_heatmap, CAMERA_RADAR_HEATMAP_ALPHA)
# draw decoded radar tracks when present, otherwise fall back to liveTracks
draw_radar_points(active_radar_tracks, top_down[1])
draw_radar_points_camera(active_radar_tracks, img, calibration)
# *** blits ***
# Update camera texture from numpy array
img_rgba = cv2.cvtColor(img, cv2.COLOR_RGB2RGBA)
rl.update_texture(camera_texture, rl.ffi.cast("void *", img_rgba.ctypes.data))
rl.draw_texture_pro(
camera_texture,
rl.Rectangle(0, 0, camera_texture.width, camera_texture.height),
rl.Rectangle(0, 0, CAMERA_DRAW_WIDTH, CAMERA_DRAW_HEIGHT),
rl.Vector2(0, 0),
0.0,
rl.WHITE,
)
# display alerts
rl.draw_text_ex(font, sm['selfdriveState'].alertText1, rl.Vector2(180, 150), 30, 0, rl.RED)
rl.draw_text_ex(font, sm['selfdriveState'].alertText2, rl.Vector2(180, 190), 20, 0, rl.RED)
# draw plots (texture is reused internally)
plot_texture = draw_plots(plot_arr)
if hor_mode:
rl.draw_texture_pro(
plot_texture,
rl.Rectangle(0, 0, plot_texture.width, plot_texture.height),
rl.Rectangle(CAMERA_DRAW_WIDTH + TOP_DOWN_DRAW_WIDTH, 0, PLOT_DRAW_WIDTH, PLOT_DRAW_HEIGHT),
rl.Vector2(0, 0),
0.0,
rl.WHITE,
)
else:
rl.draw_texture(plot_texture, 0, 300, rl.WHITE) # noqa: TID251
# Convert lid_overlay to RGBA and update top_down texture
# lid_overlay is (384, 960), need to transpose to (960, 384) for row-major RGBA buffer
lid_rgba = palette[lid_overlay.T]
if radar_heatmap_mode in ("TOP", "BOTH"):
overlay_heatmap(lid_rgba[..., :3], radar_heatmap.T, RADAR_HEATMAP_ALPHA)
rl.update_texture(top_down_texture, rl.ffi.cast("void *", np.ascontiguousarray(lid_rgba).ctypes.data))
rl.draw_texture(top_down_texture, CAMERA_DRAW_WIDTH, 0, rl.WHITE) # noqa: TID251
SPACING = 20
lines = [
("ENABLED", GREEN if sm['selfdriveState'].enabled else BLACK),
("SPEED: " + str(round(sm['carState'].vEgo, 1)) + " m/s", CYAN),
("LONG CONTROL STATE: " + str(sm['controlsState'].longControlState), CYAN),
("LONG MPC SOURCE: " + str(sm['longitudinalPlan'].longitudinalPlanSource), CYAN),
(f"RADAR FORMAT: {active_radar_format_name or 'NONE'}", AMBER),
(f"RADAR CAN MSGS: {can_range_msg_count}", AMBER),
(f"RADAR TRACKS: {len(active_radar_tracks)}"
+ (f" (BUS {','.join(str(bus) for bus in active_radar_buses)})" if active_radar_buses else ""),
AMBER),
(f"RADAR STATE CHECKS: {'ON' if state_checks_enabled else 'OFF'}", AMBER),
(f"RADAR HEATMAP: {RADAR_HEATMAP_MODES[radar_heatmap_mode_idx]}", AMBER),
(f"ROUTE: {current_route_name}" if current_route_name is not None else "", SLATE),
(f"PLATFORM: {current_route_model}" if current_route_model is not None else "", SLATE),
(f"OFFSET: {current_offset_seconds()}s" if route_entries else "", SOFT_WHITE),
(f"PLAYBACK: {current_playback:.1f}x" if route_entries else "", SOFT_WHITE),
(f"STATUS: {'PAUSED' if paused else 'PLAYING'}" if route_entries else "", SOFT_WHITE),
("ANGLE OFFSET (AVG): " + str(round(sm['liveParameters'].angleOffsetAverageDeg, 2)) + " deg", SOFT_WHITE),
("ANGLE OFFSET (INSTANT): " + str(round(sm['liveParameters'].angleOffsetDeg, 2)) + " deg", SOFT_WHITE),
("STIFFNESS: " + str(round(sm['liveParameters'].stiffnessFactor * 100.0, 2)) + " %", SOFT_WHITE),
("STEER RATIO: " + str(round(sm['liveParameters'].steerRatio, 2)), SOFT_WHITE),
]
hud_height = len(lines) * SPACING + 18
rl.draw_rectangle(write_x - 10, write_y - 12, 560, hud_height, rl.Color(8, 12, 18, 155))
rl.draw_rectangle(write_x - 10, write_y - 12, 4, hud_height, rl.Color(90, 235, 255, 255))
rl.draw_rectangle_lines(write_x - 10, write_y - 12, 560, hud_height, rl.Color(90, 235, 255, 80))
for i, line in enumerate(lines):
if line is not None:
color = rl.Color(line[1][0], line[1][1], line[1][2], 255)
rl.draw_text_ex(font, line[0], rl.Vector2(write_x, write_y + i * SPACING), 20, 0, color)
rl.end_drawing()
rl.unload_texture(camera_texture)
rl.unload_texture(top_down_texture)
rl.unload_font(font)
rl.close_window()
stop_replay(replay_proc)
def get_arg_parser():
parser = argparse.ArgumentParser(description="Show replay data in a UI.", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument("ip_address", nargs="?", default="127.0.0.1", help="The ip address on which to receive zmq messages.")
parser.add_argument("--route", default=None, help="Route to replay locally before opening the UI.")
parser.add_argument("--routes", action="store_true", help="Cycle Hyundai/Kia/Genesis routes from opendbc/car/tests/routes.py.")
parser.add_argument("--custom-routes", nargs="?", type=int, const=1, default=None,
help="Cycle routes from tools/replay/custom_routes.py, optionally starting from a 1-based route index.")
parser.add_argument("--data-dir", default=None, help="Optional local route data directory to pass to replay.")
parser.add_argument("--playback", default="1.0", help="Replay playback speed when using --route.")
parser.add_argument("--prefix", default="ui-replay", help="OPENPILOT_PREFIX to use when launching replay from the UI.")
parser.add_argument("--frame-address", default=None, help="The frame address (fully qualified ZMQ endpoint for frames) on which to receive zmq messages.")
return parser
if __name__ == "__main__":
args = get_arg_parser().parse_args(sys.argv[1:])
selected_sources = int(args.route is not None) + int(args.routes) + int(args.custom_routes is not None)
if selected_sources > 1:
raise SystemExit("Use only one of --route, --routes, or --custom-routes.")
route_entries = None
start_route_idx = 0
if args.route is not None:
route_entries = [(args.route, "MANUAL_ROUTE")]
elif args.routes:
route_entries = get_hkg_routes()
elif args.custom_routes is not None:
route_entries = get_custom_routes()
start_route_idx = max(0, min(len(route_entries) - 1, args.custom_routes - 1))
if route_entries:
os.environ["OPENPILOT_PREFIX"] = args.prefix
messaging.reset_context()
elif args.ip_address != "127.0.0.1":
os.environ["ZMQ"] = "1"
messaging.reset_context()
ui_thread(args.ip_address, route_entries=route_entries, playback=args.playback, data_dir=args.data_dir, prefix=args.prefix, start_route_idx=start_route_idx)
Generated
+82 -80
View File
@@ -21,7 +21,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.13.5"
version = "3.14.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@@ -30,27 +30,29 @@ dependencies = [
{ name = "frozenlist" },
{ name = "multidict" },
{ name = "propcache" },
{ name = "typing-extensions" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" }
sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" },
{ url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" },
{ url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" },
{ url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" },
{ url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" },
{ url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" },
{ url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" },
{ url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" },
{ url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" },
{ url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" },
{ url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" },
{ url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" },
{ url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" },
{ url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" },
{ url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" },
{ url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" },
{ url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" },
{ url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" },
{ url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" },
{ url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" },
{ url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" },
{ url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" },
{ url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" },
{ url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" },
{ url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" },
{ url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" },
{ url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" },
{ url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" },
{ url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" },
{ url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" },
{ url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" },
{ url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" },
]
[[package]]
@@ -477,11 +479,11 @@ wheels = [
[[package]]
name = "idna"
version = "3.17"
version = "3.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b9/28/99c51f664567218d824af024c0251650fb27e4ca066df188dab0769c5b91/idna-3.17.tar.gz", hash = "sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f", size = 196048, upload-time = "2026-05-28T14:32:38.55Z" }
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/a7/f76514cc40ad6234098ecdebda08732d75964776c51a42845b7da10649e2/idna-3.17-py3-none-any.whl", hash = "sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c", size = 65316, upload-time = "2026-05-28T14:32:37.035Z" },
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
]
[[package]]
@@ -1333,27 +1335,27 @@ wheels = [
[[package]]
name = "ruff"
version = "0.15.15"
version = "0.15.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" }
sdist = { url = "https://files.pythonhosted.org/packages/a6/bd/5f7ec371001337d8fa61701c186ff8b613ecac1651848c5950f4c4d5f2e9/ruff-0.15.16.tar.gz", hash = "sha256:d05e78d38c78caf020b03789e25106c93017db5a0cb6e2819885018c61343b78", size = 4714267, upload-time = "2026-06-04T16:33:09.974Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" },
{ url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" },
{ url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" },
{ url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" },
{ url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" },
{ url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" },
{ url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" },
{ url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" },
{ url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" },
{ url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" },
{ url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" },
{ url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" },
{ url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" },
{ url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" },
{ url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" },
{ url = "https://files.pythonhosted.org/packages/0c/42/53ef1c3953f157956db9bf7861e3bc50b9b887ce93300aa48cdba8336fe6/ruff-0.15.16-py3-none-linux_armv6l.whl", hash = "sha256:6ac3c0b3969cc6cf6b158c4e2f8f682acb58e7d700d8a44b65ecdc72d66ab0b2", size = 10709025, upload-time = "2026-06-04T16:32:51.935Z" },
{ url = "https://files.pythonhosted.org/packages/93/9a/a79159346f19134a956607754e57d8d128f7a4c00f4ad2f7514d224c172c/ruff-0.15.16-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:197c207ed75ffba54a0dec23db4aa939a27a3053073e085e0042433cbdc58e4a", size = 11063550, upload-time = "2026-06-04T16:32:42.24Z" },
{ url = "https://files.pythonhosted.org/packages/bc/72/3ce2ac000a5299ec238e01f51397b3b653c93b077d9b1bfe8715bb895f20/ruff-0.15.16-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3a39fec45ab316cc23e7558f23fea4a70403ddb5648ea9a4a3854a16973d0071", size = 10421345, upload-time = "2026-06-04T16:32:37.251Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c2/cc7fad3ec9169373f5b6a18f1917b91080feec40c3f9658334a1d28e2f03/ruff-0.15.16-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba93191d79003116b95128c9d306e045200fdbd0bccb782b110f3cd1d4abc5cf", size = 10757217, upload-time = "2026-06-04T16:32:54.722Z" },
{ url = "https://files.pythonhosted.org/packages/69/d2/3474009eaa0a65b31fa7152a2fad5e2f050c640ceb1e6b02ee6922e94c82/ruff-0.15.16-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c6ee4b90520630120ef032aa5cc10db483852dff950e78b1d717e2993a61ac8d", size = 10507035, upload-time = "2026-06-04T16:33:05.343Z" },
{ url = "https://files.pythonhosted.org/packages/ca/81/b7ae6ccbd11f0c8dc3d5d67fc4be9b57ff57ca86ba56152021378e1277f2/ruff-0.15.16-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e4215bc938bc3c8215c1472c1aa437e310fee20cd427335fec9d7e609563628", size = 11255291, upload-time = "2026-06-04T16:32:49.49Z" },
{ url = "https://files.pythonhosted.org/packages/d9/e1/46e526f1a7cc90857ce6ddf25fbb77eb6568651ac38d71b033af07076dd5/ruff-0.15.16-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7c8d26be963b090f10e29abc8b3e74a2a321f6fa34e02424e30b5af89350ecbb", size = 12124922, upload-time = "2026-06-04T16:33:07.821Z" },
{ url = "https://files.pythonhosted.org/packages/1a/da/5c791b088b596b24d0deb967fa28ae02ad751a140c0b9ea81c5ab915d6c0/ruff-0.15.16-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f198cf4123602a2280ed46c307bcbafe41758d6fee5b456b6b6058ca1514b3b4", size = 11332186, upload-time = "2026-06-04T16:33:02.971Z" },
{ url = "https://files.pythonhosted.org/packages/72/11/5da87abe20047c8962361473923ebb2f62b595250126aadfad8c20649c1e/ruff-0.15.16-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb27515fa6240fb586ae82b901a59e67d24acff86f2190b433dc542fe0435aeb", size = 11373541, upload-time = "2026-06-04T16:32:47.007Z" },
{ url = "https://files.pythonhosted.org/packages/fe/2a/8554754c23a854ae3fd6b507e36ad61ddb121e298c6d5d617dec94ed0f14/ruff-0.15.16-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a267c46ba1593fc26b8eecbea050b39d40c0b6bb7781ee11c90a02cd10032951", size = 11353014, upload-time = "2026-06-04T16:32:34.795Z" },
{ url = "https://files.pythonhosted.org/packages/62/25/62ea41529ec89f742ea3fed9cb1059c72877ec7cf9b9e99ac9cf3294d1d9/ruff-0.15.16-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:528c68f39a91498a8d50e91ff5985df3d105782bab49cc378e73ac26bff083e8", size = 10737467, upload-time = "2026-06-04T16:32:26.348Z" },
{ url = "https://files.pythonhosted.org/packages/90/17/334d3ad9de4d40f9dd58fdd09e35ce64553bb501e2f19a839e2fb6be14fc/ruff-0.15.16-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7ed55c58950df60589a9a7a5d2f8fa5f54ebd287163be805adfe6ee95a9de123", size = 10521910, upload-time = "2026-06-04T16:32:32.54Z" },
{ url = "https://files.pythonhosted.org/packages/4d/bd/3ac7c6ae77a885c1004b3dda2446ea401768d24f851c14b4ad4b24f6639c/ruff-0.15.16-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d482feaf51512b50f9790ceb417a56a61dd1e9d9bf967662b9ed27c01b34f53a", size = 10979190, upload-time = "2026-06-04T16:32:57.492Z" },
{ url = "https://files.pythonhosted.org/packages/33/d7/609546e6a413c3f216fbf2a50c928f97c80939154f6a0503114094a86191/ruff-0.15.16-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e15bc8c94513dae2a40cc9ef07c94fdd4ecc9e29dabebeebe170f952322c9e3", size = 11477014, upload-time = "2026-06-04T16:32:44.687Z" },
{ url = "https://files.pythonhosted.org/packages/74/0d/f2cd247ad32633a5c36e97141a2c21b11c6279f7957bc2ff360b1e08fddd/ruff-0.15.16-py3-none-win32.whl", hash = "sha256:580378f7bd4aa25f72e74aa54948a9622f142b1e509521dd10902e886681cc1e", size = 10735541, upload-time = "2026-06-04T16:32:30.145Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9e/02e845ef151b1dee585e55c4739f8e1734ae1d9f1221dff65761c162208b/ruff-0.15.16-py3-none-win_amd64.whl", hash = "sha256:408256017284eddf98fff77b29aa4fb30f586042d535b2d9befc6512f400aaec", size = 11843403, upload-time = "2026-06-04T16:32:39.76Z" },
{ url = "https://files.pythonhosted.org/packages/15/19/016553f86f207450aebebc2b2b5088d086b901cc8186c02ac4284db3bd88/ruff-0.15.16-py3-none-win_arm64.whl", hash = "sha256:8cd61783afb39638a7133ef0d2dfb1e91277593962f81b5a8423eb0b888a6121", size = 11134555, upload-time = "2026-06-04T16:33:00.136Z" },
]
[[package]]
@@ -1367,15 +1369,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
version = "2.61.1"
version = "2.62.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/3b/4bc6b348bbd331daa14d4babe9f2b99bc854f4da41560eefb9488d78481d/sentry_sdk-2.61.1.tar.gz", hash = "sha256:9c6adccb3feefa9ba032c8d295ca477575c2f11896046a2b0ad686c47c4af555", size = 459429, upload-time = "2026-06-01T07:24:18.875Z" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/5d/a343201726150e05f2036eeb6e493e2e2f8bf8a66f5aa70f2f4ac96f9ca3/sentry_sdk-2.62.0.tar.gz", hash = "sha256:3c870b9f50d9fd15b58c817dbde1c7cfaa9fe3f05df0a4c6edd5571cb82f5491", size = 463986, upload-time = "2026-06-08T13:23:49.223Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/df/54/c9218db183846e08efaf68534889ef42e499dde432778881104a42f7071b/sentry_sdk-2.61.1-py3-none-any.whl", hash = "sha256:fa36eaf4b8ad708f718500d4bdcc1532637526a22beb874d88cbc0a46458b5ae", size = 483735, upload-time = "2026-06-01T07:24:17.027Z" },
{ url = "https://files.pythonhosted.org/packages/3d/07/05440381627877aae223fd68f330df9b9fc6641d08bf65328b55235617a2/sentry_sdk-2.62.0-py3-none-any.whl", hash = "sha256:27f61d13a86c3c1648dec666dd5a64f79772dd6a84b446f11866601ecab24f6f", size = 490586, upload-time = "2026-06-08T13:23:47.486Z" },
]
[[package]]
@@ -1477,39 +1479,39 @@ wheels = [
[[package]]
name = "tqdm"
version = "4.67.3"
version = "4.68.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" }
sdist = { url = "https://files.pythonhosted.org/packages/06/b3/36c8ecf72e8925200671613332db156d84b99b3aee742a41c1938ebb0808/tqdm-4.68.1.tar.gz", hash = "sha256:fc163d96b287bd031e1aa24421ce4411b25559bd0a1be4fe649bdaa4d2c02bf5", size = 171236, upload-time = "2026-06-05T17:23:15.267Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" },
{ url = "https://files.pythonhosted.org/packages/47/aa/218a0eb34de1f753c83e4d0d1c8e7c4cef27f20dcb8342e024f63a80dc86/tqdm-4.68.1-py3-none-any.whl", hash = "sha256:fea4a90e4023f764914569f7802a297277c5ab1a66be5144143e142e1a4031d8", size = 78354, upload-time = "2026-06-05T17:23:13.654Z" },
]
[[package]]
name = "ty"
version = "0.0.41"
version = "0.0.46"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/eb/8b/a64ba465cbc5d1b83c561a498ee5e7729b810606220277cafa93983e6ce1/ty-0.0.41.tar.gz", hash = "sha256:1e8b55bf4729634b2db64a7d9541cd880087cd681e87efc36e6a056cf05fb648", size = 5765398, upload-time = "2026-06-01T11:49:36.815Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/7d/d95b5a9dea83472006be3ce5e480028c44b34138d84d0172e910f287fb69/ty-0.0.46.tar.gz", hash = "sha256:c6c2d7105b5633b49950b4c3a90d1ed2613eb9d794ad582bbbf6c4ffcb93accf", size = 5832380, upload-time = "2026-06-09T03:28:05.056Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/22/b032e4d0f35a436f60eabaa19c70dbed6f5095eb7e30c53ddff918aafcff/ty-0.0.41-py3-none-linux_armv6l.whl", hash = "sha256:5f61c5c06616129ce31dd74141e18be69f5e4204a9a0f4505688213353475b30", size = 11541803, upload-time = "2026-06-01T11:49:33.837Z" },
{ url = "https://files.pythonhosted.org/packages/46/f8/78f073febd728f19f0fcbe2e320855ed337b871ed775e0abe5c7b25bbc6a/ty-0.0.41-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:25d6b4fbf198ae7523b8e4845ec3c7e1b1cd5efa1712febcad3856e70e66632b", size = 11275600, upload-time = "2026-06-01T11:49:59.384Z" },
{ url = "https://files.pythonhosted.org/packages/62/c6/b1b46684514e7372960348c723657f56db56799c36542571b49c77c18c87/ty-0.0.41-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3509788ed3753669cdd28d289f281b834ceee91b4de648fcf0ef60677a75b030", size = 10695726, upload-time = "2026-06-01T11:50:05.232Z" },
{ url = "https://files.pythonhosted.org/packages/36/73/badf085590648a14bfe06571799becefaa2b409c5838c39b198b790c8f58/ty-0.0.41-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8c04f1424c872f1d8d28efbd69fb52728fcc6c37aa2d11675bf16f76d33ae14", size = 11196461, upload-time = "2026-06-01T11:50:07.174Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8f/399ee615282f80fd912b5ce4b3fe31206a6d40283e0700046aebbbb893f2/ty-0.0.41-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e52bfce12f3723c376d690625f5c462fe318c46dbe53b16d36c266388ee04c", size = 11310318, upload-time = "2026-06-01T11:49:50.663Z" },
{ url = "https://files.pythonhosted.org/packages/e8/43/204e8fe9f4f8932dd8e42a23e3aa8a365a93ecc601f67172da1c4cea50c0/ty-0.0.41-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4f1d606602dc0eb895a0943af737d638311c7afd48b414897fd42a05e876a76", size = 11785266, upload-time = "2026-06-01T11:49:48.733Z" },
{ url = "https://files.pythonhosted.org/packages/e3/85/9098e96e40324ebd3797c3cad20ea0d855b56219806605e10b6455cf71b3/ty-0.0.41-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8f3c41f0b6b0525bcc8d76a9a5921cccda11886ae11f73e2d51e20e185bafb3", size = 12328093, upload-time = "2026-06-01T11:49:52.945Z" },
{ url = "https://files.pythonhosted.org/packages/0e/41/b6a4d29591e86a27ab1364ddc70d6cee9a78cff1ede5466f6862f0384a68/ty-0.0.41-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bff8c4468933961e656664c3ef190800e3d50582c248aed41bb8a1578d64864a", size = 11979937, upload-time = "2026-06-01T11:49:29.633Z" },
{ url = "https://files.pythonhosted.org/packages/94/b2/6772c2c24fe412ebd0d57166d36afc176948857f7e90f368d321c561a12e/ty-0.0.41-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c83077a5c56770b1dab5b086a908a4e77740fb8f29d862ed05bd4c854549603d", size = 11859185, upload-time = "2026-06-01T11:49:46.474Z" },
{ url = "https://files.pythonhosted.org/packages/75/59/e4a1e0feef9d74308621c3a8b1558db97fa1c022948341d0eed1bdfc59a1/ty-0.0.41-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:8067fae5532ad3c48295c7ddbd1657ea3ee7392810c57810c9c98fe83d43d57f", size = 12036836, upload-time = "2026-06-01T11:49:39.182Z" },
{ url = "https://files.pythonhosted.org/packages/31/67/38f5312c74d51a510fb46c55b49880af6ca186cb87618938c67e808a45a5/ty-0.0.41-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:73a245242380bac3fbf3005d085c153923a45de8918001fad475af54c6fd50e4", size = 11179559, upload-time = "2026-06-01T11:50:01.355Z" },
{ url = "https://files.pythonhosted.org/packages/8c/55/f8cb36874dc1b8b26c6ae066f8ebec392197caa22becdc55bc7e53d251a5/ty-0.0.41-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4eb25f5dc398aa0a5fbd90de62d6a0fd3713354e566ba93cc53670fe84067ee1", size = 11347231, upload-time = "2026-06-01T11:49:55.093Z" },
{ url = "https://files.pythonhosted.org/packages/8d/96/2d4369d7e8240b2dea1d7a7985709c38c6ccc57bbc8dbbdae3db5ee8d71a/ty-0.0.41-py3-none-musllinux_1_2_i686.whl", hash = "sha256:092c3a1ef9e3a189f71428092e736080ef42bf5c14412aa61bee8d644261797d", size = 11446240, upload-time = "2026-06-01T11:49:31.822Z" },
{ url = "https://files.pythonhosted.org/packages/8b/b0/36f09bfb568bc14f8496f2ace503c63c9152b4b65da4518cd3be0aa85312/ty-0.0.41-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:53edeb6e2ac430fe789454471f0e22a2a71581100818271150bd8f89fa468cc7", size = 11947795, upload-time = "2026-06-01T11:49:41.654Z" },
{ url = "https://files.pythonhosted.org/packages/92/33/ffd38f5e3ecb25e1b7aac314d902ceaa93be16f4874dc236f9da79509468/ty-0.0.41-py3-none-win32.whl", hash = "sha256:f66bdf3afb71f11d412a4c704fa9691e0209cbc8268a19489c8960c7b74eac8b", size = 10776684, upload-time = "2026-06-01T11:50:03.309Z" },
{ url = "https://files.pythonhosted.org/packages/ce/35/4c3e821906cfb05a88c232f5c1202be7ed336e05f4cc796213c809ab6177/ty-0.0.41-py3-none-win_amd64.whl", hash = "sha256:c18386e5c3a0c3d118ce58ee0d8a35f8d18d6b1020ca8ec690f6064e6230a79b", size = 11860965, upload-time = "2026-06-01T11:49:57.07Z" },
{ url = "https://files.pythonhosted.org/packages/5e/20/7c587e4c3c6da1757555831cac308c4add7a958f8b1c7f76cb1dd55ceabf/ty-0.0.41-py3-none-win_arm64.whl", hash = "sha256:289eb66ee5c3554d59b0ea69885ddc5fc8051f3228dd9fee2501799db4a07117", size = 11206414, upload-time = "2026-06-01T11:49:44.395Z" },
{ url = "https://files.pythonhosted.org/packages/0e/24/f9f7533c391610521f4164e6b8e37ef72d0c1ee8651bc0d9ce9e658b953b/ty-0.0.46-py3-none-linux_armv6l.whl", hash = "sha256:5e716337994699cbc1a1a7b7a3e6622306f2574c710330f9d9691c2c3d8391b0", size = 11756264, upload-time = "2026-06-09T03:28:20.112Z" },
{ url = "https://files.pythonhosted.org/packages/66/49/ff3d13655b9b5cc8176f4c3446bf7ec2df43c8ad9e5272d4adc5d952fa45/ty-0.0.46-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:51d618dec5403635690d0e3e298cd0ad3d84ebc6a576652939ef30ce96fce4b2", size = 11492723, upload-time = "2026-06-09T03:28:13.23Z" },
{ url = "https://files.pythonhosted.org/packages/82/4a/e7e3209e353c5835c7756339bbcdfda10852407b80fbb9ed46c17241873a/ty-0.0.46-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acbafd6a2351b07a6cf4c945b0b1d47f6d2826faac2526a351dfa74d3a3cc664", size = 10892822, upload-time = "2026-06-09T03:27:51.179Z" },
{ url = "https://files.pythonhosted.org/packages/6c/20/4390c90434a9ddefcecb65e8df00e4c2700e9739dc0baf58bed36d25f713/ty-0.0.46-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de5df602ffd760612ae36602bbad69b0123ff6cffd92e62aa92b7709317d69e3", size = 11408745, upload-time = "2026-06-09T03:27:58.049Z" },
{ url = "https://files.pythonhosted.org/packages/75/0c/f13a1bf9c6798530c773667095a6cf8f73ec9721db359423e7249bff7fbc/ty-0.0.46-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7abf5a10b30d8641faad90f6a19989daec941bb90261159e05cfeb04d2012046", size = 11544432, upload-time = "2026-06-09T03:27:53.519Z" },
{ url = "https://files.pythonhosted.org/packages/56/69/eb3710c13dff846a0362df04fadd8a39b64ccc244c0d02ce5285ede8eae5/ty-0.0.46-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8770404139c6ccee2ce2fc226478cfa4100915133c876c257e52197b8b92051d", size = 12031228, upload-time = "2026-06-09T03:28:29.816Z" },
{ url = "https://files.pythonhosted.org/packages/e9/68/5f5db9c84c1d44acdc67281089b372d9d818ee68123a60c59c66187095e2/ty-0.0.46-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f960d5a6e4860076924d2b86891d9872c4a3daa4663fb416e640b22cf3dbf68e", size = 12596073, upload-time = "2026-06-09T03:28:25.204Z" },
{ url = "https://files.pythonhosted.org/packages/14/be/cfd0bb272e6a1491f6de30c60da1f39c2b3c3524ec64a5c92b71365c9185/ty-0.0.46-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1d9000a4a3ed08fc37e8a2ff0b801cde06e1c2af3bc053677744bb5a1b751030", size = 12284885, upload-time = "2026-06-09T03:28:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/a8/3a/2cd541f6320f5d6f70a45725c4e1016efedd5545348bb23b47ffb3e4c724/ty-0.0.46-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1160e6dc86536109ab755f7142f36f4dda5333c8330cf230d61819494d27125", size = 12079480, upload-time = "2026-06-09T03:27:55.847Z" },
{ url = "https://files.pythonhosted.org/packages/de/91/8e0075bc6568fb477e7ef4d805c67fa6902b692cb4419e0bf5ce3c04c5bc/ty-0.0.46-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:b619c0efe007731f8221fa787701bfa4402da7a83eb26c61ae25e77b6ace6384", size = 12316547, upload-time = "2026-06-09T03:28:08.28Z" },
{ url = "https://files.pythonhosted.org/packages/00/28/b96cbfeda019a4044c6a8cd06ff84d08b631d4ba7d9a1e6dc0311df3563a/ty-0.0.46-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ad98fccb6a8a94c4121b993761a0deee602f5826c4162e0a91f4f8118ddadd42", size = 11392846, upload-time = "2026-06-09T03:28:00.418Z" },
{ url = "https://files.pythonhosted.org/packages/3b/d0/4d77f699a95ac7a13b94ca1a58682667cfe974f91557d9e2a9fc0b808a7f/ty-0.0.46-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:74536b13c3cc3f5944408669c202d4c57c3d19ff154732df8e6145718aef9191", size = 11559017, upload-time = "2026-06-09T03:28:17.619Z" },
{ url = "https://files.pythonhosted.org/packages/88/62/1d6f6b51c2b132da8011c6a41ead0c1fd2a0b17ea72304bcf6ce084d581a/ty-0.0.46-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5e50b1e96ced41b609e24ed27d9e4f508584ed7f4d0bb717ca8c8d75d2fd1b7c", size = 11666509, upload-time = "2026-06-09T03:28:22.454Z" },
{ url = "https://files.pythonhosted.org/packages/fe/9a/6643894bc12cb30c281f4c8bf37f6d30c1fbd9484ef39a12b0ea6dae3c1c/ty-0.0.46-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0a7d9f58d26d938e5d2f607481b7a412d8c00d675a1ec72004fa9d6b3b9def99", size = 12180448, upload-time = "2026-06-09T03:28:32.329Z" },
{ url = "https://files.pythonhosted.org/packages/86/68/0f3b7bb03a7da676ef51b1c0af0bde1e500d69d5f0c807ed63b6f30b66dd/ty-0.0.46-py3-none-win32.whl", hash = "sha256:26db0ce89c573e60132d14e9688c9329a1633b1a8c26fe457025c7c406f7d5e6", size = 10960002, upload-time = "2026-06-09T03:28:02.832Z" },
{ url = "https://files.pythonhosted.org/packages/b1/f4/91ff618b2dee39d0633d23e1adac0174aa1de80df17e270acac534034dbc/ty-0.0.46-py3-none-win_amd64.whl", hash = "sha256:90e8e6d446b9cb7cb4bede9fca7b3c99fd1e2355605ecf431c131a51db2a5e93", size = 12097413, upload-time = "2026-06-09T03:28:27.495Z" },
{ url = "https://files.pythonhosted.org/packages/e5/2e/300174fca375a27a7c28dd80e990d857d7b3e3b25980c65063f980aa2f17/ty-0.0.46-py3-none-win_arm64.whl", hash = "sha256:ebd320d82605079b901a095dc4711037a0c488b4ace79a602fef4df0d3f4cf74", size = 11439595, upload-time = "2026-06-09T03:28:15.355Z" },
]
[[package]]
@@ -1595,7 +1597,7 @@ wheels = [
[[package]]
name = "zensical"
version = "0.0.43"
version = "0.0.44"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -1607,20 +1609,20 @@ dependencies = [
{ name = "pyyaml" },
{ name = "tomli" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d4/85/ec45162e7824a8f879d887ef0774ee65926bf7d1064e2eebccc7eaee3378/zensical-0.0.43.tar.gz", hash = "sha256:dc2d3804ff562795c1024130e0c3ce79736467930729dda314f096d0e35b98c8", size = 3932396, upload-time = "2026-05-19T09:44:07.418Z" }
sdist = { url = "https://files.pythonhosted.org/packages/86/44/e8c891e607708ed6e2b38620948f0715d13cb378f9525caab84aaf4dfb6c/zensical-0.0.44.tar.gz", hash = "sha256:7452eb2a88e2e42e9a9d7861c5ee6a3b4413766a5c737aa6dc840dab344d46aa", size = 3934771, upload-time = "2026-06-04T17:30:53.326Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/c2/55e0709607ae41c266987c3b91a1a9702b37fbbef0d07eddfe5e25c2d823/zensical-0.0.43-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:17c335362b6bac3a50178181694a964f6d9f0c516fc532129ba5a0a5c4103fb6", size = 12706531, upload-time = "2026-05-19T09:43:32.729Z" },
{ url = "https://files.pythonhosted.org/packages/2c/64/ce8627bc5ea30556162b29b041fe97d6a6aef2a87b51f12def628e4fa608/zensical-0.0.43-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8fe97f185194215f6193af45a17d2b30ebd72c8113e3650f2d7d6767b9c2206", size = 12563012, upload-time = "2026-05-19T09:43:35.962Z" },
{ url = "https://files.pythonhosted.org/packages/66/d1/533bc9454f0e06b3d9d8bd2e7ac405308c3d4dee6572acab98f0ed6d1c07/zensical-0.0.43-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c4c85978c765b3e7f347e8102dfe1373d4bbe4229d7008b6bdbf352f1fbcd7f", size = 12947599, upload-time = "2026-05-19T09:43:38.754Z" },
{ url = "https://files.pythonhosted.org/packages/75/a0/94f47d6fb592997be7ab9526938c929f0199adf2637c3c2b2b9b2101b28e/zensical-0.0.43-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:90d7c06ffd07b2bdf78bef041d541baba8a3ea51fd2dd84dbdbc5b0229076524", size = 12904911, upload-time = "2026-05-19T09:43:42.434Z" },
{ url = "https://files.pythonhosted.org/packages/96/fb/1db3ad9a86ff772f74a8bc60ad5b447aa02a158e70f94adacf50bdd5c40f/zensical-0.0.43-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60022f4a6b95e46ec0023f51052fcd491743b3ebd08c0066b22a5cf1e741fecd", size = 13269386, upload-time = "2026-05-19T09:43:45.387Z" },
{ url = "https://files.pythonhosted.org/packages/31/ee/b24fd0f94885519d851c35615b086d069a1077b0198021a56755395a4633/zensical-0.0.43-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e278eb948a0b7545d50609d713c7c27e366dade4523ff73a311a5d5f136518a", size = 12999364, upload-time = "2026-05-19T09:43:48.549Z" },
{ url = "https://files.pythonhosted.org/packages/28/78/401ccd7afd9d2690f81b5319b7f1eed05108154ce20e4207053914518c1c/zensical-0.0.43-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b85e5ab99fbda13823e67c43a4be6e5ebda6600602969c6575e143f20ac203fd", size = 13124392, upload-time = "2026-05-19T09:43:50.965Z" },
{ url = "https://files.pythonhosted.org/packages/98/b3/9af6eba5826b0ef143fc8308bd1e219e221441e307a958e39f824ba9ab53/zensical-0.0.43-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:751385accc92cccfd4560dabed7c423870686ef6ede244a67e5c96286af25e8f", size = 13177538, upload-time = "2026-05-19T09:43:53.964Z" },
{ url = "https://files.pythonhosted.org/packages/be/6b/cd090bd6659d32692487206469988ee84d41aa6de4cdf9e380f847da90e2/zensical-0.0.43-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:dd3ff5bfa6e65cf3d2550dc639c3da2a3bfa11087b83d57e06623c4c1607d583", size = 13327086, upload-time = "2026-05-19T09:43:56.8Z" },
{ url = "https://files.pythonhosted.org/packages/79/5b/ac2555354b5a53cb9c2c942811905c47be0b9f5603d3c1328ee8564333eb/zensical-0.0.43-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85055a115b12f49c6ab194dcf04f966fc06b690ed6a8ddddd819929fc5f340e6", size = 13284645, upload-time = "2026-05-19T09:43:59.329Z" },
{ url = "https://files.pythonhosted.org/packages/d0/c6/1688ec6e5be15e3ab367d7804753291bfbdff3109b06e20c19ce30a7129c/zensical-0.0.43-cp310-abi3-win32.whl", hash = "sha256:8a75ddd4bb3cd3c4a8e71d2ebae44c5611fd636c1d355c6124dd96e2f9c52838", size = 12256740, upload-time = "2026-05-19T09:44:02.102Z" },
{ url = "https://files.pythonhosted.org/packages/ca/a8/d967e70eac810a7e9eb8c5150d6d02848a1f42260f42977c71debed3cb02/zensical-0.0.43-cp310-abi3-win_amd64.whl", hash = "sha256:03a9d1744a6394ad66c355d6f1de04cfd92efa525b0b94bf6dbf6971c5cd2c6b", size = 12496166, upload-time = "2026-05-19T09:44:04.915Z" },
{ url = "https://files.pythonhosted.org/packages/a7/0f/08c503dff4077c66a99f00556d02f16bd1c67790e43cdd256499d6cab251/zensical-0.0.44-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f7d562b231129356c1ac0a05147d48da759111b671be5c2d4ff6765639550606", size = 12702807, upload-time = "2026-06-04T17:30:18.476Z" },
{ url = "https://files.pythonhosted.org/packages/f7/01/88806c9e8ca6caa246dd9c5c3e15a8d25015c0862820e32b5cfd5cf01d56/zensical-0.0.44-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:534c5303b7f3e4e842a2aa21cd6afe1a19b89a62cda22bff64a8932d988c1e2a", size = 12575802, upload-time = "2026-06-04T17:30:21.617Z" },
{ url = "https://files.pythonhosted.org/packages/5e/21/bc55faf26e5a5e6d8b9216b9efba8a5f2b8c1db09d123b077696e5286fd9/zensical-0.0.44-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3255e14e648571eba1fc51b5d638e5738b96579acc2fd346ae94bb00ca1a37b9", size = 12944141, upload-time = "2026-06-04T17:30:24.532Z" },
{ url = "https://files.pythonhosted.org/packages/2d/62/27f934e443894174cddd1cc1d99fb296e0657611a21fdceb4e071a1207ac/zensical-0.0.44-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:83b167831e181451d266aedad66bf4d199b8ee3147439261c4193bcea09fd8b2", size = 12916341, upload-time = "2026-06-04T17:30:28.134Z" },
{ url = "https://files.pythonhosted.org/packages/18/65/aaa4f0630cb5cd5083176e45af9221bcabfc304af961a5c180f64b5a4dde/zensical-0.0.44-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7556f3a824b45302e29ec2145b2f8c52ca9df3c6c7f2ac40bc7ce3d39f090f01", size = 13277072, upload-time = "2026-06-04T17:30:31.916Z" },
{ url = "https://files.pythonhosted.org/packages/04/03/542ee91da33ec16fefcb5bf5fee40e29bfb15193adade6e729a2d3089982/zensical-0.0.44-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d27ca2de859241fe13871f252c488b5aabcf757772973ba6e57db6be1cdee55", size = 12977180, upload-time = "2026-06-04T17:30:34.508Z" },
{ url = "https://files.pythonhosted.org/packages/4b/71/4a20eb41b312e458e111119a2902ab402bd60320ea1c029ef1f4839c35ad/zensical-0.0.44-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:062c8a4071f75b25bcbe66c95d8984b967ed43ea9eeb3fd79374b4409618f93d", size = 13122478, upload-time = "2026-06-04T17:30:37.44Z" },
{ url = "https://files.pythonhosted.org/packages/67/90/d01e1fbe39ca687cbd076d1eeee9a2ac70b255d18a0f178ec9c0465ae349/zensical-0.0.44-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:17252630f2e4294ae8852b1616b6d87bdafb4d7608d2eb75f6d5ded043bd05ff", size = 13188163, upload-time = "2026-06-04T17:30:40.218Z" },
{ url = "https://files.pythonhosted.org/packages/08/bf/34a2080dcc131dba174f4247e2309113d9fcdaad0f1bd5bd4e0891d87991/zensical-0.0.44-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:4d0dba5a44634bff9d0661ca212d602c126efdb1d7a122c6a389630d17a5c15c", size = 13330242, upload-time = "2026-06-04T17:30:42.684Z" },
{ url = "https://files.pythonhosted.org/packages/eb/81/d752314525b6309657f5bf52b5f4414884df7b1175dc4ad6aaac5e2f5f1d/zensical-0.0.44-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f7ef005280c62a7cfa258583ff68b443bd5e460da9f996f7c671251403dcdb4a", size = 13261108, upload-time = "2026-06-04T17:30:45.516Z" },
{ url = "https://files.pythonhosted.org/packages/d5/08/cd69f37e43619f5613612d2e6b1eeee2842101c29b2db9fdde501716086d/zensical-0.0.44-cp310-abi3-win32.whl", hash = "sha256:b844e28292e9ea93e5dcca229773c027fd3931419d581e1af4fd5ff310679237", size = 12261668, upload-time = "2026-06-04T17:30:48.217Z" },
{ url = "https://files.pythonhosted.org/packages/d4/4d/261244d82be63383482717651befa9971255a6c1399bae61d6c14a117dd9/zensical-0.0.44-cp310-abi3-win_amd64.whl", hash = "sha256:912219e11af23081a7b6bc13e81131bdeacd6bb3516b9508c6b52d8b23aa8208", size = 12502071, upload-time = "2026-06-04T17:30:51.02Z" },
]
[[package]]