diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml
index 27d36f9a4e..23b20a86ff 100644
--- a/.github/workflows/docs.yaml
+++ b/.github/workflows/docs.yaml
@@ -29,9 +29,9 @@ jobs:
# Build
- name: Build docs
run: |
- # TODO: can we install just the "docs" dependency group without the normal deps?
- pip install mkdocs
- mkdocs build
+ git lfs pull
+ pip install zensical
+ python scripts/docs.py build
# Push to docs.comma.ai
- uses: actions/checkout@v6
diff --git a/.gitignore b/.gitignore
index 738a150b7e..3434c7254a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,6 +46,7 @@ compile_commands.json
compare_runtime*.html
# build artifacts
+docs_site/
selfdrive/pandad/pandad
cereal/services.h
cereal/gen
diff --git a/docs/CARS.md b/docs/CARS.md
index 92e6392236..288d5b7a47 100644
--- a/docs/CARS.md
+++ b/docs/CARS.md
@@ -22,7 +22,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Audi[11](#footnotes)|Q3 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Audi[11](#footnotes)|RS3 2018|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Audi[11](#footnotes)|S3 2015-17|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |
||
+|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim, without Super Cruise Package|openpilot available[1](#footnotes)|3 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |
||
|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |||
|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |||
|Chevrolet|Silverado 1500 2020-21|Safety Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |||
@@ -32,34 +32,34 @@ A supported vehicle is one that just works when you install a comma device. All
|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|
||
+|comma|body|All|openpilot|0 mph|0 mph|[](##)|[](##)|None|
||
|CUPRA[11](#footnotes)|Ateca 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
-|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
-|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Focus 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Focus Hybrid 2018[2](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
-|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q4 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Genesis|G70 2018|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -74,18 +74,18 @@ A supported vehicle is one that just works when you install a comma device. All
|Genesis|GV70 Electrified (Australia Only) 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Genesis|GV70 Electrified (with HDA II) 2023-24|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Genesis|GV80 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai M connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |
||
-|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[](##)|[](##)|Parts
- 1 GM connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 harness box
- 1 mount
Buy Here |
||
+|Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Accord 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Accord Hybrid 2023-25|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Nidec connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[4](#footnotes)|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[](##)|[](##)|Parts
- 1 Honda Bosch A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Honda|Civic Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Honda Bosch B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -117,9 +117,9 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Custin 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Elantra 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Elantra Hybrid 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[](##)|[](##)|Parts
- 1 Hyundai J connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Ioniq 5 (Southeast Asia and Europe only) 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -136,17 +136,17 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai O connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Kona Electric (with HDA II, Korea only) 2023|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Kona Hybrid 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai I connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Nexo 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Palisade 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Santa Cruz 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai D connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Hyundai|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Staria 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai L connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -156,8 +156,8 @@ A supported vehicle is one that just works when you install a comma device. All
|Hyundai|Tucson Hybrid 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Tucson Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Hyundai|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[](##)|[](##)|Parts
- 1 FCA connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Kia|Carnival 2022-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Carnival (China only) 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -170,10 +170,10 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|K5 Hybrid 2020-22|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|K7 2017|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|K8 Hybrid (with HDA II) 2023|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai Q connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Niro EV 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai F connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Niro EV 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Niro EV 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Kia|Niro EV (with HDA II) 2024-25|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai R connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Niro EV (without HDA II) 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -188,21 +188,21 @@ A supported vehicle is one that just works when you install a comma device. All
|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai G connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Seltos 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai E connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Kia|Sorento 2021-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Sorento Hybrid 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Sorento Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Sportage 2023-24|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Sportage Hybrid 2023|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai N connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Kia|Stinger 2018-20|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai C connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Kia|Stinger 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai K connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Kia|Telluride 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Hyundai H connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|CT Hybrid 2017-18|Lexus Safety System+|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|ES 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|ES 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|ES Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Lexus|ES Hybrid 2019-25|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Lexus|GS F 2016|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|IS 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lexus|IS 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
@@ -223,25 +223,25 @@ A supported vehicle is one that just works when you install a comma device. All
|Lexus|UX Hybrid 2019-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Ford Q3 connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|MAN[11](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|MAN[11](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|MAN[11](#footnotes)|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|MAN[11](#footnotes)|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|Parts
- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[](##)|[](##)|Parts
- 1 Mazda connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Nissan[5](#footnotes)|Altima 2019-20, 2024|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan B connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Nissan[5](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Nissan[5](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Nissan[5](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Nissan[5](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 Nissan A connector
- 1 OBD-C cable (2 ft)
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Ram connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Rivian|R1S 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
+|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here ||
|
|Rivian|R1T 2025|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Rivian B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|SEAT[11](#footnotes)|Ateca 2016-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|SEAT[11](#footnotes)|Leon 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Subaru|Ascent 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
-|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |
||
+|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |
||
|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
|Subaru|Forester 2017-18|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
|Subaru|Forester 2019-21|All[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
@@ -252,7 +252,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Subaru|Outback 2015-17|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
|Subaru|Outback 2018-19|EyeSight Driver Assistance[6](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
|Subaru|Outback 2020-22|All[6](#footnotes)|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru B connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
-|Subaru|XV 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |
||
+|Subaru|XV 2018-19|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |
||
|Subaru|XV 2020-21|EyeSight Driver Assistance[6](#footnotes)|openpilot available[1,7](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Subaru A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy HereTools
- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep) |||
|Škoda|Fabia 2022-23[14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here [16](#footnotes)|||
|Škoda|Kamiq 2021-23[12,14](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here [16](#footnotes)|||
@@ -279,50 +279,50 @@ A supported vehicle is one that just works when you install a comma device. All
|Toyota|C-HR 2021|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|C-HR Hybrid 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|C-HR Hybrid 2021-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Camry 2018-20|All|Stock|0 mph[10](#footnotes)|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Camry 2018-20|All|Stock|0 mph[10](#footnotes)|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Camry 2021-24|All|openpilot|0 mph[10](#footnotes)|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Camry Hybrid 2018-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Camry Hybrid 2021-24|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Corolla 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Corolla 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Corolla Hatchback 2019-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Corolla Hybrid 2020-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Highlander 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Highlander 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Highlander Hybrid 2017-19|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Highlander Hybrid 2020-23|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|Mirai 2021|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Prius 2016|Toyota Safety Sense P|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Prius 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Prius 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Prius Prime 2017-20|All|Stock|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Prius Prime 2021-22|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|Prius v 2017|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|RAV4 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|RAV4 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|RAV4 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|RAV4 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
|Toyota|RAV4 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|RAV4 Hybrid 2016|Toyota Safety Sense P|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|RAV4 Hybrid 2017-18|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
|Toyota|RAV4 Hybrid 2019-21|All|openpilot|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |||
-|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Toyota|Sienna 2018-20|All|Stock|19 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 Toyota A connector
- 1 comma four
- 1 comma power v3
- 1 harness box
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Arteon Shooting Brake 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Volkswagen[11](#footnotes)|Atlas 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Atlas Cross Sport 2020-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Volkswagen[11](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
-|Volkswagen[11](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|CC 2018-22|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Volkswagen[11](#footnotes)|e-Golf 2014-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Golf 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Golf Alltrack 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
@@ -331,7 +331,7 @@ A supported vehicle is one that just works when you install a comma device. All
|Volkswagen[11](#footnotes)|Golf GTI 2015-21|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Golf R 2015-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Golf SportsVan 2015-20|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
-|Volkswagen[11](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
+|Volkswagen[11](#footnotes)|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|31 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |
||
|Volkswagen[11](#footnotes)|Jetta 2019-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen[11](#footnotes)|Jetta GLI 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
|Volkswagen|Passat 2015-22[13](#footnotes)|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,15](#footnotes)|0 mph|0 mph|[](##)|[](##)|Parts
- 1 OBD-C cable (2 ft)
- 1 VW J533 connector
- 1 comma four
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
Buy Here |||
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
new file mode 100644
index 0000000000..e803a3fb8a
--- /dev/null
+++ b/docs/DEVELOPMENT.md
@@ -0,0 +1,24 @@
+# Docs development
+
+The `docs/` tree is the source for [docs.comma.ai](https://docs.comma.ai).
+The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
+
+Those commands must be run in the root directory of openpilot, **not /docs**
+
+**1. Install the docs dependencies**
+``` bash
+uv pip install .[docs]
+```
+
+**2. Build the new site**
+``` bash
+docs build
+```
+
+**3. Run the new site locally**
+``` bash
+docs serve
+```
+
+References:
+* https://zensical.org/docs/
diff --git a/docs/README.md b/docs/README.md
deleted file mode 100644
index 12d0b6f5dd..0000000000
--- a/docs/README.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# openpilot docs
-
-This is the source for [docs.comma.ai](https://docs.comma.ai).
-The site is updated on pushes to master by this [workflow](../.github/workflows/docs.yaml).
-
-## Development
-NOTE: Those commands must be run in the root directory of openpilot, **not /docs**
-
-**1. Install the docs dependencies**
-``` bash
-uv pip install .[docs]
-```
-
-**2. Build the new site**
-``` bash
-mkdocs build
-```
-
-**3. Run the new site locally**
-``` bash
-mkdocs serve
-```
-
-References:
-* https://www.mkdocs.org/getting-started/
-* https://github.com/ntno/mkdocs-terminal
diff --git a/docs/assets/comma-logo.png b/docs/assets/comma-logo.png
new file mode 120000
index 0000000000..2838d92bfb
--- /dev/null
+++ b/docs/assets/comma-logo.png
@@ -0,0 +1 @@
+../../selfdrive/assets/icons_mici/settings/comma_icon.png
\ No newline at end of file
diff --git a/docs/car-porting/brand-port.md b/docs/car-porting/brand-port.md
deleted file mode 100644
index a3daa7a848..0000000000
--- a/docs/car-porting/brand-port.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Developing a car brand port
-
-A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
-
-Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
diff --git a/docs/car-porting/car-state-signals.md b/docs/car-porting/car-state-signals.md
deleted file mode 100644
index 669bd0ee23..0000000000
--- a/docs/car-porting/car-state-signals.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# CarState signals
-
-## Required for basic lateral control
-
-* `brakePressed`
-* `cruiseState`
-* `doorOpen`
-* `espDisabled`
-* `gasPressed`
-* `gearShifter`
-* `leftBlinker` / `rightBlinker`
-* `seatbeltUnlatched`
-* `standstill`
-* `steeringAngleDeg`
-* `steeringPressed`
-* `steeringTorque`
-* `steerFaultPermanent`
-* `steerFaultTemporary`
-* `vCruise`
-* `wheelSpeeds.[fl|fr|rl|rr]`: Speed of each of the car's four wheels, in m/s. The car's CAN bus often broadcasts the
-speed in kph, so the helper function `parse_wheel_speeds` performs this conversion by default.
-
-## Recommended / Required for openpilot longitudinal control
-
-* `accFaulted`
-* `espActive`
-* `parkingBrake`
-
-## Application Dependent
-
-* `blockPcmEnable`
-* `buttonEnable`
-* `brakeHoldActive`
-* `carFaultedNonCritical`
-* `invalidLkasSetting`
-* `lowSpeedAlert`
-* `regenBraking`
-* `steeringAngleOffsetDeg`
-* `steeringDisengage`
-* `steeringTorqueEps`
-* `stockLkas`
-* `vCruiseCluster`
-* `vEgoCluster`
-* `vehicleSensorsInvalid`
-
-## Automatically populated
-
-* `buttonEvents`
-
-These values are populated automatically by `parse_wheel_speeds`:
-
-* `aEgo`: Acceleration of the ego vehicle, Kalman filtered derivative of `vEgo`.
-* `vEgo`: Speed of the ego vehicle, Kalman filtered from `vEgoRaw`.
-* `vEgoRaw`: Speed of the ego vehicle, based on the average of all four wheel speeds, unfiltered.
-
-## Optional
-
-* `brake`
-* `charging`
-* `fuelGauge`
-* `leftBlindspot` / `rightBlindspot`
-* `steeringRateDeg`
-* `stockAeb`
-* `stockFcw`
-* `yawRate`
diff --git a/docs/car-porting/model-port.md b/docs/car-porting/model-port.md
deleted file mode 100644
index e148a40ecb..0000000000
--- a/docs/car-porting/model-port.md
+++ /dev/null
@@ -1,5 +0,0 @@
-# Developing a car model port
-
-A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
-
-Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
diff --git a/docs/car-porting/reverse-engineering.md b/docs/car-porting/reverse-engineering.md
deleted file mode 100644
index 128ec8e776..0000000000
--- a/docs/car-porting/reverse-engineering.md
+++ /dev/null
@@ -1,85 +0,0 @@
-# Stimulus-Response Tests
-
-These are example test drives that can help identify the CAN bus messaging necessary for ADAS control. Each scripted
-test should be done in a separate route (ignition cycle). These tests are a guide, not necessarily exhaustive.
-
-While testing, constant power to the comma device is highly recommended, using [comma power](https://comma.ai/shop/comma-power) if
-necessary to make sure all test activity is fully captured and for ease of uploading. If constant power isn't
-available, keep the ignition on for at least one minute after your test to make sure power loss doesn't result
-in loss of the last minute of testing data.
-
-## Stationary ignition-only tests, part 1
-
-1. Ignition on, but don't start engine, remain in Park
-2. Open and close each door in a defined order: driver, passenger, rear left, rear right
-3. Re-enter the vehicle, close the driver's door, and fasten the driver's seatbelt
-4. Slowly press and release the accelerator pedal 3 times
-5. Slowly press and release the brake pedal 3 times
-6. Hold the brake and move the gearshift to reverse, then neutral, then drive, then sport/eco/etc if applicable
-7. Return to Park, ignition off
-
-Brake-pressed information may show up in several messages and signals, both as on/off states and as a percentage or
-pressure. It may reflect a switch on the driver's brake pedal, or a pressure-threshold state, or signals to turn on
-the rear brake lights. Start by identifying all the potential signals, and confirm while driving with ACC later.
-
-Locate signals for all four door states if possible, but some cars only expose the driver's door state on the ADAS bus.
-Driver/passenger door signals may or may not change positions for LHD vs RHD cars. For cars where only the driver's
-door signal is available, the same signal may follow the driver.
-
-## Stationary ignition-only tests, part 2
-
-1. Ignition on, but don't start engine, remain in Park
-2. Press each ACC button in a defined order: main switch on/off, set, resume, cancel, accel, decel, gap adjust
-3. Set the left turn signal for about five seconds
-4. Operate the left turn signal one time in its touch-to-pass mode
-5. Set the right turn signal for about five seconds
-6. Operate the right turn signal one time in its touch-to-pass mode
-7. Set the hazard / emergency indicator switch for about five seconds
-8. Ignition off
-
-Your vehicle may have a momentary-press main ACC switch or a physical toggle that remains set. Actual ACC engagement
-isn't necessary for purposes of detecting the ACC button presses.
-
-## Steering angle and steering torque tests
-
-Power steering should be available. On ICE cars, engine RPM may be present.
-
-1. Ignition on, start engine if applicable, remain in Park
-2. Rotate the steering wheel as follows, with a few seconds pause between each step
- * Start as close to exact center as possible
- * Turn to 45 degrees right and hold
- * Turn to 90 degrees right and hold
- * Turn to 180 degrees right and hold
- * Turn to full lock right and hold, with firm pressure against lock
- * Release the wheel and allow it to bounce back slightly from lock
- * Turn to 180 degrees left and hold
- * Return to center and release
-3. Ignition off
-
-Performing the full test to the right, followed by an abbreviated test to the left, helps give additional confirmation
-of signal scale, and sign/direction for both the steering wheel angle and driver input torque signals.
-
-## Low speed / parking lot driving tests
-
-Before this test, drive to a place like an empty parking lot where you are free to drive in a series of curves.
-
-1. Ignition on, start engine if applicable, prepare to drive
-2. Slowly (10-20mph at most) drive a figure-8 if possible, or at least one sharp left and one sharp right.
-3. Come to a complete stop
-4. When and where safe, drive in reverse for a short distance (10-15 feet)
-5. Park the car in a safe place, ignition off
-
-## High speed / highway driving tests
-
-Select a place and time where you can safely set cruise control at normal travel speeds with little interference from
-traffic ahead, and safely test the response of your factory lane guidance system.
-
-1. Ignition on, start engine if applicable, prepare to drive
-2. When safely able, engage adaptive cruise control below 50 mph
-3. When safely able, use the ACC buttons to accelerate to 50mph, then 55mph, then 60mph
-4. Disengage adaptive cruise
-5. When safely able, allow your factory lane guidance to prevent lane departures, 2-3 times on both the left and right
-
-The series of setpoints can be adjusted to local traffic regulations, and of course metric units. The specific cruise
-setpoints are useful for locating the ACC HUD signals later, and confirming their precise scaling. When the car reaches
-and holds the setpoint, that can also provide additional confirmation of wheel speed scaling.
diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md
index 3bfe71bcb4..4f4dd54756 100644
--- a/docs/concepts/glossary.md
+++ b/docs/concepts/glossary.md
@@ -1,9 +1,3 @@
# openpilot glossary
-* **onroad**: openpilot's system state while ignition is on
-* **offroad**: openpilot's system state while ignition is off
-* **route**: a route is a recording of an onroad session
-* **segment**: routes are split into one minute chunks called segments.
-* **comma connect**: the web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai).
-* **panda**: this is the secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda).
-* **comma four**: the latest hardware by comma.ai for running openpilot. more info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four).
+{{GLOSSARY_DEFINITIONS}}
diff --git a/docs/concepts/logs.md b/docs/concepts/logs.md
index 46ab2897df..e533d36297 100644
--- a/docs/concepts/logs.md
+++ b/docs/concepts/logs.md
@@ -6,9 +6,9 @@ Check out our [Python library](https://github.com/commaai/openpilot/blob/master/
For each segment, openpilot records the following log types:
-## rlog.bz2
+## rlog.zst
-rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for a list of all the logged services. They're a bzip2 archive of the serialized capnproto messages.
+rlogs contain all the messages passed amongst openpilot's processes. See [cereal/services.py](https://github.com/commaai/openpilot/blob/master/cereal/services.py) for a list of all the logged services. They're a zstd archive of the serialized [Cap’n Proto](https://capnproto.org/) messages.
## {f,e,d}camera.hevc
@@ -18,12 +18,10 @@ Each camera stream is H.265 encoded and written to its respective file.
* `ecamera.hevc` is the wide road camera
* `dcamera.hevc` is the driver camera
-## qlog.bz2 & qcamera.ts
+## qlog.zst & qcamera.ts
qlogs are a decimated subset of the rlogs. Check out [cereal/services.py](https://github.com/commaai/cereal/blob/master/services.py) for the decimation.
-
qcameras are H.264 encoded, lower res versions of the fcamera.hevc. The video shown in [comma connect](https://connect.comma.ai/) is from the qcameras.
-
-qlogs and qcameras are designed to be small enough to upload instantly on slow internet and store forever, yet useful enough for most analysis and debugging.
+qlogs and qcameras are designed to be small enough to upload instantly on slow internet, yet useful enough for most analysis and debugging.
diff --git a/docs/contributing/feedback.md b/docs/contributing/feedback.md
new file mode 100644
index 0000000000..335d24e13a
--- /dev/null
+++ b/docs/contributing/feedback.md
@@ -0,0 +1,36 @@
+# How to Give Feedback
+
+Feedback is one of the highest leverage ways to contribute to openpilot as a user.
+
+## Driving
+
+Got feedback about how your car drives?
+Join the community Discord, then use the form in `#submit-feedback`.
+
+Before posting feedback, please ensure:
+
+- **openpilot is up to date** you should be on the latest openpilot release or nightly
+- **both road-facing cameras have a clear view** your windshield is clean, lenses are clean, etc.
+- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
+
+## Driver Monitoring
+
+If you find DM annoying while being perfectly attentive, these are likely false positives and we want to fix them!
+In general, driver monitoring feedback is very actionable, and we can fix your complaint within a release cycle.
+
+To post your feedback:
+
+1. Join the [community Discord](https://discord.comma.ai).
+2. If driver camera recording is toggled off, temporarily enable driver camera recording in the settings until you reproduce the issue.
+3. Using comma connect, identify the relevant segment and upload the segment's logs and driver camera.
+4. Post the segment in the `#openpilot-experience` channel on Discord with a good description.
+
+Before posting feedback, please ensure:
+
+- **openpilot is up to date** you should be on the latest openpilot release or nightly
+- **the driver camera has a clear view of the driver** ensure nothing blocks view of the driver (e.g. a cable), the lens is clean, etc.
+- **your device is mounted properly** your device must be mounted horizontally center and relatively high on the windshield
+
+## Other bugs
+
+Got an issue with something else? Open an issue on our [GitHub issue tracker](https://github.com/commaai/openpilot/issues/new/choose).
diff --git a/docs/contributing/roadmap.md b/docs/contributing/roadmap.md
index 1262017a0b..ae27a5461c 100644
--- a/docs/contributing/roadmap.md
+++ b/docs/contributing/roadmap.md
@@ -7,25 +7,11 @@ This is the roadmap for the next major openpilot releases. Also check out
* [Bounties](https://comma.ai/bounties) for paid individual issues
* [#current-projects](https://discord.com/channels/469524606043160576/1249579909739708446) in Discord for discussion on work-in-progress projects
-## openpilot 0.10
-
-openpilot 0.10 will be the first release with a driving policy trained in
-a [learned simulator](https://youtu.be/EqQNZXqzFSI).
-
-* Driving model trained in a learned simulator
-* Always-on driver monitoring (behind a toggle)
-* GPS removed from the driving stack
-* 100KB qlogs
-* `nightly` pushed after 1000 hours of hardware-in-the-loop testing
-* Car interface code moved into [opendbc](https://github.com/commaai/opendbc)
-* openpilot on PC for Linux x86, Linux arm64, and Mac (Apple Silicon)
-
## openpilot 1.0
openpilot 1.0 will feature a fully end-to-end driving policy.
* End-to-end longitudinal control in Chill mode
-* Automatic Emergency Braking (AEB)
* Driver monitoring with sleep detection
* Rolling updates/releases pushed out by CI
* [panda safety 1.0](https://github.com/orgs/commaai/projects/27)
diff --git a/docs/css/tooltip.css b/docs/css/tooltip.css
deleted file mode 100644
index b9a54f793f..0000000000
--- a/docs/css/tooltip.css
+++ /dev/null
@@ -1,44 +0,0 @@
-[data-tooltip] {
- position: relative;
- display: inline-block;
- border-bottom: 1px dotted black;
-}
-
-[data-tooltip] .tooltip-content {
- width: max-content;
- max-width: 25em;
- position: absolute;
- top: 100%;
- left: 50%;
- transform: translateX(-50%);
- background-color: white;
- color: #404040;
- box-shadow: 0 4px 14px 0 rgba(0,0,0,.2), 0 0 0 1px rgba(0,0,0,.05);
- padding: 10px;
- font: 14px/1.5 Lato, proxima-nova, Helvetica Neue, Arial, sans-serif;
- text-decoration: none;
- opacity: 0;
- visibility: hidden;
- transition: opacity 0.1s, visibility 0s;
- z-index: 1000;
- pointer-events: none; /* Prevent accidental interaction */
-}
-
-[data-tooltip]:hover .tooltip-content {
- opacity: 1;
- visibility: visible;
- pointer-events: auto; /* Allow interaction when visible */
-}
-
-.tooltip-content .tooltip-glossary-link {
- display: inline-block;
- margin-top: 8px;
- font-size: 12px;
- color: #007bff;
- text-decoration: none;
-}
-
-.tooltip-content .tooltip-glossary-link:hover {
- color: #0056b3;
- text-decoration: underline;
-}
diff --git a/docs/ext/glossary.py b/docs/ext/glossary.py
new file mode 100644
index 0000000000..35c92add10
--- /dev/null
+++ b/docs/ext/glossary.py
@@ -0,0 +1,216 @@
+import posixpath
+import re
+import tomllib
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+from markdown.extensions import Extension
+from markdown.preprocessors import Preprocessor
+from markdown.treeprocessors import Treeprocessor
+
+from zensical.extensions.links import LinksProcessor
+
+GlossaryTerm = tuple[str, re.Pattern[str], str]
+
+GLOSSARY_FILE = Path(__file__).with_name("glossary.toml")
+GLOSSARY_PAGE = "concepts/glossary.md"
+GLOSSARY_PLACEHOLDER = "{{GLOSSARY_DEFINITIONS}}"
+
+SKIP_TAGS = {
+ "a",
+ "code",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "kbd",
+ "pre",
+ "script",
+ "style",
+}
+
+def clean_tooltip(description: str) -> str:
+ text = re.sub(r"\[([^\]]+)]\([^)]+\)", r"\1", description)
+ text = re.sub(r"`([^`]+)`", r"\1", text)
+ text = re.sub(r"[*_~]", "", text)
+ return re.sub(r"\s+", " ", text).strip()
+
+
+def load_glossary() -> tuple[list[GlossaryTerm], str]:
+ with GLOSSARY_FILE.open("rb") as f:
+ glossary_data = tomllib.load(f).get("glossary", {})
+
+ glossary: list[GlossaryTerm] = []
+ rendered = []
+ for key, value in glossary_data.items():
+ label = str(key).strip().replace("_", " ")
+ description = str(value).strip()
+ if not description:
+ continue
+
+ slug = label.replace(" ", "-").replace("_", "-").lower()
+ glossary.append((slug, re.compile(rf"(?**{label}**: {description}')
+
+ return glossary, "\n".join(rendered)
+
+
+class GlossaryPreprocessor(Preprocessor):
+ def __init__(self, md, glossary: str):
+ super().__init__(md)
+ self.glossary = glossary
+
+ def run(self, lines: list[str]) -> list[str]:
+ markdown = "\n".join(lines)
+ if GLOSSARY_PLACEHOLDER not in markdown:
+ return lines
+ return markdown.replace(GLOSSARY_PLACEHOLDER, self.glossary).splitlines()
+
+
+class GlossaryTreeprocessor(Treeprocessor):
+ def __init__(self, md, glossary: list[GlossaryTerm]):
+ super().__init__(md)
+ self.glossary = glossary
+ self.seen: set[str] = set()
+
+ def run(self, root: ET.Element) -> None:
+ at = self.md.treeprocessors.get_index_for_name("zrelpath")
+ processor = self.md.treeprocessors[at]
+ if not isinstance(processor, LinksProcessor):
+ raise TypeError("Links processor not registered")
+ if processor.path == GLOSSARY_PAGE:
+ return
+
+ self.seen.clear()
+ glossary_href = f"{posixpath.relpath(GLOSSARY_PAGE, posixpath.dirname(processor.path) or '.')}#"
+ self._walk(root, glossary_href)
+
+ def _walk(self, element: ET.Element, glossary_href: str) -> None:
+ if element.tag in SKIP_TAGS or element.attrib.get("data-glossary-skip") is not None:
+ return
+
+ self._replace(element, glossary_href)
+
+ idx = 0
+ while idx < len(element):
+ child = element[idx]
+ self._walk(child, glossary_href)
+ idx = self._replace(element, glossary_href, idx) + 1
+
+ def _replace(self, parent: ET.Element, glossary_href: str, index: int | None = None) -> int:
+ child = None if index is None else parent[index]
+ text = parent.text if child is None else child.tail
+ pieces = self._pieces(text or "", glossary_href)
+ if not pieces:
+ return -1 if index is None else index
+
+ if child is None:
+ parent.text = pieces[0] if isinstance(pieces[0], str) else ""
+ # Insert replacements for parent.text before the first existing child.
+ insert_at = -1
+ else:
+ assert index is not None
+ child.tail = pieces[0] if isinstance(pieces[0], str) else ""
+ insert_at = index
+
+ start = 1 if isinstance(pieces[0], str) else 0
+ previous = child
+
+ for piece in pieces[start:]:
+ if isinstance(piece, str):
+ previous.tail = (previous.tail or "") + piece
+ continue
+
+ insert_at += 1
+ parent.insert(insert_at, piece)
+ previous = piece
+
+ return insert_at
+
+ def _pieces(self, text: str, glossary_href: str) -> list[str | ET.Element]:
+ if not text.strip():
+ return []
+
+ pieces: list[str | ET.Element] = []
+ cursor = 0
+
+ while True:
+ best = None
+ for slug, pattern, tooltip in self.glossary:
+ if slug in self.seen:
+ continue
+
+ found = pattern.search(text, cursor)
+ if found is None:
+ continue
+
+ candidate = (slug, tooltip, found.start(), found.end())
+ if best is None:
+ best = candidate
+ continue
+
+ _, _, best_start, best_end = best
+ _, _, current_start, current_end = candidate
+ if current_start < best_start:
+ best = candidate
+ continue
+
+ if current_start == best_start and current_end - current_start > best_end - best_start:
+ best = candidate
+
+ if best is None:
+ break
+
+ slug, tooltip, start, end = best
+ if start > cursor:
+ pieces.append(text[cursor:start])
+
+ link = ET.Element(
+ "a",
+ {
+ "class": "glossary-term",
+ "data-glossary-term": "",
+ "href": f"{glossary_href}{slug}",
+ },
+ )
+ ET.SubElement(link, "span", {"class": "glossary-term__label"}).text = text[start:end]
+ ET.SubElement(
+ link,
+ "span",
+ {
+ "class": "glossary-term__tooltip",
+ "data-search-exclude": "",
+ },
+ ).text = tooltip
+ pieces.append(link)
+ self.seen.add(slug)
+ cursor = end
+
+ if not pieces:
+ return []
+ if cursor < len(text):
+ pieces.append(text[cursor:])
+ return pieces
+
+
+class GlossaryExtension(Extension):
+ def extendMarkdown(self, md) -> None:
+ md.registerExtension(self)
+ glossary, rendered = load_glossary()
+
+ md.preprocessors.register(
+ GlossaryPreprocessor(md, rendered),
+ "docs-ext-glossary-preprocessor",
+ 27,
+ )
+ md.treeprocessors.register(
+ GlossaryTreeprocessor(md, glossary),
+ "docs-ext-glossary-treeprocessor",
+ 0,
+ )
+
+
+def makeExtension(**kwargs) -> GlossaryExtension:
+ return GlossaryExtension(**kwargs)
diff --git a/docs/ext/glossary.toml b/docs/ext/glossary.toml
new file mode 100644
index 0000000000..62408d9ddd
--- /dev/null
+++ b/docs/ext/glossary.toml
@@ -0,0 +1,8 @@
+[glossary]
+onroad = "openpilot's system state while ignition is on."
+offroad = "openpilot's system state while ignition is off."
+route = "A route is a recording of an onroad session."
+segment = "Routes are split into one minute chunks called segments."
+"comma connect" = "The web viewer for all your routes; check it out at [connect.comma.ai](https://connect.comma.ai)."
+panda = "The secondary processor on the device that implements the functional safety and directly talks to the car over CAN. See the [panda repo](https://github.com/commaai/panda)."
+"comma four" = "The latest hardware by comma.ai for running openpilot. More info at [comma.ai/shop/comma-four](https://www.comma.ai/shop/comma-four)."
diff --git a/docs/getting-started/what-is-openpilot.md b/docs/getting-started/what-is-openpilot.md
deleted file mode 100644
index 6fab2b979b..0000000000
--- a/docs/getting-started/what-is-openpilot.md
+++ /dev/null
@@ -1,12 +0,0 @@
-# What is openpilot?
-
-[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
-
-
-## How do I use it?
-
-openpilot is designed to be used on the comma four.
-
-## How does it work?
-
-In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
diff --git a/docs/glossary.toml b/docs/glossary.toml
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/docs/hooks/glossary.py b/docs/hooks/glossary.py
deleted file mode 100644
index e2fa3d51e0..0000000000
--- a/docs/hooks/glossary.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import re
-import tomllib
-
-def load_glossary(file_path="docs/glossary.toml"):
- with open(file_path, "rb") as f:
- glossary_data = tomllib.load(f)
- return glossary_data.get("glossary", {})
-
-def generate_anchor_id(name):
- return name.replace(" ", "-").replace("_", "-").lower()
-
-def format_markdown_term(name, definition):
- anchor_id = generate_anchor_id(name)
- markdown = f"* [**{name.replace('_', ' ').title()}**](#{anchor_id})"
- if definition.get("abbreviation"):
- markdown += f" *({definition['abbreviation']})*"
- if definition.get("description"):
- markdown += f": {definition['description']}\n"
- return markdown
-
-def glossary_markdown(vocabulary):
- markdown = ""
- for category, terms in vocabulary.items():
- markdown += f"## {category.replace('_', ' ').title()}\n\n"
- for name, definition in terms.items():
- markdown += format_markdown_term(name, definition)
- return markdown
-
-def format_tooltip_html(term_key, definition, html):
- display_term = term_key.replace("_", " ").title()
- clean_description = re.sub(r"\[(.+)]\(.+\)", r"\1", definition["description"])
- glossary_link = (
- f"Glossary🔗"
- )
- return re.sub(
- re.escape(display_term),
- lambda
- match: f"{match.group(0)}{clean_description} {glossary_link}",
- html,
- flags=re.IGNORECASE,
- )
-
-def apply_tooltip(_term_key, _definition, pattern, html):
- return re.sub(
- pattern,
- lambda match: format_tooltip_html(_term_key, _definition, match.group(0)),
- html,
- flags=re.IGNORECASE,
- )
-
-def tooltip_html(vocabulary, html):
- for _category, terms in vocabulary.items():
- for term_key, definition in terms.items():
- if definition.get("description"):
- pattern = rf"(?)(?!\([^)]*\))"
- html = apply_tooltip(term_key, definition, pattern, html)
- return html
-
-# Page Hooks
-def on_page_markdown(markdown, **kwargs):
- glossary = load_glossary()
- return markdown.replace("{{GLOSSARY_DEFINITIONS}}", glossary_markdown(glossary))
-
-def on_page_content(html, **kwargs):
- if kwargs.get("page").title == "Glossary":
- return html
- glossary = load_glossary()
- return tooltip_html(glossary, html)
diff --git a/docs/car-porting/what-is-a-car-port.md b/docs/how-to/car-port.md
similarity index 65%
rename from docs/car-porting/what-is-a-car-port.md
rename to docs/how-to/car-port.md
index 3480e4e5d5..ca565e53f6 100644
--- a/docs/car-porting/what-is-a-car-port.md
+++ b/docs/how-to/car-port.md
@@ -8,7 +8,7 @@ A car port enables openpilot support on a particular car. Each car model openpil
# Structure of a car port
-Virtually all car-specific code is contained in two other repositories: [opendbc](https://github.com/commaai/opendbc) and [panda](https://github.com/commaai/panda).
+All car-specific code is contained in the [opendbc](https://github.com/commaai/opendbc) project.
## opendbc
@@ -23,8 +23,8 @@ Each car brand is supported by a standard interface structure in `opendbc/car/[b
## safety
-* `opendbc_repo/opendbc/safety/modes/[brand].h`: Brand-specific safety logic
-* `opendbc_repo/opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
+* `opendbc/safety/modes/[brand].h`: Brand-specific safety logic
+* `opendbc/safety/tests/test_[brand].py`: Brand-specific safety CI tests
## openpilot
@@ -32,8 +32,20 @@ For historical reasons, openpilot still contains a small amount of car-specific
* `selfdrive/car/car_specific.py`: Brand-specific event logic
-# Overview
+# How do I port car?
[Jason Young](https://github.com/jyoung8607) gave a talk at COMMA_CON with an overview of the car porting process. The talk is available on YouTube:
https://www.youtube.com/watch?v=XxPS5TpTUnI
+
+## Brand Port
+
+A brand port is a port of openpilot to a substantially new car brand or platform within a brand.
+
+Here's an example of one: https://github.com/commaai/openpilot/pull/23331.
+
+## Model Port
+
+A model port is a port of openpilot to a new car model within an already supported brand. Model ports are easier than brand ports because the car's existing APIs are already known.
+
+Here's an example of one: https://github.com/commaai/openpilot/pull/30672/.
diff --git a/docs/how-to/connect-to-comma.md b/docs/how-to/connect-to-comma.md
index 58d4f91bb2..e4e322f111 100644
--- a/docs/how-to/connect-to-comma.md
+++ b/docs/how-to/connect-to-comma.md
@@ -1,15 +1,15 @@
-# connect to a comma four
+# connect to a comma 3X or comma four
-A comma four is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
+A comma device is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console).
## Serial Console
-On both the comma three and comma four, the serial console is accessible from the main OBD-C port.
-Connect the comma four to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power.
+On the comma 3X, the serial console is accessible from the main OBD-C port, forwarded through the panda.
+Access it using `panda/scripts/som_debug.sh`.
-On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect.
+comma four also exposes a serial console, albeit through an internal debug connector. Dedicated debug hardware coming soon to the comma shop.
-On the comma four, the serial console is accessible through the [panda](https://github.com/commaai/panda) using the `panda/tests/som_debug.sh` script.
+Login to the default user with:
* Username: `comma`
* Password: `comma`
@@ -25,7 +25,7 @@ In order to SSH into your device, you'll need a GitHub account with SSH keys. Se
* Port: `22`
Here's an example command for connecting to your device using its tethered connection:
-`ssh comma@192.168.43.1`
+`ssh comma@192.168.43.1 -i ~/.ssh/my_github_key`
For doing development work on device, it's recommended to use [SSH agent forwarding](https://docs.github.com/en/developers/overview/using-ssh-agent-forwarding).
@@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u
* Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555`
> [!NOTE]
-> The default port for ADB is 5555 on the comma four.
+> The default port for ADB is 5555.
For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb).
@@ -55,7 +55,7 @@ The public keys are only fetched from your GitHub account once. In order to upda
The `id_rsa` key in this directory only works while your device is in the setup state with no software installed. After installation, that default key will be removed.
-#### ssh.comma.ai proxy
+## ssh.comma.ai proxy
With a [comma prime subscription](https://comma.ai/connect), you can SSH into your comma device from anywhere.
@@ -79,6 +79,7 @@ Host ssh.comma.ai
```
ssh -i ~/.ssh/my_github_key -o ProxyCommand="ssh -i ~/.ssh/my_github_key -W %h:%p -p %p %h@ssh.comma.ai" comma@ffffffffffffffff
```
+
(Replace `ffffffffffffffff` with your dongle_id)
### ssh.comma.ai host key fingerprint
diff --git a/docs/index.md b/docs/index.md
deleted file mode 120000
index 74ea27aeeb..0000000000
--- a/docs/index.md
+++ /dev/null
@@ -1 +0,0 @@
-getting-started/what-is-openpilot.md
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 100644
index 0000000000..6fab2b979b
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1,12 @@
+# What is openpilot?
+
+[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](https://github.com/commaai/openpilot/blob/master/docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](https://github.com/commaai/openpilot/blob/master/docs/INTEGRATION.md) and [limitations](https://github.com/commaai/openpilot/blob/master/docs/LIMITATIONS.md).
+
+
+## How do I use it?
+
+openpilot is designed to be used on the comma four.
+
+## How does it work?
+
+In short, openpilot uses the car's existing APIs for the built-in [ADAS](https://en.wikipedia.org/wiki/Advanced_driver-assistance_system) system and simply provides better acceleration, braking, and steering inputs than the stock system.
diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css
new file mode 100644
index 0000000000..36ce354af1
--- /dev/null
+++ b/docs/stylesheets/extra.css
@@ -0,0 +1,42 @@
+.md-logo img {
+ filter: invert(1);
+}
+
+.glossary-term {
+ position: relative;
+ color: inherit;
+ text-decoration: none;
+}
+
+.glossary-term__label {
+ border-bottom: 1px dotted currentColor;
+}
+
+.glossary-term__tooltip {
+ position: absolute;
+ top: calc(100% + 0.4rem);
+ left: 50%;
+ width: max-content;
+ max-width: min(30rem, 80vw);
+ padding: 0.65rem 0.8rem;
+ border-radius: 0.6rem;
+ background: rgb(26 26 26 / 96%);
+ color: white;
+ box-shadow: 0 0.6rem 1.8rem rgb(0 0 0 / 22%);
+ font-size: 0.85rem;
+ line-height: 1.45;
+ opacity: 0;
+ pointer-events: none;
+ transform: translateX(-50%) translateY(-0.15rem);
+ transition: opacity 120ms ease, transform 120ms ease;
+ visibility: hidden;
+ z-index: 20;
+}
+
+.glossary-term:hover .glossary-term__tooltip,
+.glossary-term:focus-visible .glossary-term__tooltip,
+.glossary-term:focus-within .glossary-term__tooltip {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ visibility: visible;
+}
diff --git a/mkdocs.yml b/mkdocs.yml
deleted file mode 100644
index 550f807aca..0000000000
--- a/mkdocs.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-site_name: openpilot docs
-repo_url: https://github.com/commaai/openpilot/
-site_url: https://docs.comma.ai
-
-exclude_docs: README.md
-
-strict: true
-docs_dir: docs
-site_dir: docs_site/
-
-hooks:
- - docs/hooks/glossary.py
-extra_css:
- - css/tooltip.css
-theme:
- name: readthedocs
- navigation_depth: 3
-
-nav:
- - Getting Started:
- - What is openpilot?: getting-started/what-is-openpilot.md
- - How-to:
- - Turn the speed blue: how-to/turn-the-speed-blue.md
- - Connect to a comma 3X: how-to/connect-to-comma.md
- # - Make your first pull request: how-to/make-first-pr.md
- #- Replay a drive: how-to/replay-a-drive.md
- - Concepts:
- - Logs: concepts/logs.md
- - Safety: concepts/safety.md
- - Glossary: concepts/glossary.md
- - Car Porting:
- - What is a car port?: car-porting/what-is-a-car-port.md
- - Porting a car brand: car-porting/brand-port.md
- - Porting a car model: car-porting/model-port.md
- - Contributing:
- - Roadmap: contributing/roadmap.md
- #- Architecture: contributing/architecture.md
- - Contributing Guide →: https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md
- - Links:
- - Blog →: https://blog.comma.ai
- - Bounties →: https://comma.ai/bounties
- - GitHub →: https://github.com/commaai
- - Discord →: https://discord.comma.ai
- - X →: https://x.com/comma_ai
diff --git a/pyproject.toml b/pyproject.toml
index a05f1a8944..fd72c128fc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -82,7 +82,7 @@ dependencies = [
[project.optional-dependencies]
docs = [
"Jinja2",
- "mkdocs",
+ "zensical",
]
testing = [
@@ -150,7 +150,7 @@ quiet-level = 3
# if you've got a short variable name that's getting flagged, add it here
ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite,ser"
builtin = "clear,rare,informal,code,names,en-GB_to_en-US"
-skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
+skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.po, uv.lock, *.onnx, *.pem, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*, selfdrive/assets/offroad/mici_fcc.html"
# https://docs.astral.sh/ruff/configuration/#using-pyprojecttoml
[tool.ruff]
diff --git a/scripts/docs.py b/scripts/docs.py
new file mode 100644
index 0000000000..d60bfb791f
--- /dev/null
+++ b/scripts/docs.py
@@ -0,0 +1,63 @@
+"""
+ wrapper that materializes symlinks in docs/ before build
+
+ we can delete this once zensical supports symlinks:
+ https://github.com/zensical/backlog/issues/55
+"""
+import os
+import shutil
+import signal
+import sys
+from pathlib import Path
+
+REPO_ROOT = Path(__file__).resolve().parent.parent
+DOCS_DIR = REPO_ROOT / "docs"
+SITE_DIR = REPO_ROOT / "docs_site"
+sys.path.insert(0, str(REPO_ROOT))
+# Local docs build helpers live under docs/ so they stay near the content
+# source. The wrapper prunes them from docs_site/ after build.
+sys.path.insert(0, str(DOCS_DIR))
+
+
+def _materialize(docs: Path) -> dict[Path, str]:
+ originals: dict[Path, str] = {}
+ for link in docs.rglob("*"):
+ if not link.is_symlink():
+ continue
+ target = link.resolve()
+ if not target.is_file():
+ continue
+ originals[link] = os.readlink(link)
+ link.unlink()
+ shutil.copy2(target, link)
+ return originals
+
+
+def _restore(originals: dict[Path, str]) -> None:
+ for link, target in originals.items():
+ link.unlink(missing_ok=True)
+ os.symlink(target, link)
+
+
+def _raise_interrupt(*_):
+ raise KeyboardInterrupt
+
+
+def _prune_site_output() -> None:
+ shutil.rmtree(SITE_DIR / "ext", ignore_errors=True)
+
+
+def main() -> None:
+ signal.signal(signal.SIGTERM, _raise_interrupt)
+ originals = _materialize(DOCS_DIR)
+ try:
+ from zensical.main import cli
+ cli(standalone_mode=False)
+ if len(sys.argv) > 1 and sys.argv[1] == "build":
+ _prune_site_output()
+ finally:
+ _restore(originals)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/selfdrive/assets/icons_mici/alerts_bell.png b/selfdrive/assets/icons_mici/alerts_bell.png
new file mode 100644
index 0000000000..5d775425eb
--- /dev/null
+++ b/selfdrive/assets/icons_mici/alerts_bell.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:4ce1d357acadd798939b398cce1761ceb05564b44f2a5bc6865c7842e60e79f2
+size 1474
diff --git a/selfdrive/assets/icons_mici/alerts_pill.png b/selfdrive/assets/icons_mici/alerts_pill.png
new file mode 100644
index 0000000000..29ab2ad5b3
--- /dev/null
+++ b/selfdrive/assets/icons_mici/alerts_pill.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b3fe73cd1a24c05346a9b4a02e4f900a314c83a422beb38b0f88f91389582cd4
+size 3960
diff --git a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_center.png b/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_center.png
deleted file mode 100644
index a8a68b372c..0000000000
--- a/selfdrive/assets/icons_mici/onroad/driver_monitoring/dm_center.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b5aee9f6cec03f1967014cd2ea2a23982b262e7d86dadca602ecfa8875b38101
-size 5875
diff --git a/selfdrive/car/CARS_template.md b/selfdrive/car/CARS_template.md
index cd352b2ede..bc335b6bd3 100644
--- a/selfdrive/car/CARS_template.md
+++ b/selfdrive/car/CARS_template.md
@@ -1,6 +1,6 @@
{% set footnote_tag = '[{}](#footnotes)' %}
{% set star_icon = '[](##)' %}
-{% set video_icon = '
' %}
+{% set video_icon = '
' %}
{# Force hardware column wider by using a blank image with max width. #}
{% set width_tag = '
%s
' %}
{% set hardware_col_name = 'Hardware Needed' %}
diff --git a/selfdrive/debug/print_docs_diff.py b/selfdrive/debug/print_docs_diff.py
index 388acf3af5..c7850939f0 100755
--- a/selfdrive/debug/print_docs_diff.py
+++ b/selfdrive/debug/print_docs_diff.py
@@ -11,7 +11,7 @@ FOOTNOTE_TAG = "{}"
STAR_ICON = '
'
VIDEO_ICON = '' + \
- '
'
+ '
'
COLUMNS = "|" + "|".join([column.value for column in Column]) + "|"
COLUMN_HEADER = "|---|---|---|{}|".format("|".join([":---:"] * (len(Column) - 3)))
ARROW_SYMBOL = "➡️"
diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc
index cafc3e1225..cb60917008 100644
--- a/selfdrive/pandad/pandad.cc
+++ b/selfdrive/pandad/pandad.cc
@@ -299,7 +299,7 @@ void process_panda_state(Panda *panda, PubMaster *pm, bool engaged, bool engaged
panda->send_heartbeat(engaged, engaged_mads);
}
-void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) {
+void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control, bool is_onroad) {
static Params params;
static SubMaster sm({"deviceState", "driverCameraState"});
@@ -309,6 +309,8 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control)
static int prev_ir_pwr = 999;
static uint32_t prev_frame_id = UINT32_MAX;
static bool driver_view = false;
+ static bool not_car = false;
+ static bool not_car_checked = false;
// TODO: can we merge these?
static FirstOrderFilter integ_lines_filter(0, 30.0, 0.05);
@@ -354,6 +356,21 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control)
ir_pwr = 0;
}
+ // turn off IR leds if body
+ if (!not_car_checked && is_onroad) {
+ std::string cp_bytes = params.get("CarParams");
+ if (cp_bytes.size() > 0) {
+ AlignedBuffer aligned_buf;
+ capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size()));
+ cereal::CarParams::Reader CP = cmsg.getRoot();
+ not_car = CP.getNotCar();
+ not_car_checked = true;
+ }
+ }
+ if (not_car) {
+ ir_pwr = 0;
+ }
+
if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0) {
int16_t ir_panda = util::map_val(ir_pwr, 0, 100, 0, MAX_IR_PANDA_VAL);
panda->set_ir_pwr(ir_panda);
@@ -387,7 +404,7 @@ void pandad_run(Panda *panda) {
// Process peripheral state at 20 Hz
if (rk.frame() % 5 == 0) {
- process_peripheral_state(panda, &pm, no_fan_control);
+ process_peripheral_state(panda, &pm, no_fan_control, is_onroad);
}
// Process panda state at 10 Hz
diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py
index 94d73392ad..1a10d91cd7 100755
--- a/selfdrive/selfdrived/selfdrived.py
+++ b/selfdrive/selfdrived/selfdrived.py
@@ -230,7 +230,7 @@ class SelfdriveD(CruiseHelper):
if self.CP.notCar:
# wait for everything to init first
- if self.sm.frame > int(5. / DT_CTRL) and self.initialized:
+ if self.sm.frame > int(2. / DT_CTRL) and self.initialized:
# body always wants to enable
self.events.add(EventName.pcmEnable)
diff --git a/selfdrive/test/process_replay/compare_logs.py b/selfdrive/test/process_replay/compare_logs.py
index e2d912a833..4c522c9150 100755
--- a/selfdrive/test/process_replay/compare_logs.py
+++ b/selfdrive/test/process_replay/compare_logs.py
@@ -76,7 +76,7 @@ def _diff_capnp_values(v1, v2, path, tolerance):
for i in range(n):
yield from _diff_capnp_values(v1[i], v2[i], path + (str(i),), tolerance)
if n2 > n:
- yield 'add', dot, list(enumerate(v2[n:], n))
+ yield 'add', dot, [(i, v2[i]) for i in range(n, n2)]
if n1 > n:
yield 'remove', dot, list(reversed([(i, v1[i]) for i in range(n, n1)]))
diff --git a/selfdrive/test/process_replay/diff_report.py b/selfdrive/test/process_replay/diff_report.py
index 32f058f8ee..5da78657f4 100644
--- a/selfdrive/test/process_replay/diff_report.py
+++ b/selfdrive/test/process_replay/diff_report.py
@@ -49,6 +49,8 @@ def diff_format(diffs, ref, new, field) -> list[str]:
msg_type = field.split(".")[0]
ref_ts = [(m.logMonoTime, MsgWrap(m)) for m in ref.get(msg_type, [])]
new_wrapped = [MsgWrap(m) for m in new.get(msg_type, [])]
+ if not ref_ts or not new_wrapped:
+ return format_numeric_diffs(diffs)
return format_diff(diffs, ref_ts, new_wrapped, field)
diff --git a/selfdrive/ui/mici/layouts/home.py b/selfdrive/ui/mici/layouts/home.py
index 8200089c28..cc424fa62b 100644
--- a/selfdrive/ui/mici/layouts/home.py
+++ b/selfdrive/ui/mici/layouts/home.py
@@ -7,13 +7,15 @@ from collections.abc import Callable
from openpilot.system.ui.widgets import Widget
from openpilot.system.ui.widgets.layouts import HBoxLayout
from openpilot.system.ui.widgets.icon_widget import IconWidget
-from openpilot.system.ui.widgets.label import UnifiedLabel
+from openpilot.system.ui.widgets.label import UnifiedLabel, gui_label
from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.system.version import RELEASE_BRANCHES
HEAD_BUTTON_FONT_SIZE = 40
HOME_PADDING = 8
+SETTINGS_ZONE_WIDTH = 280
+ALERTS_ZONE_WIDTH = 180
NetworkType = log.DeviceState.NetworkType
@@ -28,6 +30,37 @@ NETWORK_TYPES = {
}
+class AlertsPill(Widget):
+ ICON_OFFSET = 12
+ COUNT_OFFSET = 40
+
+ def __init__(self):
+ super().__init__()
+ self.set_rect(rl.Rectangle(0, 0, 104, 52))
+
+ self._pill_bg_txt = gui_app.texture("icons_mici/alerts_pill.png", 104, 52)
+ self._warning_txt = gui_app.texture("icons_mici/offroad_alerts/red_warning.png", 36, 36)
+ self._alert_count_callback: Callable[[], int] | None = None
+
+ def set_alert_count_callback(self, callback: Callable[[], int] | None):
+ self._alert_count_callback = callback
+
+ def _render(self, _):
+ alert_count = self._alert_count_callback() if self._alert_count_callback else 0
+ if alert_count > 0:
+ pill_w, pill_h = self._pill_bg_txt.width, self._pill_bg_txt.height
+ rl.draw_texture_ex(self._pill_bg_txt, rl.Vector2(self.rect.x, self.rect.y), 0.0, 1.0, rl.WHITE)
+
+ warn_x = self.rect.x + self.ICON_OFFSET
+ warn_y = self.rect.y + (pill_h - self._warning_txt.height) / 2
+ rl.draw_texture_ex(self._warning_txt, rl.Vector2(warn_x, warn_y), 0.0, 1.0, rl.WHITE)
+
+ count_rect = rl.Rectangle(self.rect.x + self.COUNT_OFFSET, self.rect.y, pill_w - self.COUNT_OFFSET, pill_h)
+ gui_label(count_rect, str(alert_count), font_size=36,
+ alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER,
+ alignment_vertical=rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE)
+
+
class NetworkIcon(Widget):
def __init__(self):
super().__init__()
@@ -84,6 +117,8 @@ class MiciHomeLayout(Widget):
def __init__(self):
super().__init__()
self._on_settings_click: Callable | None = None
+ self._on_alerts_click: Callable | None = None
+ self._alert_count_callback: Callable[[], int] | None = None
self._last_refresh = 0
self._mouse_down_t: None | float = None
@@ -96,6 +131,8 @@ class MiciHomeLayout(Widget):
self._experimental_icon = IconWidget("icons_mici/experimental_mode.png", (48, 48))
self._mic_icon = IconWidget("icons_mici/microphone.png", (32, 46))
+ self._alerts_pill = AlertsPill()
+
self._status_bar_layout = HBoxLayout([
IconWidget("icons_mici/settings.png", (48, 48), opacity=0.9),
NetworkIcon(),
@@ -141,13 +178,23 @@ class MiciHomeLayout(Widget):
self._last_refresh = rl.get_time()
self._update_params()
- def set_callbacks(self, on_settings: Callable | None = None):
+ def set_callbacks(self, on_settings: Callable | None = None, on_alerts: Callable | None = None,
+ alert_count_callback: Callable[[], int] | None = None):
self._on_settings_click = on_settings
+ self._on_alerts_click = on_alerts
+ self._alert_count_callback = alert_count_callback
+ self._alerts_pill.set_alert_count_callback(alert_count_callback)
def _handle_mouse_release(self, mouse_pos: MousePos):
if not self._did_long_press:
- if self._on_settings_click:
- self._on_settings_click()
+ relative_x = mouse_pos.x - self.rect.x
+ has_alerts = self._alert_count_callback and self._alert_count_callback() > 0
+ if relative_x < SETTINGS_ZONE_WIDTH:
+ if self._on_settings_click:
+ self._on_settings_click()
+ elif has_alerts and relative_x > self.rect.width - ALERTS_ZONE_WIDTH:
+ if self._on_alerts_click:
+ self._on_alerts_click()
self._did_long_press = False
def _get_version_text(self) -> tuple[str, str, str, str] | None:
@@ -203,3 +250,8 @@ class MiciHomeLayout(Widget):
footer_rect = rl.Rectangle(self.rect.x + HOME_PADDING, self.rect.y + self.rect.height - 48, self.rect.width - HOME_PADDING, 48)
self._status_bar_layout.render(footer_rect)
+
+ # TODO: add alignment to hboxlayout and add to there
+ self._alerts_pill.set_position(self.rect.x + self.rect.width - self._alerts_pill.rect.width - HOME_PADDING,
+ self.rect.y + self.rect.height - self._alerts_pill.rect.height)
+ self._alerts_pill.render()
diff --git a/selfdrive/ui/mici/layouts/main.py b/selfdrive/ui/mici/layouts/main.py
index e7dfd34101..0c762e098d 100644
--- a/selfdrive/ui/mici/layouts/main.py
+++ b/selfdrive/ui/mici/layouts/main.py
@@ -60,7 +60,11 @@ class MiciMainLayout(Scroller):
gui_app.push_widget(self._onboarding_window)
def _setup_callbacks(self):
- self._home_layout.set_callbacks(on_settings=lambda: gui_app.push_widget(self._settings_layout))
+ self._home_layout.set_callbacks(
+ on_settings=lambda: gui_app.push_widget(self._settings_layout),
+ on_alerts=lambda: self._scroll_to(self._alerts_layout),
+ alert_count_callback=self._alerts_layout.active_alerts,
+ )
self._onroad_layout.set_click_callback(lambda: self._scroll_to(self._home_layout))
device.add_interactive_timeout_callback(self._on_interactive_timeout)
@@ -68,6 +72,11 @@ class MiciMainLayout(Scroller):
layout_x = int(layout.rect.x)
self._scroller.scroll_to(layout_x, smooth=True)
+ def _update_state(self):
+ super()._update_state()
+ # TODO: Hack to run alert updates while not in view. Add a nav stack tick?
+ self._alerts_layout._update_state()
+
def _render(self, _):
if not self._setup:
if self._alerts_layout.active_alerts() > 0:
diff --git a/selfdrive/ui/mici/onroad/augmented_road_view.py b/selfdrive/ui/mici/onroad/augmented_road_view.py
index 72be9cbe4d..9910c955ec 100644
--- a/selfdrive/ui/mici/onroad/augmented_road_view.py
+++ b/selfdrive/ui/mici/onroad/augmented_road_view.py
@@ -178,6 +178,8 @@ class AugmentedRoadView(CameraView):
# update offroad label
if ui_state.panda_type == log.PandaState.PandaType.unknown:
self._offroad_label.set_text("system booting")
+ elif ui_state.ignition and not ui_state.started:
+ self._offroad_label.set_text("openpilot can't start\ncheck alerts")
else:
self._offroad_label.set_text("start the car to\nuse sunnypilot")
diff --git a/selfdrive/ui/mici/onroad/driver_state.py b/selfdrive/ui/mici/onroad/driver_state.py
index 92ff07c1e9..b2be5a8e34 100644
--- a/selfdrive/ui/mici/onroad/driver_state.py
+++ b/selfdrive/ui/mici/onroad/driver_state.py
@@ -6,11 +6,13 @@ from openpilot.common.filter_simple import FirstOrderFilter
from openpilot.system.ui.lib.application import gui_app
from openpilot.system.ui.widgets import Widget
from openpilot.selfdrive.ui.ui_state import ui_state
+from openpilot.selfdrive.monitoring.helpers import face_orientation_from_net
AlertSize = log.SelfdriveState.AlertSize
DEBUG = False
+# TODO: Only left for DM preview, remove
LOOKING_CENTER_THRESHOLD_UPPER = math.radians(6)
LOOKING_CENTER_THRESHOLD_LOWER = math.radians(3)
@@ -59,8 +61,6 @@ class DriverStateRenderer(Widget):
self._dm_person = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_person.png", cone_and_person_size, cone_and_person_size)
self._dm_cone = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_cone.png", cone_and_person_size, cone_and_person_size)
- center_size = round(36 / self.BASE_SIZE * self._rect.width)
- self._dm_center = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_center.png", center_size, center_size)
self._dm_background = gui_app.texture("icons_mici/onroad/driver_monitoring/dm_background.png", int(self._rect.width), int(self._rect.height))
def set_should_draw(self, should_draw: bool):
@@ -113,16 +113,7 @@ class DriverStateRenderer(Widget):
dest_rect,
rl.Vector2(dest_rect.width / 2, dest_rect.height / 2),
self._rotation_filter.x - 90,
- rl.Color(255, 255, 255, int(255 * self._fade_filter.x * (1 - self._looking_center_filter.x))),
- )
-
- rl.draw_texture_ex(
- self._dm_center,
- (int(self._rect.x + (self._rect.width - self._dm_center.width) / 2),
- int(self._rect.y + (self._rect.height - self._dm_center.height) / 2)),
- 0,
- 1.0,
- rl.Color(255, 255, 255, int(255 * self._fade_filter.x * self._looking_center_filter.x)),
+ rl.Color(255, 255, 255, int(255 * self._fade_filter.x)),
)
else:
@@ -174,11 +165,22 @@ class DriverStateRenderer(Widget):
# Get monitoring state
driver_data = self.get_driver_data()
driver_orient = driver_data.faceOrientation
+ driver_position = driver_data.facePosition
if len(driver_orient) != 3:
return
- pitch, yaw, roll = driver_orient
+ # Calibrate orientation so looking straight ahead at road (instead of at device) is (0, 0, 0)
+ sm = ui_state.sm
+ if sm.valid['liveCalibration'] and len(sm['liveCalibration'].rpyCalib) == 3:
+ cal_rpy = sm['liveCalibration'].rpyCalib
+ else:
+ cal_rpy = [0.0, 0.0, 0.0]
+
+ _, pitch, yaw = face_orientation_from_net(driver_orient, driver_position, cal_rpy)
+ pitch += math.radians(6) # calib or DM pose is not accurate, add a fake upward pitch to bias forward
+ yaw = -yaw # undo sign flip in face_orientation_from_net to match UI convention
+
pitch = self._pitch_filter.update(pitch)
yaw = self._yaw_filter.update(yaw)
@@ -192,7 +194,6 @@ class DriverStateRenderer(Widget):
if DEBUG:
pitchd = math.degrees(pitch)
yawd = math.degrees(yaw)
- rolld = math.degrees(roll)
rl.draw_line_ex((0, 100), (200, 100), 3, rl.RED)
rl.draw_line_ex((0, 120), (200, 120), 3, rl.RED)
@@ -200,13 +201,11 @@ class DriverStateRenderer(Widget):
pitch_x = 100 + pitchd
yaw_x = 100 + yawd
- roll_x = 100 + rolld
rl.draw_circle(int(pitch_x), 100, 5, rl.GREEN)
rl.draw_circle(int(yaw_x), 120, 5, rl.GREEN)
- rl.draw_circle(int(roll_x), 140, 5, rl.GREEN)
# filter head rotation, handling wrap-around
- rotation = math.degrees(math.atan2(pitch, yaw))
+ rotation = math.degrees(math.atan2(pitch * 2, yaw)) # reduce yaw sensitivity
angle_diff = rotation - self._rotation_filter.x
angle_diff = ((angle_diff + 180) % 360) - 180
self._rotation_filter.update(self._rotation_filter.x + angle_diff)
diff --git a/sunnypilot/system/updated/tests/test_sp_branch_migrations.py b/sunnypilot/system/updated/tests/test_sp_branch_migrations.py
new file mode 100644
index 0000000000..661fa19840
--- /dev/null
+++ b/sunnypilot/system/updated/tests/test_sp_branch_migrations.py
@@ -0,0 +1,73 @@
+"""
+Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors.
+
+This file is part of sunnypilot and is licensed under the MIT License.
+See the LICENSE.md file in the root directory for more details.
+"""
+import pytest
+
+from openpilot.common.params import Params
+from openpilot.system.updated.updated import Updater
+
+
+@pytest.mark.parametrize(("device_type", "branch", "expected"), [
+ ("tici", "staging-c3-new", "staging-tici"),
+ ("tici", "dev-c3-new", "staging-tici"),
+ ("tici", "master", "master-tici"),
+ ("tici", "master-dev-c3-new", "master-tici"),
+ ("tizi", "staging-c3-new", "staging"),
+ ("tizi", "dev-c3-new", "dev"),
+ ("tizi", "master-dev-c3-new", "master-dev"),
+ ("tizi", "release3", "release-tizi"),
+ ("tizi", "release3-staging", "release-tizi-staging"),
+ ("mici", "release3", "release-mici"),
+ ("mici", "release3-staging", "release-mici-staging"),
+])
+def test_sp_branch_migrations_from_current_branch(mocker, device_type, branch, expected):
+ params = Params()
+ params.remove("UpdaterTargetBranch")
+
+ mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+ mocker.patch.object(Updater, "get_branch", return_value=branch)
+
+ assert Updater().target_branch == expected
+
+
+@pytest.mark.parametrize(("device_type", "branch", "expected"), [
+ ("tici", "staging-c3-new", "staging-tici"),
+ ("tici", "dev-c3-new", "staging-tici"),
+ ("tici", "master", "master-tici"),
+ ("tici", "master-dev-c3-new", "master-tici"),
+ ("tizi", "staging-c3-new", "staging"),
+ ("tizi", "dev-c3-new", "dev"),
+ ("tizi", "master-dev-c3-new", "master-dev"),
+ ("tizi", "release3", "release-tizi"),
+ ("tizi", "release3-staging", "release-tizi-staging"),
+ ("mici", "release3", "release-mici"),
+ ("mici", "release3-staging", "release-mici-staging"),
+])
+def test_sp_branch_migrations_from_param(mocker, device_type, branch, expected):
+ params = Params()
+ params.put("UpdaterTargetBranch", branch)
+
+ mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+
+ try:
+ assert Updater().target_branch == expected
+ finally:
+ params.remove("UpdaterTargetBranch")
+
+
+@pytest.mark.parametrize(("device_type", "branch"), [
+ ("tici", "unknown"),
+ ("tizi", "unknown"),
+ ("mici", "unknown"),
+])
+def test_sp_branch_migrations_passthrough(mocker, device_type, branch):
+ params = Params()
+ params.remove("UpdaterTargetBranch")
+
+ mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+ mocker.patch.object(Updater, "get_branch", return_value=branch)
+
+ assert Updater().target_branch == branch
diff --git a/system/hardware/base.py b/system/hardware/base.py
index 1a19f908c6..ef2b146043 100644
--- a/system/hardware/base.py
+++ b/system/hardware/base.py
@@ -91,6 +91,9 @@ class LPABase(ABC):
def switch_profile(self, iccid: str) -> None:
pass
+ def process_notifications(self) -> None:
+ pass
+
def is_comma_profile(self, iccid: str) -> bool:
return any(iccid.startswith(prefix) for prefix in ('8985235',))
diff --git a/system/hardware/tici/gsma_ci_bundle.pem b/system/hardware/tici/gsma_ci_bundle.pem
new file mode 100644
index 0000000000..3ee7fd1252
--- /dev/null
+++ b/system/hardware/tici/gsma_ci_bundle.pem
@@ -0,0 +1,133 @@
+# GSMA Certificate Issuer (CI) bundle for eSIM RSP
+# Source: https://euicc-manual.osmocom.org/docs/pki/ci/bundle.pem
+
+issuer=
+ countryName = CH
+ organizationName = OISTE Foundation
+ commonName = OISTE GSMA CI G1
+notBefore=2024-01-16 23:17:39Z
+notAfter=2059-01-07 23:17:38Z
+-----BEGIN CERTIFICATE-----
+MIIB9zCCAZ2gAwIBAgIUSpBSCCDYPOEG/IFHUCKpZ2pIAQMwCgYIKoZIzj0EAwIw
+QzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xGTAXBgNV
+BAMMEE9JU1RFIEdTTUEgQ0kgRzEwIBcNMjQwMTE2MjMxNzM5WhgPMjA1OTAxMDcy
+MzE3MzhaMEMxCzAJBgNVBAYTAkNIMRkwFwYDVQQKDBBPSVNURSBGb3VuZGF0aW9u
+MRkwFwYDVQQDDBBPSVNURSBHU01BIENJIEcxMFkwEwYHKoZIzj0CAQYIKoZIzj0D
+AQcDQgAEvZ3s3PFC4NgrCcCMmHJ6DJ66uzAHuLcvjJnOn+TtBNThS7YHLDyHCa2v
+7D+zTP+XTtgqgcLoB56Gha9EQQQ4xKNtMGswDwYDVR0TAQH/BAUwAwEB/zAQBgNV
+HREECTAHiAVghXQFDjAXBgNVHSABAf8EDTALMAkGB2eBEgECAQAwHQYDVR0OBBYE
+FEwnlnrSDBSzkelgHkHmBK1XwCIvMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQD
+AgNIADBFAiBVcywTj017jKpAQ+gwy4MqK2hQvzve6lkvQkgSP6ykHwIhAI0KFwCD
+jnPbmcJsG41hUrWNlf+IcrMvFuYii0DasBNi
+-----END CERTIFICATE-----
+issuer=
+ organizationName = GSM Association
+ commonName = GSM Association - RSP2 Root CI1
+notBefore=2017-02-22 00:00:00Z
+notAfter=2052-02-21 23:59:59Z
+-----BEGIN CERTIFICATE-----
+MIICSTCCAe+gAwIBAgIQbmhWeneg7nyF7hg5Y9+qejAKBggqhkjOPQQDAjBEMRgw
+FgYDVQQKEw9HU00gQXNzb2NpYXRpb24xKDAmBgNVBAMTH0dTTSBBc3NvY2lhdGlv
+biAtIFJTUDIgUm9vdCBDSTEwIBcNMTcwMjIyMDAwMDAwWhgPMjA1MjAyMjEyMzU5
+NTlaMEQxGDAWBgNVBAoTD0dTTSBBc3NvY2lhdGlvbjEoMCYGA1UEAxMfR1NNIEFz
+c29jaWF0aW9uIC0gUlNQMiBSb290IENJMTBZMBMGByqGSM49AgEGCCqGSM49AwEH
+A0IABJ1qutL0HCMX52GJ6/jeibsAqZfULWj/X10p/Min6seZN+hf5llovbCNuB2n
+unLz+O8UD0SUCBUVo8e6n9X1TuajgcAwgb0wDgYDVR0PAQH/BAQDAgEGMA8GA1Ud
+EwEB/wQFMAMBAf8wEwYDVR0RBAwwCogIKwYBBAGC6WAwFwYDVR0gAQH/BA0wCzAJ
+BgdngRIBAgEAME0GA1UdHwRGMEQwQqBAoD6GPGh0dHA6Ly9nc21hLWNybC5zeW1h
+dXRoLmNvbS9vZmZsaW5lY2EvZ3NtYS1yc3AyLXJvb3QtY2kxLmNybDAdBgNVHQ4E
+FgQUgTcPUSXQsdQI1MOyMubSXnlb6/swCgYIKoZIzj0EAwIDSAAwRQIgIJdYsOMF
+WziPK7l8nh5mu0qiRiVf25oa9ullG/OIASwCIQDqCmDrYf+GziHXBOiwJwnBaeBO
+aFsiLzIEOaUuZwdNUw==
+-----END CERTIFICATE-----
+issuer=
+ countryName = US
+ organizationName = Entrust, Inc.
+ organizationalUnitName = See www.entrust.net/legal-terms
+ organizationalUnitName = (c) 2016 Entrust, Inc. - for authorized use only
+ commonName = Entrust eSIM Certification Authority
+notBefore=2016-11-16 16:04:02Z
+notAfter=2051-10-16 16:34:02Z
+-----BEGIN CERTIFICATE-----
+MIIC6DCCAo2gAwIBAgIRAIy4GT7M5nHsAAAAAFgsinowCgYIKoZIzj0EAwIwgbkx
+CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9T
+ZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAx
+NiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNV
+BAMTJEVudHJ1c3QgZVNJTSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAgFw0xNjEx
+MTYxNjA0MDJaGA8yMDUxMTAxNjE2MzQwMlowgbkxCzAJBgNVBAYTAlVTMRYwFAYD
+VQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0
+L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNiBFbnRydXN0LCBJbmMuIC0g
+Zm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxLTArBgNVBAMTJEVudHJ1c3QgZVNJTSBD
+ZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
+BAdzwGHeQ1Wb2f4DmHTByR5/IWL3JugQ1U3908a++bHdlt+TTA7K4c5cYZ+51Yz/
+hg/bacxguPDh9uQUK6Wg3a6jcjBwMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
+BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAVBgNVHREEDjAMiApghkgB
+hvpsFAoAMB0GA1UdDgQWBBQWcEt/NR42B/GMS3AAXDoAPf1BSjAKBggqhkjOPQQD
+AgNJADBGAiEAspjXMvaBZyAg86Z0AAtT0yBRAi1EyaAfNz9kDJeAE04CIQC3efj8
+ATL7/tDBOhANy3cK8PS/1NIlu9vqMLCZsZvJ0Q==
+-----END CERTIFICATE-----
+issuer=
+ countryName = FR
+ organizationName = OBERTHUR TECHNOLOGIES
+ organizationalUnitName = TELECOM
+ commonName = MC4 OT ROOT CI v1
+notBefore=2016-11-15 00:00:01Z
+notAfter=2046-11-08 23:59:59Z
+-----BEGIN CERTIFICATE-----
+MIICOjCCAeGgAwIBAgIBATAKBggqhkjOPQQDAjBbMQswCQYDVQQGEwJGUjEeMBwG
+A1UEChMVT0JFUlRIVVIgVEVDSE5PTE9HSUVTMRAwDgYDVQQLEwdURUxFQ09NMRow
+GAYDVQQDExFNQzQgT1QgUk9PVCBDSSB2MTAeFw0xNjExMTUwMDAwMDFaFw00NjEx
+MDgyMzU5NTlaMFsxCzAJBgNVBAYTAkZSMR4wHAYDVQQKExVPQkVSVEhVUiBURUNI
+Tk9MT0dJRVMxEDAOBgNVBAsTB1RFTEVDT00xGjAYBgNVBAMTEU1DNCBPVCBST09U
+IENJIHYxMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHb/Gajt3OZxuaDSklBQE
+D4lOd6PGPLSvtfkM952ubdyy45tJwAeA0eEii0CLrFT6tcfXkW+H/5mQyMRXaAUk
+T6OBlTCBkjAfBgNVHSMEGDAWgBTNbmC3LXoGPLyEYluR6A/jBAbhPjAdBgNVHQ4E
+FgQUzW5gty16Bjy8hGJbkegP4wQG4T4wDgYDVR0PAQH/BAQDAgAGMBcGA1UdIAEB
+/wQNMAswCQYHZ4ESAQIBADAWBgNVHREEDzANiAsrBgEEAYHvb7OITTAPBgNVHRMB
+Af8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIEw4Nc7f2fDtoH+6ON/bknfDQxmT
+ikThXjhpLtSrSKN2AiAxHxgC87L0FDnH8dJNlkdGX9c0JIx6oLheIplfS6k+jg==
+-----END CERTIFICATE-----
+issuer=
+ commonName = SubMan V4.2 CI Google Pixel
+ organizationName = Giesecke and Devrient GmbH
+ organizationalUnitName = Mobile Security
+ countryName = DE
+notBefore=2017-05-10 00:00:00Z
+notAfter=2027-05-10 00:00:00Z
+-----BEGIN CERTIFICATE-----
+MIICaTCCAg6gAwIBAgICASwwCgYIKoZIzj0EAwIwczElMCMGA1UEAxMcIFN1Yk1h
+biBWNC4yIENJIEdvb2dsZSBQaXhlbDEjMCEGA1UEChMaR2llc2Vja2UgYW5kIERl
+dnJpZW50IEdtYkgxGDAWBgNVBAsTD01vYmlsZSBTZWN1cml0eTELMAkGA1UEBhMC
+REUwHhcNMTcwNTEwMDAwMDAwWhcNMjcwNTEwMDAwMDAwWjBzMSUwIwYDVQQDExwg
+U3ViTWFuIFY0LjIgQ0kgR29vZ2xlIFBpeGVsMSMwIQYDVQQKExpHaWVzZWNrZSBh
+bmQgRGV2cmllbnQgR21iSDEYMBYGA1UECxMPTW9iaWxlIFNlY3VyaXR5MQswCQYD
+VQQGEwJERTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHNorfaJsGzqWNawyAhl
+IAv9QL2/+b9RsUoso06t/dKX1MRr5CUJ51acvv5TAFhQKIml+dwLbFnV5aO+8W6Z
+wxajgZEwgY4wHwYDVR0jBBgwFoAUtg8LiX/WMLiM/tYWH46oCMU4KsMwHQYDVR0O
+BBYEFLYPC4l/1jC4jP7WFh+OqAjFOCrDMA4GA1UdDwEB/wQEAwIBBjAXBgNVHSAB
+Af8EDTALMAkGB2eBEgECAQAwDwYDVR0TAQH/BAUwAwEB/zASBgNVHREECzAJiAcr
+BgEEAdwPMAoGCCqGSM49BAMCA0kAMEYCIQDpoZcuAQrjATW8U+AWqMUJ0dY6nWW1
+R1QmFzVZ1yMXSwIhALCvRqkCtgiavdeFeSgsSNbY5Fhd+QoCltuSh1U4TE7A
+-----END CERTIFICATE-----
+issuer=
+ countryName = DE
+ commonName = SubMan V4.2 CI
+ organizationName = Giesecke and Devrient
+ organizationalUnitName = Mobile Security
+notBefore=2016-08-12 13:51:48Z
+notAfter=2026-08-12 13:51:48Z
+-----BEGIN CERTIFICATE-----
+MIICUjCCAfigAwIBAgIDQgAAMAoGCCqGSM49BAMCMGAxCzAJBgNVBAYTAkRFMRcw
+FQYDVQQDEw5TdWJNYW4gVjQuMiBDSTEeMBwGA1UEChMVR2llc2Vja2UgYW5kIERl
+dnJpZW50MRgwFgYDVQQLEw9Nb2JpbGUgU2VjdXJpdHkwHhcNMTYwODEyMTM1MTQ4
+WhcNMjYwODEyMTM1MTQ4WjBgMQswCQYDVQQGEwJERTEXMBUGA1UEAxMOU3ViTWFu
+IFY0LjIgQ0kxHjAcBgNVBAoTFUdpZXNlY2tlIGFuZCBEZXZyaWVudDEYMBYGA1UE
+CxMPTW9iaWxlIFNlY3VyaXR5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEYIgl
+VQr9wbXOlwPp8qMg5Df08Cli9Mc+lpr3Lwa9PlVA3QWlLeX4GfD4H3phLBqVIa17
+yHttmtheTxi0KoEqhKOBoDCBnTAdBgNVHQ4EFgQU6lOt7zMpuVCa/XVf1Ei4LcG8
+7P8wDgYDVR0PAQH/BAQDAgEGMBcGA1UdIAEB/wQNMAswCQYHZ4ESAQIBADAPBgNV
+HRMBAf8EBTADAQH/MBIGA1UdEQQLMAmIBysGAQQB3A8wLgYDVR0fBCcwJTAjoCGg
+H4YdaHR0cDovL2dpLWRlLmNvbS90ZXN0LmNybC5wZW0wCgYIKoZIzj0EAwIDSAAw
+RQIhAMMx2L/VHDiOW+Fl/OuFmhCdizYM17Yn9zAVieKO2T0iAiANWtCMmY+DzkqK
+yHxBFX0U2tBd682zP4DpgRt8j3Ylew==
+-----END CERTIFICATE-----
diff --git a/system/hardware/tici/lpa.py b/system/hardware/tici/lpa.py
index ceb901e2f7..e46ac005b8 100644
--- a/system/hardware/tici/lpa.py
+++ b/system/hardware/tici/lpa.py
@@ -3,8 +3,10 @@
import atexit
import base64
import fcntl
+import hashlib
import math
import os
+import requests
import serial
import subprocess
import sys
@@ -15,8 +17,12 @@ from collections.abc import Callable, Generator
from contextlib import contextmanager
from typing import Any
+from pathlib import Path
+
+from openpilot.common.time_helpers import system_time_valid
from openpilot.system.hardware.base import LPABase, LPAError, Profile
+GSMA_CI_BUNDLE = str(Path(__file__).parent / "gsma_ci_bundle.pem")
DEFAULT_DEVICE = "/dev/modem_at0"
DEFAULT_BAUD = 9600
@@ -26,6 +32,7 @@ ISDR_AID = "A0000005591010FFFFFFFF8900000100"
MM = "org.freedesktop.ModemManager1"
MM_MODEM = MM + ".Modem"
ES10X_MSS = 120
+HTTP_TIMEOUT = 30
OPEN_ISDR_RETRIES = 10
OPEN_ISDR_RETRY_DELAY_S = 0.25
OPEN_ISDR_RESET_ATTEMPT = 5
@@ -37,10 +44,24 @@ DEBUG = os.environ.get("DEBUG") == "1"
# TLV Tags
TAG_ICCID = 0x5A
TAG_STATUS = 0x80
-TAG_PROFILE_INFO_LIST = 0xBF2D
+TAG_EUICC_INFO = 0xBF20
+TAG_PREPARE_DOWNLOAD = 0xBF21
+TAG_BPP_COMMAND = 0xBF23
+TAG_PROFILE_METADATA = 0xBF25
+TAG_INSTALL_RESULT_DATA = 0xBF27
+TAG_LIST_NOTIFICATION = 0xBF28
TAG_SET_NICKNAME = 0xBF29
+TAG_RETRIEVE_NOTIFICATION = 0xBF2B
+TAG_PROFILE_INFO_LIST = 0xBF2D
+TAG_EUICC_CHALLENGE = 0xBF2E
+TAG_NOTIFICATION_METADATA = 0xBF2F
+TAG_NOTIFICATION_SENT = 0xBF30
TAG_ENABLE_PROFILE = 0xBF31
TAG_DELETE_PROFILE = 0xBF33
+TAG_BPP = 0xBF36
+TAG_PROFILE_INSTALL_RESULT = 0xBF37
+TAG_AUTH_SERVER = 0xBF38
+TAG_CANCEL_SESSION = 0xBF41
TAG_OK = 0xA0
PROFILE_OK = 0x00
@@ -52,6 +73,42 @@ PROFILE_ERROR_CODES = {
0x03: "disallowedByPolicy", 0x04: "wrongProfileReenabling",
PROFILE_CAT_BUSY: "catBusy", 0x06: "undefinedError",
}
+AUTH_SERVER_ERROR_CODES = {
+ 0x01: "eUICCVerificationFailed", 0x02: "eUICCCertificateExpired",
+ 0x03: "eUICCCertificateRevoked", 0x05: "invalidServerSignature",
+ 0x06: "euiccCiPKUnknown", 0x0A: "matchingIdRefused",
+ 0x10: "insufficientMemory",
+}
+BPP_COMMAND_NAMES = {
+ 0: "initialiseSecureChannel", 1: "configureISDP", 2: "storeMetadata",
+ 3: "storeMetadata2", 4: "replaceSessionKeys", 5: "loadProfileElements",
+}
+BPP_ERROR_REASONS = {
+ 1: "incorrectInputValues", 2: "invalidSignature", 3: "invalidTransactionId",
+ 4: "unsupportedCrtValues", 5: "unsupportedRemoteOperationType",
+ 6: "unsupportedProfileClass", 7: "scp03tStructureError", 8: "scp03tSecurityError",
+ 9: "iccidAlreadyExistsOnEuicc", 10: "insufficientMemoryForProfile",
+ 11: "installInterrupted", 12: "peProcessingError", 13: "dataMismatch",
+ 14: "invalidNAA",
+}
+BPP_ERROR_MESSAGES = {
+ 9: "This eSIM profile is already installed on this device.",
+ 10: "Not enough memory on the eUICC to install this profile.",
+ 12: "Profile installation failed. The QR code may have already been used.",
+}
+
+# SGP.22 §5.2.6 — SM-DP+ reason/subject codes mapped to user-friendly messages
+ES9P_ERROR_MESSAGES: dict[tuple[str, str], str] = {
+ ('3.8', '8.2.6'): "This eSIM profile is already installed on another device. Please use a new QR code.",
+ ('3.8', '8.2.1'): "This eSIM profile has expired. Please request a new QR code.",
+ ('3.8', '8.1'): "The SM-DP+ server refused this request.",
+ ('3.1', '8.2.6'): "This eSIM profile has been revoked by the carrier.",
+ ('3.9', '8.2.6'): "This eSIM profile download has already been completed.",
+ ('2.1', '8.8'): "The device is not compatible with this eSIM profile.",
+ ('1.2', '8.1'): "The SM-DP+ server is temporarily unavailable. Try again later.",
+}
+
+NOTIFICATION_OPERATIONS = {0x80: "install", 0x40: "enable", 0x20: "disable", 0x10: "delete"}
STATE_LABELS = {0: "disabled", 1: "enabled", 255: "unknown"}
ICON_LABELS = {0: "jpeg", 1: "png", 255: "unknown"}
@@ -345,6 +402,15 @@ def es10x_command(client: AtClient, data: bytes) -> bytes:
# --- Profile operations ---
+NOTIFICATION: FieldMap = {
+ TAG_STATUS: ("seqNumber", lambda v: int.from_bytes(v, "big")),
+ 0x81: ("profileManagementOperation",
+ lambda v: NOTIFICATION_OPERATIONS.get(next((m for m in NOTIFICATION_OPERATIONS if len(v) >= 2 and v[1] & m), 0), "unknown")),
+ 0x0C: ("notificationAddress", lambda v: v.decode("utf-8", errors="ignore")),
+ TAG_ICCID: ("iccid", tbcd_to_string),
+}
+
+
def decode_profiles(blob: bytes) -> list[dict]:
root = require_tag(blob, TAG_PROFILE_INFO_LIST, "ProfileInfoList")
list_ok = find_tag(root, TAG_OK)
@@ -370,6 +436,278 @@ def set_profile_nickname(client: AtClient, iccid: str, nickname: str) -> None:
raise RuntimeError(f"SetNickname failed with status 0x{code:02X}")
+# --- ES9P HTTP ---
+
+def es9p_request(smdp_address: str, endpoint: str, payload: dict, error_prefix: str = "Request", session: requests.Session | None = None) -> dict:
+ url = f"https://{smdp_address}/gsma/rsp2/es9plus/{endpoint}"
+ headers = {"User-Agent": "gsma-rsp-lpad", "X-Admin-Protocol": "gsma/rsp/v2.3.0", "Content-Type": "application/json"}
+ http = session or requests
+ resp = http.post(url, json=payload, headers=headers, timeout=HTTP_TIMEOUT, verify=GSMA_CI_BUNDLE)
+ resp.raise_for_status()
+ if not resp.content:
+ return {}
+ data = resp.json()
+ if "header" in data and "functionExecutionStatus" in data["header"]:
+ status = data["header"]["functionExecutionStatus"]
+ if status.get("status") == "Failed":
+ sd = status.get("statusCodeData", {})
+ reason = sd.get("reasonCode", "unknown")
+ subject = sd.get("subjectCode", "unknown")
+ msg = ES9P_ERROR_MESSAGES.get((reason, subject),
+ f"{error_prefix} failed: {reason}/{subject} - {sd.get('message', 'unknown')}")
+ raise RuntimeError(msg)
+ return data
+
+
+# --- Notifications ---
+
+def list_notifications(client: AtClient) -> list[dict]:
+ response = es10x_command(client, encode_tlv(TAG_LIST_NOTIFICATION, b""))
+ root = require_tag(response, TAG_LIST_NOTIFICATION, "ListNotificationResponse")
+ metadata_list = find_tag(root, TAG_OK)
+ if metadata_list is None:
+ return []
+ return [decode_struct(value, NOTIFICATION) for tag, value in iter_tlv(metadata_list) if tag == TAG_NOTIFICATION_METADATA]
+
+
+def process_notifications(client: AtClient) -> None:
+ for notification in list_notifications(client):
+ seq_number, smdp_address = notification["seqNumber"], notification["notificationAddress"]
+ try:
+ request = encode_tlv(TAG_RETRIEVE_NOTIFICATION, encode_tlv(TAG_OK, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
+ response = es10x_command(client, request)
+ content = require_tag(require_tag(response, TAG_RETRIEVE_NOTIFICATION, "RetrieveNotificationsListResponse"),
+ TAG_OK, "RetrieveNotificationsListResponse")
+ pending_notif = next((v for t, v in iter_tlv(content) if t in (TAG_PROFILE_INSTALL_RESULT, 0x30)), None)
+ if pending_notif is None:
+ raise RuntimeError("Missing PendingNotification")
+
+ es9p_request(smdp_address, "handleNotification", {"pendingNotification": b64e(pending_notif)}, "HandleNotification")
+
+ response = es10x_command(client, encode_tlv(TAG_NOTIFICATION_SENT, encode_tlv(TAG_STATUS, int_bytes(seq_number))))
+ root = require_tag(response, TAG_NOTIFICATION_SENT, "NotificationSentResponse")
+ if int.from_bytes(require_tag(root, TAG_STATUS, "RemoveNotificationFromList status"), "big") != 0:
+ raise RuntimeError("RemoveNotificationFromList failed")
+ except Exception as e:
+ print(f"notification {seq_number} failed: {e}", file=sys.stderr)
+
+
+# --- Authentication & Download ---
+
+def get_challenge_and_info(client: AtClient) -> tuple[bytes, bytes]:
+ challenge_resp = es10x_command(client, encode_tlv(TAG_EUICC_CHALLENGE, b""))
+ challenge = require_tag(require_tag(challenge_resp, TAG_EUICC_CHALLENGE, "GetEuiccDataResponse"),
+ TAG_STATUS, "challenge in response")
+ info_resp = es10x_command(client, encode_tlv(TAG_EUICC_INFO, b""))
+ require_tag(info_resp, TAG_EUICC_INFO, "GetEuiccInfo1Response")
+ return challenge, info_resp
+
+
+def authenticate_server(client: AtClient, b64_signed1: str, b64_sig1: str, b64_pk_id: str, b64_cert: str, matching_id: str) -> str:
+ tac = bytes([0x35, 0x29, 0x06, 0x11])
+ device_info = encode_tlv(TAG_STATUS, tac) + encode_tlv(0xA1, b"")
+ ctx_inner = encode_tlv(TAG_STATUS, matching_id.encode("utf-8")) + encode_tlv(0xA1, device_info)
+ content = b64d(b64_signed1) + b64d(b64_sig1) + b64d(b64_pk_id) + b64d(b64_cert) + encode_tlv(0xA0, ctx_inner)
+ response = es10x_command(client, encode_tlv(TAG_AUTH_SERVER, content))
+ root = require_tag(response, TAG_AUTH_SERVER, "AuthenticateServerResponse")
+ error_tag = find_tag(root, 0xA1)
+ if error_tag is not None:
+ code = int.from_bytes(error_tag, "big") if error_tag else 0
+ raise RuntimeError(f"AuthenticateServer rejected by eUICC: {AUTH_SERVER_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
+ return b64e(response)
+
+
+def prepare_download(client: AtClient, b64_signed2: str, b64_sig2: str, b64_cert: str, cc: str | None = None) -> str:
+ smdp_signed2 = b64d(b64_signed2)
+ smdp_signature2 = b64d(b64_sig2)
+ smdp_certificate = b64d(b64_cert)
+ smdp_signed2_root = find_tag(smdp_signed2, 0x30)
+ if smdp_signed2_root is None:
+ raise RuntimeError("Invalid smdpSigned2")
+ transaction_id = find_tag(smdp_signed2_root, TAG_STATUS)
+ cc_required_flag = find_tag(smdp_signed2_root, 0x01)
+ if transaction_id is None or cc_required_flag is None:
+ raise RuntimeError("Invalid smdpSigned2")
+ content = smdp_signed2 + smdp_signature2
+ if int.from_bytes(cc_required_flag, "big") != 0:
+ if not cc:
+ raise RuntimeError("Confirmation code required but not provided")
+ content += encode_tlv(0x04, hashlib.sha256(hashlib.sha256(cc.encode("utf-8")).digest() + transaction_id).digest())
+ content += smdp_certificate
+ response = es10x_command(client, encode_tlv(TAG_PREPARE_DOWNLOAD, content))
+ require_tag(response, TAG_PREPARE_DOWNLOAD, "PrepareDownloadResponse")
+ return b64e(response)
+
+
+def _parse_tlv_header_len(data: bytes) -> int:
+ tag_len = 2 if data[0] & 0x1F == 0x1F else 1
+ length_byte = data[tag_len]
+ return tag_len + (1 + (length_byte & 0x7F) if length_byte & 0x80 else 1)
+
+
+def _split_bpp(bpp: bytes) -> list[bytes]:
+ """Split a BoundProfilePackage into APDU chunks per SGP.22 §5.7.6."""
+ root_value = None
+ for tag, value, start, end in iter_tlv(bpp, with_positions=True):
+ if tag == TAG_BPP:
+ root_value = value
+ val_start = start + _parse_tlv_header_len(bpp[start:end])
+ break
+ if root_value is None:
+ raise RuntimeError("Invalid BoundProfilePackage")
+
+ chunks: list[bytes] = []
+ for tag, value, start, end in iter_tlv(root_value, with_positions=True):
+ if tag == TAG_BPP_COMMAND:
+ chunks.append(bpp[0 : val_start + end])
+ elif tag in (0xA0, 0xA2):
+ chunks.append(bpp[val_start + start : val_start + end])
+ elif tag in (0xA1, 0xA3):
+ hdr_len = _parse_tlv_header_len(root_value[start:end])
+ chunks.append(bpp[val_start + start : val_start + start + hdr_len])
+ for _, _, cs, ce in iter_tlv(value, with_positions=True):
+ chunks.append(value[cs:ce])
+ return chunks
+
+
+def _parse_install_result(response: bytes) -> dict[str, Any] | None:
+ """Parse a ProfileInstallResult from an APDU response, or None if not present."""
+ root = find_tag(response, TAG_PROFILE_INSTALL_RESULT)
+ if not root:
+ return None
+ result_data = find_tag(root, TAG_INSTALL_RESULT_DATA)
+ if not result_data:
+ return None
+ result: dict[str, Any] = {"seqNumber": 0, "success": False, "bppCommandId": None, "errorReason": None}
+ notif_meta = find_tag(result_data, TAG_NOTIFICATION_METADATA)
+ if notif_meta:
+ seq_num = find_tag(notif_meta, TAG_STATUS)
+ if seq_num:
+ result["seqNumber"] = int.from_bytes(seq_num, "big")
+ final_result = find_tag(result_data, 0xA2)
+ if final_result:
+ for tag, value in iter_tlv(final_result):
+ if tag == 0xA0:
+ result["success"] = True
+ elif tag == 0xA1:
+ bpp_cmd = find_tag(value, TAG_STATUS)
+ if bpp_cmd:
+ result["bppCommandId"] = int.from_bytes(bpp_cmd, "big")
+ err = find_tag(value, 0x81)
+ if err:
+ result["errorReason"] = int.from_bytes(err, "big")
+ return result
+
+
+def load_bpp(client: AtClient, b64_bpp: str) -> dict:
+ bpp = b64d(b64_bpp)
+ result = None
+ for chunk in _split_bpp(bpp):
+ response = es10x_command(client, chunk)
+ if response:
+ result = _parse_install_result(response) or result
+
+ if result is None:
+ raise RuntimeError("Profile installation failed: no result from eUICC")
+ if not result["success"] and result["errorReason"] is not None:
+ msg = BPP_ERROR_MESSAGES.get(result["errorReason"])
+ if not msg:
+ cmd_name = BPP_COMMAND_NAMES.get(result["bppCommandId"], f"unknown({result['bppCommandId']})")
+ err_name = BPP_ERROR_REASONS.get(result["errorReason"], f"unknown({result['errorReason']})")
+ msg = f"Profile installation failed at {cmd_name}: {err_name}"
+ raise RuntimeError(msg)
+ if not result["success"]:
+ raise RuntimeError("Profile installation failed: no result from eUICC")
+ return result
+
+
+def parse_metadata(b64_metadata: str) -> dict:
+ root = find_tag(b64d(b64_metadata), TAG_PROFILE_METADATA)
+ if root is None:
+ raise RuntimeError("Invalid profileMetadata")
+ return decode_struct(root, PROFILE)
+
+
+def cancel_session(client: AtClient, transaction_id: bytes, reason: int = 127) -> str:
+ content = encode_tlv(0x80, transaction_id) + encode_tlv(0x81, bytes([reason]))
+ response = es10x_command(client, encode_tlv(TAG_CANCEL_SESSION, content))
+ return b64e(response)
+
+
+def parse_lpa_activation_code(activation_code: str) -> tuple[str, str]:
+ """Parse 'LPA:1$smdp.example.com$MATCHING-ID' into (smdp_address, matching_id)."""
+ if not activation_code.startswith("LPA:"):
+ raise ValueError("Invalid activation code format")
+ parts = activation_code[4:].split("$")
+ if len(parts) != 3:
+ raise ValueError("Invalid activation code format")
+ return parts[1], parts[2]
+
+
+def _b64_field(data: dict, key: str) -> str:
+ return base64_trim(data[key])
+
+
+def _cancel_session_safe(client: AtClient, smdp: str, tx_id: str, session: requests.Session) -> None:
+ b64_cancel = ""
+ try:
+ b64_cancel = cancel_session(client, b64d(tx_id))
+ except Exception:
+ pass
+ try:
+ es9p_request(smdp, "cancelSession", {"transactionId": tx_id, "cancelSessionResponse": b64_cancel}, "CancelSession", session=session)
+ except Exception:
+ pass
+
+
+def download_profile(client: AtClient, activation_code: str) -> str:
+ """Download and install an eSIM profile. Returns the ICCID of the installed profile."""
+ if not system_time_valid():
+ raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
+ smdp, matching_id = parse_lpa_activation_code(activation_code)
+ challenge, euicc_info = get_challenge_and_info(client)
+ session = requests.Session()
+ tx_id = None
+
+ try:
+ # step 1: initiate authentication
+ auth = es9p_request(smdp, "initiateAuthentication", {
+ "smdpAddress": smdp, "euiccChallenge": b64e(challenge),
+ "euiccInfo1": b64e(euicc_info), "matchingId": matching_id,
+ }, "Authentication", session=session)
+ tx_id = _b64_field(auth, "transactionId")
+
+ # step 2: authenticate server
+ b64_auth = authenticate_server(client,
+ _b64_field(auth, "serverSigned1"), _b64_field(auth, "serverSignature1"),
+ _b64_field(auth, "euiccCiPKIdToBeUsed"), _b64_field(auth, "serverCertificate"),
+ matching_id)
+
+ # step 3: authenticate client + get metadata
+ cli = es9p_request(smdp, "authenticateClient", {
+ "transactionId": tx_id, "authenticateServerResponse": b64_auth,
+ }, "Authentication", session=session)
+ iccid = parse_metadata(_b64_field(cli, "profileMetadata"))["iccid"]
+
+ # step 4: prepare download
+ b64_prep = prepare_download(client,
+ _b64_field(cli, "smdpSigned2"), _b64_field(cli, "smdpSignature2"),
+ _b64_field(cli, "smdpCertificate"))
+
+ # step 5: get and install bound profile package
+ bpp = es9p_request(smdp, "getBoundProfilePackage", {
+ "transactionId": tx_id, "prepareDownloadResponse": b64_prep,
+ }, "GetBoundProfilePackage", session=session)
+ load_bpp(client, _b64_field(bpp, "boundProfilePackage"))
+ return iccid
+ except Exception:
+ if tx_id:
+ _cancel_session_safe(client, smdp, tx_id, session)
+ raise
+ finally:
+ session.close()
+
+
class TiciLPA(LPABase):
def __init__(self):
if hasattr(self, '_client'):
@@ -409,6 +747,12 @@ class TiciLPA(LPABase):
def get_active_profile(self) -> Profile | None:
return None
+ def process_notifications(self) -> None:
+ if not system_time_valid():
+ raise RuntimeError("System time is not set; TLS certificate validation requires a valid clock")
+ with self._acquire_channel():
+ process_notifications(self._client)
+
def delete_profile(self, iccid: str) -> None:
if self.is_comma_profile(iccid):
raise LPAError("refusing to delete a comma profile")
@@ -420,7 +764,10 @@ class TiciLPA(LPABase):
raise LPAError(f"DeleteProfile failed: {PROFILE_ERROR_CODES.get(code, 'unknown')} (0x{code:02X})")
def download_profile(self, qr: str, nickname: str | None = None) -> None:
- return None
+ with self._acquire_channel():
+ iccid = download_profile(self._client, qr)
+ if nickname and iccid:
+ set_profile_nickname(self._client, iccid, nickname)
def nickname_profile(self, iccid: str, nickname: str) -> None:
with self._acquire_channel():
diff --git a/system/updated/tests/test_updated.py b/system/updated/tests/test_updated.py
new file mode 100644
index 0000000000..d36d4dd4e1
--- /dev/null
+++ b/system/updated/tests/test_updated.py
@@ -0,0 +1,38 @@
+import pytest
+
+from openpilot.common.params import Params
+from openpilot.system.updated.updated import Updater
+
+
+@pytest.mark.parametrize(("device_type", "branch", "expected"), [
+ ("tizi", "release3", "release-tizi"),
+ ("tizi", "release3-staging", "release-tizi-staging"),
+ ("mici", "release3", "release-mici"),
+ ("mici", "release3-staging", "release-mici-staging"),
+])
+def test_target_branch_migration_from_current_branch(mocker, device_type, branch, expected):
+ params = Params()
+ params.remove("UpdaterTargetBranch")
+
+ mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+ mocker.patch.object(Updater, "get_branch", return_value=branch)
+
+ assert Updater().target_branch == expected
+
+
+@pytest.mark.parametrize(("device_type", "branch", "expected"), [
+ ("tizi", "release3", "release-tizi"),
+ ("tizi", "release3-staging", "release-tizi-staging"),
+ ("mici", "release3", "release-mici"),
+ ("mici", "release3-staging", "release-mici-staging"),
+])
+def test_target_branch_migration_from_param(mocker, device_type, branch, expected):
+ params = Params()
+ params.put("UpdaterTargetBranch", branch)
+
+ mocker.patch("openpilot.system.updated.updated.HARDWARE.get_device_type", return_value=device_type)
+
+ try:
+ assert Updater().target_branch == expected
+ finally:
+ params.remove("UpdaterTargetBranch")
diff --git a/system/version.py b/system/version.py
index ae6ac1b13a..2d4fc8f765 100755
--- a/system/version.py
+++ b/system/version.py
@@ -24,6 +24,10 @@ SP_BRANCH_MIGRATIONS = {
("tizi", "staging-c3-new"): "staging",
("tizi", "dev-c3-new"): "dev",
("tizi", "master-dev-c3-new"): "master-dev",
+ ("tizi", "release3"): "release-tizi",
+ ("tizi", "release3-staging"): "release-tizi-staging",
+ ("mici", "release3"): "release-mici",
+ ("mici", "release3-staging"): "release-mici-staging",
}
BUILD_METADATA_FILENAME = "build.json"
diff --git a/tools/jotpluggler/app.h b/tools/jotpluggler/app.h
index 71f71f2d9f..872a6973d7 100644
--- a/tools/jotpluggler/app.h
+++ b/tools/jotpluggler/app.h
@@ -276,7 +276,10 @@ struct RouteIdentifier {
std::string display_slice() const {
const int begin = slice_explicit ? slice_begin : available_begin;
const int end = slice_explicit ? slice_end : available_end;
- if (end < 0 || end == begin) {
+ if (end < 0) {
+ return std::to_string(begin) + ":";
+ }
+ if (end == begin) {
return std::to_string(begin);
}
return std::to_string(begin) + ":" + std::to_string(end);
@@ -378,7 +381,7 @@ public:
StreamAccumulator &operator=(const StreamAccumulator &) = delete;
void setDbcName(const std::string &dbc_name);
- void appendEvent(cereal::Event::Which which, kj::ArrayPtr data);
+ void appendEvent(kj::ArrayPtr data);
void appendCanFrames(CanServiceKind service, const std::vector &frames);
StreamExtractBatch takeBatch();
const std::string &carFingerprint() const;
diff --git a/tools/jotpluggler/runtime.cc b/tools/jotpluggler/runtime.cc
index 47344c5519..6eb2be80e8 100644
--- a/tools/jotpluggler/runtime.cc
+++ b/tools/jotpluggler/runtime.cc
@@ -598,9 +598,7 @@ struct StreamPoller::Impl {
}
kj::ArrayPtr data(reinterpret_cast(msg->getData()),
size / sizeof(capnp::word));
- capnp::FlatArrayMessageReader event_reader(data);
- const cereal::Event::Reader event = event_reader.getRoot();
- accumulator->appendEvent(event.which(), data);
+ accumulator->appendEvent(data);
received_messages.fetch_add(1);
}
}
diff --git a/tools/jotpluggler/session.cc b/tools/jotpluggler/session.cc
index 22dd7dd463..173df7bc04 100644
--- a/tools/jotpluggler/session.cc
+++ b/tools/jotpluggler/session.cc
@@ -355,7 +355,7 @@ bool apply_route_slice_change(AppSession *session, UiState *state, std::string_v
int begin = 0;
int end = 0;
if (!parse_slice_spec(slice_text, &begin, &end)) {
- state->error_text = "Slice must be N or N:M.";
+ state->error_text = "Slice must be N, N:, or N:M.";
state->open_error_popup = true;
return false;
}
diff --git a/tools/jotpluggler/sketch_layout.cc b/tools/jotpluggler/sketch_layout.cc
index bc110b534f..d9622dde6d 100644
--- a/tools/jotpluggler/sketch_layout.cc
+++ b/tools/jotpluggler/sketch_layout.cc
@@ -3,6 +3,7 @@
#include "tools/jotpluggler/common.h"
#include
+#include
#include
#include
@@ -205,6 +206,18 @@ struct LoadStats {
mutable std::mutex progress_mutex;
};
+// Skip individual messages that our local Cap'n Proto schema can't project,
+// such as logs recorded by a newer build.
+template
+void with_parseable_event(kj::ArrayPtr data, Fn &&fn) {
+ try {
+ capnp::FlatArrayMessageReader event_reader(data);
+ fn(event_reader.getRoot());
+ } catch (const kj::Exception &) {
+ return;
+ }
+}
+
std::string curve_label(std::string_view series_name) {
return std::string(series_name.empty() ? std::string_view{"plot"} : series_name);
}
@@ -666,11 +679,11 @@ std::vector extract_segment_timeline(const std::vector &ev
if (event_record.which != cereal::Event::Which::SELFDRIVE_STATE) {
continue;
}
- capnp::FlatArrayMessageReader event_reader(event_record.data);
- const cereal::Event::Reader event = event_reader.getRoot();
- const auto sd = event.getSelfdriveState();
- const double mono_time = static_cast(event.getLogMonoTime()) / 1.0e9;
- append_timeline_entry(&timeline, mono_time, alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
+ with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
+ const auto sd = event.getSelfdriveState();
+ const double mono_time = static_cast(event.getLogMonoTime()) / 1.0e9;
+ append_timeline_entry(&timeline, mono_time, alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
+ });
}
return timeline;
@@ -682,9 +695,9 @@ std::vector extract_segment_logs(const std::vector &events) {
std::string last_alert_key;
for (const Event &event_record : events) {
- capnp::FlatArrayMessageReader event_reader(event_record.data);
- const cereal::Event::Reader event = event_reader.getRoot();
- append_log_event(event_record.which, event, 0.0, &logs, &last_alert_key);
+ with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
+ append_log_event(event_record.which, event, 0.0, &logs, &last_alert_key);
+ });
}
return logs;
@@ -694,9 +707,9 @@ RouteMetadata extract_segment_metadata(const std::vector &events) {
RouteMetadata metadata;
for (const Event &event_record : events) {
if (event_record.which != cereal::Event::Which::CAR_PARAMS) continue;
- capnp::FlatArrayMessageReader event_reader(event_record.data);
- const cereal::Event::Reader event = event_reader.getRoot();
- metadata.car_fingerprint = event.getCarParams().getCarFingerprint().cStr();
+ with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
+ metadata.car_fingerprint = event.getCarParams().getCarFingerprint().cStr();
+ });
if (!metadata.car_fingerprint.empty()) break;
}
return metadata;
@@ -1263,24 +1276,20 @@ void append_fast_node(const ResolvedNode &node,
}
}
-void append_event_fast(cereal::Event::Which which,
- int32_t eidx_segnum,
- kj::ArrayPtr data,
- const SchemaIndex &schema,
- const dbc::Database *can_dbc,
- bool skip_raw_can,
- double time_offset,
- SeriesAccumulator *series) {
- if (eidx_segnum != -1) {
- return;
- }
+void append_event_fast_reader(cereal::Event::Which which,
+ const cereal::Event::Reader &event,
+ const SchemaIndex &schema,
+ const dbc::Database *can_dbc,
+ bool skip_raw_can,
+ double time_offset,
+ SeriesAccumulator *series) {
const uint16_t which_index = static_cast(which);
if (which_index >= schema.by_which.size() || !schema.by_which[which_index].has_value()) {
return;
}
const ResolvedService &service = *schema.by_which[which_index];
- capnp::FlatArrayMessageReader event_reader(data);
- const cereal::Event::Reader event = event_reader.getRoot();
+ const capnp::DynamicStruct::Reader dynamic_event(event);
+ const capnp::DynamicValue::Reader payload = dynamic_event.get(service.union_field);
const double tm = static_cast(event.getLogMonoTime()) / 1.0e9 - time_offset;
append_fixed_scalar_point(&series->fixed_series[static_cast(service.valid_slot)],
tm,
@@ -1329,8 +1338,23 @@ void append_event_fast(cereal::Event::Which which,
}
}
- const capnp::DynamicStruct::Reader dynamic_event(event);
- append_fast_node(service.payload, dynamic_event.get(service.union_field), tm, series);
+ append_fast_node(service.payload, payload, tm, series);
+}
+
+void append_event_fast(cereal::Event::Which which,
+ int32_t eidx_segnum,
+ kj::ArrayPtr data,
+ const SchemaIndex &schema,
+ const dbc::Database *can_dbc,
+ bool skip_raw_can,
+ double time_offset,
+ SeriesAccumulator *series) {
+ if (eidx_segnum != -1) {
+ return;
+ }
+ with_parseable_event(data, [&](const cereal::Event::Reader &event) {
+ append_event_fast_reader(which, event, schema, can_dbc, skip_raw_can, time_offset, series);
+ });
}
void append_events_fast_range(const std::vector &events,
@@ -1987,10 +2011,7 @@ struct StreamAccumulator::Impl {
return;
}
detected_dbc_name = next_dbc;
- can_dbc.reset();
- if (!detected_dbc_name.empty()) {
- can_dbc.emplace(resolve_dbc_path(detected_dbc_name));
- }
+ can_dbc = load_dbc_by_name(detected_dbc_name);
}
};
@@ -2008,35 +2029,35 @@ void StreamAccumulator::setDbcName(const std::string &dbc_name) {
impl_->refresh_dbc();
}
-void StreamAccumulator::appendEvent(cereal::Event::Which which, kj::ArrayPtr data) {
- capnp::FlatArrayMessageReader event_reader(data);
- const cereal::Event::Reader event = event_reader.getRoot();
- const double boot_time = static_cast(event.getLogMonoTime()) / 1.0e9;
- if (!impl_->time_offset.has_value()) {
- impl_->time_offset = boot_time;
- }
- if (which == cereal::Event::Which::CAR_PARAMS) {
- const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr();
- if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) {
- impl_->car_fingerprint = fingerprint;
- impl_->refresh_dbc();
+void StreamAccumulator::appendEvent(kj::ArrayPtr data) {
+ with_parseable_event(data, [&](const cereal::Event::Reader &event) {
+ const cereal::Event::Which which = event.which();
+ const double boot_time = static_cast(event.getLogMonoTime()) / 1.0e9;
+ if (!impl_->time_offset.has_value()) {
+ impl_->time_offset = boot_time;
+ }
+ if (which == cereal::Event::Which::CAR_PARAMS) {
+ const std::string fingerprint = event.getCarParams().getCarFingerprint().cStr();
+ if (!fingerprint.empty() && fingerprint != impl_->car_fingerprint) {
+ impl_->car_fingerprint = fingerprint;
+ impl_->refresh_dbc();
+ }
}
- }
- append_event_fast(which,
- -1,
- data,
- impl_->schema,
- impl_->can_dbc ? &*impl_->can_dbc : nullptr,
- true,
- *impl_->time_offset,
- &impl_->series);
- append_log_event(which, event, *impl_->time_offset, &impl_->logs, &impl_->last_alert_key);
- if (which == cereal::Event::Which::SELFDRIVE_STATE) {
- const auto sd = event.getSelfdriveState();
- append_timeline_entry(&impl_->timeline, boot_time - *impl_->time_offset,
- alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
- }
+ append_event_fast_reader(which,
+ event,
+ impl_->schema,
+ impl_->can_dbc ? &*impl_->can_dbc : nullptr,
+ impl_->can_dbc.has_value(),
+ *impl_->time_offset,
+ &impl_->series);
+ append_log_event(which, event, *impl_->time_offset, &impl_->logs, &impl_->last_alert_key);
+ if (which == cereal::Event::Which::SELFDRIVE_STATE) {
+ const auto sd = event.getSelfdriveState();
+ append_timeline_entry(&impl_->timeline, boot_time - *impl_->time_offset,
+ alert_status_to_timeline_type(sd.getAlertStatus(), sd.getEnabled()));
+ }
+ });
}
void StreamAccumulator::appendCanFrames(CanServiceKind service, const std::vector &frames) {
@@ -2126,13 +2147,11 @@ RouteData load_route_data(const std::string &route_name,
const RouteMetadata metadata = detect_route_metadata(segments, route.selector);
const std::string resolved_dbc = !dbc_name.empty() ? dbc_name : detect_dbc_for_fingerprint(metadata.car_fingerprint);
- const std::optional can_dbc = resolved_dbc.empty()
- ? std::nullopt
- : std::optional(std::in_place, resolve_dbc_path(resolved_dbc));
+ const std::optional can_dbc = load_dbc_by_name(resolved_dbc);
const SchemaIndex &schema = SchemaIndex::instance();
LoadedRouteArtifacts artifacts = load_route_series_parallel(segments, schema, can_dbc ? &*can_dbc : nullptr,
- route.selector, !resolved_dbc.empty(), &stats);
+ route.selector, can_dbc.has_value(), &stats);
RouteData route_data = build_route_data(std::move(artifacts.series),
std::move(artifacts.can_messages),
std::move(artifacts.logs),
diff --git a/tools/op.sh b/tools/op.sh
index 3b02602619..dccf080829 100755
--- a/tools/op.sh
+++ b/tools/op.sh
@@ -26,15 +26,6 @@ function op_install() {
echo -e " ↳ [${GREEN}✔${NC}] op installed successfully. Open a new shell to use it."
}
-function loge() {
- if [[ -f "$LOG_FILE" ]]; then
- # error type
- echo "$1" >> $LOG_FILE
- # error log
- echo "$2" >> $LOG_FILE
- fi
-}
-
function retry() {
local attempts=$1
shift
@@ -148,13 +139,11 @@ function op_check_os() {
;;
* )
echo -e " ↳ [${RED}✗${NC}] Incompatible Ubuntu version $VERSION_CODENAME detected!"
- loge "ERROR_INCOMPATIBLE_UBUNTU" "$VERSION_CODENAME"
return 1
;;
esac
else
echo -e " ↳ [${RED}✗${NC}] No /etc/os-release on your system. Make sure you're running on Ubuntu, or similar!"
- loge "ERROR_UNKNOWN_UBUNTU"
return 1
fi
@@ -162,7 +151,6 @@ function op_check_os() {
echo -e " ↳ [${GREEN}✔${NC}] macOS detected."
else
echo -e " ↳ [${RED}✗${NC}] OS type $OSTYPE not supported!"
- loge "ERROR_UNKNOWN_OS" "$OSTYPE"
return 1
fi
}
@@ -210,7 +198,6 @@ function op_setup() {
SETUP_SCRIPT="tools/setup_dependencies.sh"
if ! $OPENPILOT_ROOT/$SETUP_SCRIPT; then
echo -e " ↳ [${RED}✗${NC}] Dependencies installation failed!"
- loge "ERROR_DEPENDENCIES_INSTALLATION"
return 1
fi
et="$(date +%s)"
@@ -222,7 +209,6 @@ function op_setup() {
st="$(date +%s)"
if ! retry 3 git submodule update --jobs 4 --init --recursive; then
echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!"
- loge "ERROR_GIT_SUBMODULES"
return 1
fi
et="$(date +%s)"
@@ -232,7 +218,6 @@ function op_setup() {
st="$(date +%s)"
if ! retry 3 git lfs pull; then
echo -e " ↳ [${RED}✗${NC}] Pulling git lfs files failed!"
- loge "ERROR_GIT_LFS"
return 1
fi
et="$(date +%s)"
@@ -468,7 +453,6 @@ function _op() {
-d | --dir ) shift 1; OPENPILOT_ROOT="$1"; shift 1 ;;
--dry ) shift 1; DRY="1" ;;
-n | --no-verify ) shift 1; NO_VERIFY="1" ;;
- -l | --log ) shift 1; LOG_FILE="$1" ; shift 1 ;;
esac
# parse Commands
diff --git a/tools/setup.sh b/tools/setup.sh
index fd7efcee90..dafd466ef9 100755
--- a/tools/setup.sh
+++ b/tools/setup.sh
@@ -33,39 +33,6 @@ cat << 'EOF'
EOF
}
-function sentry_send_event() {
- SENTRY_KEY=dd0cba62ba0ac07ff9f388f8f1e6a7f4
- SENTRY_URL=https://sentry.io/api/4507726145781760/store/
-
- EVENT=$1
- EVENT_TYPE=${2:-$EVENT}
- EVENT_LOG=${3:-"NA"}
-
- PLATFORM=$(uname -s)
- ARCH=$(uname -m)
- SYSTEM=$(uname -a)
- if [[ $PLATFORM == "Darwin" ]]; then
- OS="macos"
- elif [[ $PLATFORM == "Linux" ]]; then
- OS="linux"
- fi
-
- if [[ $ARCH == armv8* ]] || [[ $ARCH == arm64* ]] || [[ $ARCH == aarch64* ]]; then
- ARCH="aarch64"
- elif [[ $ARCH == "x86_64" ]] || [[ $ARCH == i686* ]]; then
- ARCH="x86"
- fi
-
- PYTHON_VERSION=$(echo $(python3 --version 2> /dev/null || echo "NA"))
- BRANCH=$(echo $(git -C $OPENPILOT_ROOT rev-parse --abbrev-ref HEAD 2> /dev/null || echo "NA"))
- COMMIT=$(echo $(git -C $OPENPILOT_ROOT rev-parse HEAD 2> /dev/null || echo "NA"))
-
- curl -s -o /dev/null -X POST -g --data "{ \"exception\": { \"values\": [{ \"type\": \"$EVENT\" }] }, \"tags\" : { \"event_type\" : \"$EVENT_TYPE\", \"event_log\" : \"$EVENT_LOG\", \"os\" : \"$OS\", \"arch\" : \"$ARCH\", \"python_version\" : \"$PYTHON_VERSION\" , \"git_branch\" : \"$BRANCH\", \"git_commit\" : \"$COMMIT\", \"system\" : \"$SYSTEM\" } }" \
- -H 'Content-Type: application/json' \
- -H "X-Sentry-Auth: Sentry sentry_version=7, sentry_key=$SENTRY_KEY, sentry_client=op_setup/0.1" \
- $SENTRY_URL 2> /dev/null
-}
-
function check_stdin() {
if [ -t 0 ]; then
INTERACTIVE=1
@@ -131,7 +98,6 @@ function check_git() {
echo "Checking for git..."
if ! command -v "git" > /dev/null 2>&1; then
echo -e " ↳ [${RED}✗${NC}] git not found on your system, can't continue!"
- sentry_send_event "SETUP_FAILURE" "ERROR_GIT_NOT_FOUND"
return 1
else
echo -e " ↳ [${GREEN}✔${NC}] git found.\n"
@@ -150,7 +116,6 @@ function git_clone() {
fi
echo -e " ↳ [${RED}✗${NC}] failed to clone openpilot!"
- sentry_send_event "SETUP_FAILURE" "ERROR_GIT_CLONE"
return 1
}
@@ -159,18 +124,9 @@ function install_with_op() {
$OPENPILOT_ROOT/tools/op.sh install
$OPENPILOT_ROOT/tools/op.sh post-commit
- LOG_FILE=$(mktemp)
-
- if ! $OPENPILOT_ROOT/tools/op.sh --log $LOG_FILE setup; then
+ if ! $OPENPILOT_ROOT/tools/op.sh setup; then
echo -e "\n[${RED}✗${NC}] failed to install openpilot!"
-
- ERROR_TYPE="$(cat "$LOG_FILE" | sed '1p;d')"
- ERROR_LOG="$(cat "$LOG_FILE" | sed '2p;d')"
- sentry_send_event "SETUP_FAILURE" "$ERROR_TYPE" "$ERROR_LOG" || true
-
return 1
- else
- sentry_send_event "SETUP_SUCCESS" || true
fi
echo -e "\n----------------------------------------------------------------------"
diff --git a/uv.lock b/uv.lock
index 7ecb51f5d1..f8278c1368 100644
--- a/uv.lock
+++ b/uv.lock
@@ -116,12 +116,12 @@ wheels = [
[[package]]
name = "bzip2"
version = "1.0.8"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2#1ddfd3eb7b9e30a957c263930e1b0660e5dce6d1" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=bzip2&rev=release-bzip2#13755b73dbcda1b186641fcccce90d55f815d6bc" }
[[package]]
name = "capnproto"
version = "1.0.1"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto#6e99db11a1dc5dfa74be40d1e0666ebe10c8e0d7" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=capnproto&rev=release-capnproto#eba2fe8b8208b5408fbda1bc0104a91e4375aee3" }
[[package]]
name = "casadi"
@@ -359,6 +359,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" },
]
+[[package]]
+name = "deepmerge"
+version = "2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a8/3a/b0ba594708f1ad0bc735884b3ad854d3ca3bdc1d741e56e40bbda6263499/deepmerge-2.0.tar.gz", hash = "sha256:5c3d86081fbebd04dd5de03626a0607b809a98fb6ccba5770b62466fe940ff20", size = 19890, upload-time = "2024-08-30T05:31:50.308Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
+]
+
[[package]]
name = "dnspython"
version = "2.8.0"
@@ -371,7 +380,7 @@ wheels = [
[[package]]
name = "eigen"
version = "3.4.0"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen#891c42d8029b2a633f3aca7f60cc7aa4b5305405" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=eigen&rev=release-eigen#9157467a9e343d876e85f6187eae8c974fe3d83f" }
[[package]]
name = "execnet"
@@ -385,7 +394,7 @@ wheels = [
[[package]]
name = "ffmpeg"
version = "7.1.0"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg#8261317427e81a0fa1f53a7ef77f15004ec78889" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ffmpeg&rev=release-ffmpeg#4be3ad687902199df76b78cc8cf07f61e69ec266" }
[[package]]
name = "fonttools"
@@ -432,24 +441,12 @@ wheels = [
[[package]]
name = "gcc-arm-none-eabi"
version = "13.2.1"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#fd995de677db114e2862cf4ed245ca9a17536668" }
-
-[[package]]
-name = "ghp-import"
-version = "2.1.0"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "python-dateutil" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
-]
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=gcc-arm-none-eabi&rev=release-gcc-arm-none-eabi#0e1ae2548977f6cd78c51d4d0c16ebd1863241b8" }
[[package]]
name = "git-lfs"
version = "3.6.1"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs#9fdbe7eb0257d7a13851ed4baa52fbccbe7e2e9d" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=git-lfs&rev=release-git-lfs#ab3064b6e7df110e32aa7748689cb43b26f07b54" }
[[package]]
name = "google-crc32c"
@@ -498,7 +495,7 @@ wheels = [
[[package]]
name = "imgui"
version = "1.92.7"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui#f3d874be2f3aa44869ffd4775e0957e986a30a68" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=imgui&rev=release-imgui#58d66087adacabb2bb4e56e74ebdea7d55c78e34" }
[[package]]
name = "iniconfig"
@@ -578,12 +575,12 @@ wheels = [
[[package]]
name = "libjpeg"
version = "3.1.0"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg#d90bc630661092de49428bfc3a82a371ee35a889" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libjpeg&rev=release-libjpeg#71f7a3f2aaccdc0612d93fac858b78f35bc2a565" }
[[package]]
name = "libusb"
version = "1.0.29"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb#6562b0138726a380368d68a6ac5f6e36d6aea2da" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libusb&rev=release-libusb#222120c19c857d6d0a681aff2e335c829ffcf89c" }
[[package]]
name = "libusb1"
@@ -599,7 +596,7 @@ wheels = [
[[package]]
name = "libyuv"
version = "1922.0"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv#22b976c39a3f2607ef5458056b1a10558da0e85f" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv#febc42742ebf25429575caf784adecc6e516b892" }
[[package]]
name = "markdown"
@@ -655,15 +652,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/76/934db220026b5fef85f45d51a738b91dea7d70207581063cd9bd8fafcf74/matplotlib-3.10.8-cp312-cp312-win_arm64.whl", hash = "sha256:3c624e43ed56313651bc18a47f838b60d7b8032ed348911c54906b130b20071b", size = 8012751, upload-time = "2025-12-10T22:55:42.684Z" },
]
-[[package]]
-name = "mergedeep"
-version = "1.3.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
-]
-
[[package]]
name = "metadrive-simulator"
version = "0.4.2.3"
@@ -674,44 +662,6 @@ dependencies = [
{ name = "panda3d-gltf" },
]
-[[package]]
-name = "mkdocs"
-version = "1.6.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "click" },
- { name = "colorama", marker = "sys_platform == 'win32'" },
- { name = "ghp-import" },
- { name = "jinja2" },
- { name = "markdown" },
- { name = "markupsafe" },
- { name = "mergedeep" },
- { name = "mkdocs-get-deps" },
- { name = "packaging" },
- { name = "pathspec" },
- { name = "pyyaml" },
- { name = "pyyaml-env-tag" },
- { name = "watchdog" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
-]
-
-[[package]]
-name = "mkdocs-get-deps"
-version = "0.2.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "mergedeep" },
- { name = "platformdirs" },
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" },
-]
-
[[package]]
name = "mpmath"
version = "1.3.0"
@@ -751,7 +701,7 @@ wheels = [
[[package]]
name = "ncurses"
version = "6.5"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses#b733e08a93873e8d8ac47caabc2eb64a425f7146" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses#e78a693655261b101325aaa5b3cd9f1eb35f496b" }
[[package]]
name = "numpy"
@@ -849,7 +799,7 @@ dev = [
]
docs = [
{ name = "jinja2" },
- { name = "mkdocs" },
+ { name = "zensical" },
]
testing = [
{ name = "codespell" },
@@ -899,7 +849,6 @@ requires-dist = [
{ name = "libyuv", git = "https://github.com/commaai/dependencies.git?subdirectory=libyuv&rev=release-libyuv" },
{ name = "matplotlib", marker = "extra == 'dev'" },
{ name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", git = "https://github.com/commaai/metadrive.git?rev=minimal" },
- { name = "mkdocs", marker = "extra == 'docs'" },
{ name = "ncurses", git = "https://github.com/commaai/dependencies.git?subdirectory=ncurses&rev=release-ncurses" },
{ name = "numpy", specifier = ">=2.0" },
{ name = "opencv-python-headless", marker = "extra == 'dev'" },
@@ -932,6 +881,7 @@ requires-dist = [
{ name = "ty", marker = "extra == 'testing'" },
{ name = "websocket-client" },
{ name = "xattr" },
+ { name = "zensical", marker = "extra == 'docs'" },
{ name = "zeromq", git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq" },
{ name = "zstandard" },
{ name = "zstd", git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd" },
@@ -940,11 +890,11 @@ provides-extras = ["docs", "testing", "dev", "tools"]
[[package]]
name = "packaging"
-version = "26.0"
+version = "26.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
]
[[package]]
@@ -986,15 +936,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/5d/3744c6550dddf933785a37cdd4a9921fe13284e6d115b5a2637fe390f158/panda3d_simplepbr-0.13.1-py3-none-any.whl", hash = "sha256:cda41cb57cff035b851646956cfbdcc408bee42511dabd4f2d7bd4fbf48c57a9", size = 2457097, upload-time = "2025-03-30T16:57:39.729Z" },
]
-[[package]]
-name = "pathspec"
-version = "1.0.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
-]
-
[[package]]
name = "pillow"
version = "12.2.0"
@@ -1014,15 +955,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
]
-[[package]]
-name = "platformdirs"
-version = "4.9.4"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
-]
-
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -1186,6 +1118,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/ec/6e02b2561d056ea5b33046e3cad21238e6a9097b97d6ccc0fbe52b50c858/pylibsrtp-1.0.0-cp310-abi3-win_arm64.whl", hash = "sha256:2696bdb2180d53ac55d0eb7b58048a2aa30cd4836dd2ca683669889137a94d2a", size = 1159246, upload-time = "2025-10-13T16:12:30.285Z" },
]
+[[package]]
+name = "pymdown-extensions"
+version = "10.21.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" },
+]
+
[[package]]
name = "pyopenssl"
version = "26.0.0"
@@ -1322,18 +1267,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
]
-[[package]]
-name = "pyyaml-env-tag"
-version = "1.1"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "pyyaml" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
-]
-
[[package]]
name = "pyzmq"
version = "27.1.0"
@@ -1411,27 +1344,27 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.15.9"
+version = "0.15.10"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/e6/97/e9f1ca355108ef7194e38c812ef40ba98c7208f47b13ad78d023caa583da/ruff-0.15.9.tar.gz", hash = "sha256:29cbb1255a9797903f6dde5ba0188c707907ff44a9006eb273b5a17bfa0739a2", size = 4617361, upload-time = "2026-04-02T18:17:20.829Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/d9/aa3f7d59a10ef6b14fe3431706f854dbf03c5976be614a9796d36326810c/ruff-0.15.10.tar.gz", hash = "sha256:d1f86e67ebfdef88e00faefa1552b5e510e1d35f3be7d423dc7e84e63788c94e", size = 4631728, upload-time = "2026-04-09T14:06:09.884Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/0b/1f/9cdfd0ac4b9d1e5a6cf09bedabdf0b56306ab5e333c85c87281273e7b041/ruff-0.15.9-py3-none-linux_armv6l.whl", hash = "sha256:6efbe303983441c51975c243e26dff328aca11f94b70992f35b093c2e71801e1", size = 10511206, upload-time = "2026-04-02T18:16:41.574Z" },
- { url = "https://files.pythonhosted.org/packages/3d/f6/32bfe3e9c136b35f02e489778d94384118bb80fd92c6d92e7ccd97db12ce/ruff-0.15.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:4965bac6ac9ea86772f4e23587746f0b7a395eccabb823eb8bfacc3fa06069f7", size = 10923307, upload-time = "2026-04-02T18:17:08.645Z" },
- { url = "https://files.pythonhosted.org/packages/ca/25/de55f52ab5535d12e7aaba1de37a84be6179fb20bddcbe71ec091b4a3243/ruff-0.15.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf05aad70ca5b5a0a4b0e080df3a6b699803916d88f006efd1f5b46302daab8", size = 10316722, upload-time = "2026-04-02T18:16:44.206Z" },
- { url = "https://files.pythonhosted.org/packages/48/11/690d75f3fd6278fe55fff7c9eb429c92d207e14b25d1cae4064a32677029/ruff-0.15.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9439a342adb8725f32f92732e2bafb6d5246bd7a5021101166b223d312e8fc59", size = 10623674, upload-time = "2026-04-02T18:16:50.951Z" },
- { url = "https://files.pythonhosted.org/packages/bd/ec/176f6987be248fc5404199255522f57af1b4a5a1b57727e942479fec98ad/ruff-0.15.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c5e6faf9d97c8edc43877c3f406f47446fc48c40e1442d58cfcdaba2acea745", size = 10351516, upload-time = "2026-04-02T18:16:57.206Z" },
- { url = "https://files.pythonhosted.org/packages/b2/fc/51cffbd2b3f240accc380171d51446a32aa2ea43a40d4a45ada67368fbd2/ruff-0.15.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b34a9766aeec27a222373d0b055722900fbc0582b24f39661aa96f3fe6ad901", size = 11150202, upload-time = "2026-04-02T18:17:06.452Z" },
- { url = "https://files.pythonhosted.org/packages/d6/d4/25292a6dfc125f6b6528fe6af31f5e996e19bf73ca8e3ce6eb7fa5b95885/ruff-0.15.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89dd695bc72ae76ff484ae54b7e8b0f6b50f49046e198355e44ea656e521fef9", size = 11988891, upload-time = "2026-04-02T18:17:18.575Z" },
- { url = "https://files.pythonhosted.org/packages/13/e1/1eebcb885c10e19f969dcb93d8413dfee8172578709d7ee933640f5e7147/ruff-0.15.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce187224ef1de1bd225bc9a152ac7102a6171107f026e81f317e4257052916d5", size = 11480576, upload-time = "2026-04-02T18:16:52.986Z" },
- { url = "https://files.pythonhosted.org/packages/ff/6b/a1548ac378a78332a4c3dcf4a134c2475a36d2a22ddfa272acd574140b50/ruff-0.15.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b0c7c341f68adb01c488c3b7d4b49aa8ea97409eae6462d860a79cf55f431b6", size = 11254525, upload-time = "2026-04-02T18:17:02.041Z" },
- { url = "https://files.pythonhosted.org/packages/42/aa/4bb3af8e61acd9b1281db2ab77e8b2c3c5e5599bf2a29d4a942f1c62b8d6/ruff-0.15.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:55cc15eee27dc0eebdfcb0d185a6153420efbedc15eb1d38fe5e685657b0f840", size = 11204072, upload-time = "2026-04-02T18:17:13.581Z" },
- { url = "https://files.pythonhosted.org/packages/69/48/d550dc2aa6e423ea0bcc1d0ff0699325ffe8a811e2dba156bd80750b86dc/ruff-0.15.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6537f6eed5cda688c81073d46ffdfb962a5f29ecb6f7e770b2dc920598997ed", size = 10594998, upload-time = "2026-04-02T18:16:46.369Z" },
- { url = "https://files.pythonhosted.org/packages/63/47/321167e17f5344ed5ec6b0aa2cff64efef5f9e985af8f5622cfa6536043f/ruff-0.15.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6d3fcbca7388b066139c523bda744c822258ebdcfbba7d24410c3f454cc9af71", size = 10359769, upload-time = "2026-04-02T18:17:10.994Z" },
- { url = "https://files.pythonhosted.org/packages/67/5e/074f00b9785d1d2c6f8c22a21e023d0c2c1817838cfca4c8243200a1fa87/ruff-0.15.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:058d8e99e1bfe79d8a0def0b481c56059ee6716214f7e425d8e737e412d69677", size = 10850236, upload-time = "2026-04-02T18:16:48.749Z" },
- { url = "https://files.pythonhosted.org/packages/76/37/804c4135a2a2caf042925d30d5f68181bdbd4461fd0d7739da28305df593/ruff-0.15.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8e1ddb11dbd61d5983fa2d7d6370ef3eb210951e443cace19594c01c72abab4c", size = 11358343, upload-time = "2026-04-02T18:16:55.068Z" },
- { url = "https://files.pythonhosted.org/packages/88/3d/1364fcde8656962782aa9ea93c92d98682b1ecec2f184e625a965ad3b4a6/ruff-0.15.9-py3-none-win32.whl", hash = "sha256:bde6ff36eaf72b700f32b7196088970bf8fdb2b917b7accd8c371bfc0fd573ec", size = 10583382, upload-time = "2026-04-02T18:17:04.261Z" },
- { url = "https://files.pythonhosted.org/packages/4c/56/5c7084299bd2cacaa07ae63a91c6f4ba66edc08bf28f356b24f6b717c799/ruff-0.15.9-py3-none-win_amd64.whl", hash = "sha256:45a70921b80e1c10cf0b734ef09421f71b5aa11d27404edc89d7e8a69505e43d", size = 11744969, upload-time = "2026-04-02T18:16:59.611Z" },
- { url = "https://files.pythonhosted.org/packages/03/36/76704c4f312257d6dbaae3c959add2a622f63fcca9d864659ce6d8d97d3d/ruff-0.15.9-py3-none-win_arm64.whl", hash = "sha256:0694e601c028fd97dc5c6ee244675bc241aeefced7ef80cd9c6935a871078f53", size = 11005870, upload-time = "2026-04-02T18:17:15.773Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/00/a1c2fdc9939b2c03691edbda290afcd297f1f389196172826b03d6b6a595/ruff-0.15.10-py3-none-linux_armv6l.whl", hash = "sha256:0744e31482f8f7d0d10a11fcbf897af272fefdfcb10f5af907b18c2813ff4d5f", size = 10563362, upload-time = "2026-04-09T14:06:21.189Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/15/006990029aea0bebe9d33c73c3e28c80c391ebdba408d1b08496f00d422d/ruff-0.15.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b1e7c16ea0ff5a53b7c2df52d947e685973049be1cdfe2b59a9c43601897b22e", size = 10951122, upload-time = "2026-04-09T14:06:02.236Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/c0/4ac978fe874d0618c7da647862afe697b281c2806f13ce904ad652fa87e4/ruff-0.15.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93cc06a19e5155b4441dd72808fdf84290d84ad8a39ca3b0f994363ade4cebb1", size = 10314005, upload-time = "2026-04-09T14:06:00.026Z" },
+ { url = "https://files.pythonhosted.org/packages/da/73/c209138a5c98c0d321266372fc4e33ad43d506d7e5dd817dd89b60a8548f/ruff-0.15.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83e1dd04312997c99ea6965df66a14fb4f03ba978564574ffc68b0d61fd3989e", size = 10643450, upload-time = "2026-04-09T14:05:42.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/76/0deec355d8ec10709653635b1f90856735302cb8e149acfdf6f82a5feb70/ruff-0.15.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8154d43684e4333360fedd11aaa40b1b08a4e37d8ffa9d95fee6fa5b37b6fab1", size = 10379597, upload-time = "2026-04-09T14:05:49.984Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/be/86bba8fc8798c081e28a4b3bb6d143ccad3fd5f6f024f02002b8f08a9fa3/ruff-0.15.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ab88715f3a6deb6bde6c227f3a123410bec7b855c3ae331b4c006189e895cef", size = 11146645, upload-time = "2026-04-09T14:06:12.246Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/89/140025e65911b281c57be1d385ba1d932c2366ca88ae6663685aed8d4881/ruff-0.15.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a768ff5969b4f44c349d48edf4ab4f91eddb27fd9d77799598e130fb628aa158", size = 12030289, upload-time = "2026-04-09T14:06:04.776Z" },
+ { url = "https://files.pythonhosted.org/packages/88/de/ddacca9545a5e01332567db01d44bd8cf725f2db3b3d61a80550b48308ea/ruff-0.15.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ee3ef42dab7078bda5ff6a1bcba8539e9857deb447132ad5566a038674540d0", size = 11496266, upload-time = "2026-04-09T14:05:55.485Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/bb/7ddb00a83760ff4a83c4e2fc231fd63937cc7317c10c82f583302e0f6586/ruff-0.15.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51cb8cc943e891ba99989dd92d61e29b1d231e14811db9be6440ecf25d5c1609", size = 11256418, upload-time = "2026-04-09T14:05:57.69Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/8d/55de0d35aacf6cd50b6ee91ee0f291672080021896543776f4170fc5c454/ruff-0.15.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:e59c9bdc056a320fb9ea1700a8d591718b8faf78af065484e801258d3a76bc3f", size = 11288416, upload-time = "2026-04-09T14:05:44.695Z" },
+ { url = "https://files.pythonhosted.org/packages/68/cf/9438b1a27426ec46a80e0a718093c7f958ef72f43eb3111862949ead3cc1/ruff-0.15.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:136c00ca2f47b0018b073f28cb5c1506642a830ea941a60354b0e8bc8076b151", size = 10621053, upload-time = "2026-04-09T14:05:52.782Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/50/e29be6e2c135e9cd4cb15fbade49d6a2717e009dff3766dd080fcb82e251/ruff-0.15.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8b80a2f3c9c8a950d6237f2ca12b206bccff626139be9fa005f14feb881a1ae8", size = 10378302, upload-time = "2026-04-09T14:06:14.361Z" },
+ { url = "https://files.pythonhosted.org/packages/18/2f/e0b36a6f99c51bb89f3a30239bc7bf97e87a37ae80aa2d6542d6e5150364/ruff-0.15.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e3e53c588164dc025b671c9df2462429d60357ea91af7e92e9d56c565a9f1b07", size = 10850074, upload-time = "2026-04-09T14:06:16.581Z" },
+ { url = "https://files.pythonhosted.org/packages/11/08/874da392558ce087a0f9b709dc6ec0d60cbc694c1c772dab8d5f31efe8cb/ruff-0.15.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b0c52744cf9f143a393e284125d2576140b68264a93c6716464e129a3e9adb48", size = 11358051, upload-time = "2026-04-09T14:06:18.948Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/46/602938f030adfa043e67112b73821024dc79f3ab4df5474c25fa4c1d2d14/ruff-0.15.10-py3-none-win32.whl", hash = "sha256:d4272e87e801e9a27a2e8df7b21011c909d9ddd82f4f3281d269b6ba19789ca5", size = 10588964, upload-time = "2026-04-09T14:06:07.14Z" },
+ { url = "https://files.pythonhosted.org/packages/25/b6/261225b875d7a13b33a6d02508c39c28450b2041bb01d0f7f1a83d569512/ruff-0.15.10-py3-none-win_amd64.whl", hash = "sha256:28cb32d53203242d403d819fd6983152489b12e4a3ae44993543d6fe62ab42ed", size = 11745044, upload-time = "2026-04-09T14:05:39.473Z" },
+ { url = "https://files.pythonhosted.org/packages/58/ed/dea90a65b7d9e69888890fb14c90d7f51bf0c1e82ad800aeb0160e4bacfd/ruff-0.15.10-py3-none-win_arm64.whl", hash = "sha256:601d1610a9e1f1c2165a4f561eeaa2e2ea1e97f3287c5aa258d3dab8b57c6188", size = 11035607, upload-time = "2026-04-09T14:05:47.593Z" },
]
[[package]]
@@ -1445,15 +1378,15 @@ wheels = [
[[package]]
name = "sentry-sdk"
-version = "2.57.0"
+version = "2.58.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "urllib3" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/4f/87/46c0406d8b5ddd026f73adaf5ab75ce144219c41a4830b52df4b9ab55f7f/sentry_sdk-2.57.0.tar.gz", hash = "sha256:4be8d1e71c32fb27f79c577a337ac8912137bba4bcbc64a4ec1da4d6d8dc5199", size = 435288, upload-time = "2026-03-31T09:39:29.264Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/26/b3/fb8291170d0e844173164709fc0fa0c221ed75a5da740c8746f2a83b4eb1/sentry_sdk-2.58.0.tar.gz", hash = "sha256:c1144d947352d54e5b7daa63596d9f848adf684989c06c4f5a659f0c85a18f6f", size = 438764, upload-time = "2026-04-13T17:23:26.265Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c9/64/982e07b93219cb52e1cca5d272cb579e2f3eb001956c9e7a9a6d106c9473/sentry_sdk-2.57.0-py2.py3-none-any.whl", hash = "sha256:812c8bf5ff3d2f0e89c82f5ce80ab3a6423e102729c4706af7413fd1eb480585", size = 456489, upload-time = "2026-03-31T09:39:27.524Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/eb/d875669993b762556ae8b2efd86219943b4c0864d22204d622a9aee3052b/sentry_sdk-2.58.0-py2.py3-none-any.whl", hash = "sha256:688d1c704ddecf382ea3326f21a67453d4caa95592d722b7c780a36a9d23109e", size = 460919, upload-time = "2026-04-13T17:23:24.675Z" },
]
[[package]]
@@ -1549,26 +1482,26 @@ wheels = [
[[package]]
name = "ty"
-version = "0.0.29"
+version = "0.0.31"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/47/d5/853561de49fae38c519e905b2d8da9c531219608f1fccc47a0fc2c896980/ty-0.0.29.tar.gz", hash = "sha256:e7936cca2f691eeda631876c92809688dbbab68687c3473f526cd83b6a9228d8", size = 5469221, upload-time = "2026-04-05T15:01:21.328Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/31/cc/5ea5d3a72216c8c2bf77d83066dd4f3553532d0aacc03d4a8397dd9845e1/ty-0.0.31.tar.gz", hash = "sha256:4a4094292d9671caf3b510c7edf36991acd9c962bb5d97205374ffed9f541c45", size = 5516619, upload-time = "2026-04-15T15:47:59.87Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/03/b7/911f9962115acfa24e3b2ec9d4992dd994c38e8769e1b1d7680bb4d28a51/ty-0.0.29-py3-none-linux_armv6l.whl", hash = "sha256:b8a40955f7660d3eaceb0d964affc81b790c0765e7052921a5f861ff8a471c30", size = 10568206, upload-time = "2026-04-05T15:01:19.165Z" },
- { url = "https://files.pythonhosted.org/packages/fe/c3/fcae2167d4c77a97269f92f11d1b43b03617f81de1283d5d05b43432110c/ty-0.0.29-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6b6849adae15b00bbe2d3c5b078967dcb62eba37d38936b8eeb4c81a82d2e3b8", size = 10442530, upload-time = "2026-04-05T15:01:28.471Z" },
- { url = "https://files.pythonhosted.org/packages/97/33/5a6bfa240cfcb9c36046ae2459fa9ea23238d20130d8656ff5ac4d6c012a/ty-0.0.29-py3-none-macosx_11_0_arm64.whl", hash = "sha256:dcdd9b17209788152f7b7ea815eda07989152325052fe690013537cc7904ce49", size = 9915735, upload-time = "2026-04-05T15:01:10.365Z" },
- { url = "https://files.pythonhosted.org/packages/b3/1e/318f45fae232118e81a6306c30f50de42c509c412128d5bd231eab699ffb/ty-0.0.29-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d8ed4789bae78ffaf94462c0d25589a734cab0366b86f2bbcb1bb90e1a7a169", size = 10419748, upload-time = "2026-04-05T15:01:32.375Z" },
- { url = "https://files.pythonhosted.org/packages/a9/a8/5687872e2ab5a0f7dd4fd8456eac31e9381ad4dc74961f6f29965ad4dd91/ty-0.0.29-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ec374b8565e0ad0900011c24641ebbef2da51adbd4fb69ff3280c8a7eceb02", size = 10394738, upload-time = "2026-04-05T15:01:06.473Z" },
- { url = "https://files.pythonhosted.org/packages/de/68/015d118097eeb95e6a44c4abce4c0a28b7b9dfb3085b7f0ee48e4f099633/ty-0.0.29-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:298a8d5faa2502d3810bbbb47a030b9455495b9921594206043c785dd61548cf", size = 10910613, upload-time = "2026-04-05T15:01:17.17Z" },
- { url = "https://files.pythonhosted.org/packages/1c/01/47ce3c6c53e0670eadbe80756b167bf80ed6681d1ba57cfde2e8065a13d1/ty-0.0.29-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c8fba1a3524c6109d1e020d92301c79d41bf442fa8d335b9fa366239339cb70", size = 11475750, upload-time = "2026-04-05T15:01:30.461Z" },
- { url = "https://files.pythonhosted.org/packages/c4/cf/e361845b1081c9264ad5b7c963231bab03f2666865a9f2a115c4233f2137/ty-0.0.29-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c48adf88a70d264128c39ee922ed14a947817fced1e93c08c1a89c9244edcde", size = 11190055, upload-time = "2026-04-05T15:01:12.369Z" },
- { url = "https://files.pythonhosted.org/packages/79/12/0fb0857e9a62cb11586e9a712103877bbf717f5fb570d16634408cfdefee/ty-0.0.29-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ce0a7a0e96bc7b42518cd3a1a6a6298ef64ff40ca4614355c1aa807059b5c6f", size = 11020539, upload-time = "2026-04-05T15:01:37.022Z" },
- { url = "https://files.pythonhosted.org/packages/20/36/5a26753802083f80cd125db6c4348ad42b3c982ec36e718e0bf4c18f75e5/ty-0.0.29-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6ac86a05b4a3731d45365ab97780acc7b8146fa62fccb3cbe94fe6546c67a97", size = 10396399, upload-time = "2026-04-05T15:01:26.167Z" },
- { url = "https://files.pythonhosted.org/packages/00/e6/b4e75b5752239ab3ab400f19faef4dbef81d05aab5d3419fda0c062a3765/ty-0.0.29-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:6bbbf53141af0f3150bf288d716263f1a3550054e4b3551ca866d38192ba9891", size = 10421461, upload-time = "2026-04-05T15:01:08.367Z" },
- { url = "https://files.pythonhosted.org/packages/c0/21/1084b5b609f9abed62070ec0b31c283a403832a6310c8bbc208bd45ee1e6/ty-0.0.29-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1c9e06b770c1d0ff5efc51e34312390db31d53fcf3088163f413030b42b74f84", size = 10599187, upload-time = "2026-04-05T15:01:23.52Z" },
- { url = "https://files.pythonhosted.org/packages/ab/a1/ce19a2ca717bbcc1ee11378aba52ef70b6ce5b87245162a729d9fdc2360f/ty-0.0.29-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0307fe37e3f000ef1a4ae230bbaf511508a78d24a5e51b40902a21b09d5e6037", size = 11121198, upload-time = "2026-04-05T15:01:15.22Z" },
- { url = "https://files.pythonhosted.org/packages/6b/6b/f1430b279af704321566ce7ec2725d3d8258c2f815ebd93e474c64cd4543/ty-0.0.29-py3-none-win32.whl", hash = "sha256:7a2a898217960a825f8bc0087e1fdbaf379606175e98f9807187221d53a4a8ed", size = 9995331, upload-time = "2026-04-05T15:01:01.32Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ef/3ef01c17785ff9a69378465c7d0faccd48a07b163554db0995e5d65a5a23/ty-0.0.29-py3-none-win_amd64.whl", hash = "sha256:fc1294200226b91615acbf34e0a9ad81caf98c081e9c6a912a31b0a7b603bc3f", size = 11023644, upload-time = "2026-04-05T15:01:04.432Z" },
- { url = "https://files.pythonhosted.org/packages/2c/55/87280a994d6a2d2647c65e12abbc997ed49835794366153c04c4d9304d76/ty-0.0.29-py3-none-win_arm64.whl", hash = "sha256:f9794bbd1bb3ce13f78c191d0c89ae4c63f52c12b6daa0c6fe220b90d019d12c", size = 10428165, upload-time = "2026-04-05T15:01:34.665Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/10/ea805cbbd75d5d50792551a2b383de8521eeab0c44f38c73e12819ced65e/ty-0.0.31-py3-none-linux_armv6l.whl", hash = "sha256:761651dc17ad7bc0abfc1b04b3f0e84df263ed435d34f29760b3da739ab02d35", size = 10834749, upload-time = "2026-04-15T15:48:14.877Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/4c/fabf951850401d24d36b21bced088a366c6827e1c37dab4523afff84c4b2/ty-0.0.31-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c529922395a07231c27488f0290651e05d27d149f7e0aa807678f1f7e9c58a5e", size = 10626012, upload-time = "2026-04-15T15:48:22.554Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b0/4a5aff88d2544f19514a59c8f693d63144aa7307fe2ee5df608333ab5460/ty-0.0.31-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5f345df2b87d747859e72c2cbc9be607ea1bbc8bc93dd32fa3d03ea091cb4fee", size = 10075790, upload-time = "2026-04-15T15:47:46.959Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/73/9d4dcad12cd4e85274014f2c0510ef93f590b2a1e5148de3a9f276098dad/ty-0.0.31-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4b207eddcfbafd376132689d3435b14efcb531289cb59cd961c6a611133bd54", size = 10590286, upload-time = "2026-04-15T15:48:06.222Z" },
+ { url = "https://files.pythonhosted.org/packages/47/45/fe40adde18692359ded174ae7ddbfac056e876eb0f43b65be74fde7f6072/ty-0.0.31-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:663778b220f357067488ce68bfc52335ccbd161549776f70dcbde6bbde82f77a", size = 10623824, upload-time = "2026-04-15T15:48:12.965Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/e8/0ffa2e09b548e6daa9ebc368d68b767dc2405ca4cbeadb7ede0e2cb21059/ty-0.0.31-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3506cfe87dfade0fb2960dd4fffd4fd8089003587b3445c0a1a295c9d83764fb", size = 11156864, upload-time = "2026-04-15T15:48:08.473Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e9/fd44c2075115d569593ee9473d7e2a38b750fd7e783421c95eb528c15df5/ty-0.0.31-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8b3f3d8492f08e81916026354c1d1599e9ddfa1241804141a74d5662fc710085", size = 11696401, upload-time = "2026-04-15T15:48:17.355Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/50/35aad8eadf964d23e2a4faa5b38a206aa85c78833c8ce335dddd2c34ba63/ty-0.0.31-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97de32ee6a619393a4c495e056a1c547de7877510f3152e61345c71d774d2d0", size = 11374903, upload-time = "2026-04-15T15:47:55.893Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/37/01eccd25d23f5aaa7f7ff1a87b5b215469f6b202cf689a1812b71c1e7f6b/ty-0.0.31-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c906354ce441e342646582bc9b8f48a676f79f3d061e25de15ff870e015ca14e", size = 11206624, upload-time = "2026-04-15T15:47:51.778Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/70/baad2914cb097453f127a221f8addb2b41926098059cd773c75e6a662fc4/ty-0.0.31-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:275bb7c82afcbf89fe2dbef1b2692f2bc98451f1ee2c8eb809ddd91317822388", size = 10575089, upload-time = "2026-04-15T15:47:49.448Z" },
+ { url = "https://files.pythonhosted.org/packages/83/12/bae3a7bba2e785eb72ce00f9da70eedcb8c5e8299efecbd16e6e436abd82/ty-0.0.31-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:405da247027c6efd1e264886b6ac4a86ab3a4f09200b02e33630efe85f119e53", size = 10642315, upload-time = "2026-04-15T15:48:19.661Z" },
+ { url = "https://files.pythonhosted.org/packages/93/9e/cad04d5d839bc60355cea98c7e09d724ea65f47184def0fae8b90dc54591/ty-0.0.31-py3-none-musllinux_1_2_i686.whl", hash = "sha256:54d9835608eed196853d6643f645c50ce83bcc7fe546cdb3e210c1bcf7c58c09", size = 10834473, upload-time = "2026-04-15T15:48:02.091Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/ba/84112d280182d37690d3d2b4018b2667e42bc281585e607015635310016a/ty-0.0.31-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5ee11be9b07e8c0c6b455ff075a0abe4f194de9476f57624db98eec9df618355", size = 11315785, upload-time = "2026-04-15T15:48:10.754Z" },
+ { url = "https://files.pythonhosted.org/packages/50/9f/ac42dc223d7e0950e97a1854567a8b3e7fe09ad7375adbf91bfb43290482/ty-0.0.31-py3-none-win32.whl", hash = "sha256:7286587aacf3eef0956062d6492b893b02f82b0f22c5e230008e13ff0d216a8b", size = 10187657, upload-time = "2026-04-15T15:48:04.264Z" },
+ { url = "https://files.pythonhosted.org/packages/75/3e/57ba7ea7ecb2f4751644ba91756e2be70e33ef5952c0c41a256a0e4c2437/ty-0.0.31-py3-none-win_amd64.whl", hash = "sha256:81134e25d2a2562ab372f24de8f9bd05034d27d30377a5d7540f259791c6234c", size = 11205258, upload-time = "2026-04-15T15:47:53.759Z" },
+ { url = "https://files.pythonhosted.org/packages/88/39/bca669095ccf0a400af941fdf741578d4c2d6719f1b7f10e6dbec10aa862/ty-0.0.31-py3-none-win_arm64.whl", hash = "sha256:e9cb15fad26545c6a608f40f227af3a5513cb376998ca6feddd47ca7d93ffafa", size = 10590392, upload-time = "2026-04-15T15:47:57.968Z" },
]
[[package]]
@@ -1589,27 +1522,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
-[[package]]
-name = "watchdog"
-version = "6.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
- { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
- { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
- { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
- { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
- { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
- { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
- { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
- { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
- { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
- { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
- { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
- { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
-]
-
[[package]]
name = "websocket-client"
version = "1.9.0"
@@ -1669,10 +1581,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" },
]
+[[package]]
+name = "zensical"
+version = "0.0.33"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "deepmerge" },
+ { name = "markdown" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/59/c2/dea4b86dc1ca2a7b55414017f12cfb12b5cfdf3a1ed7c77a04c271eb523b/zensical-0.0.33.tar.gz", hash = "sha256:05209cb4f80185c533e0d37c25d084ddc2050e3d5a4dd1b1812961c2ee0c3380", size = 3892278, upload-time = "2026-04-14T11:08:19.895Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/74/5f/45d5200405420a9d8ac91cf9e7826622ea12f3198e8e6ac4ffb481eb53bf/zensical-0.0.33-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f658e3c241cfbb560bd8811116a9486cff7e04d7d5aed73569dd533c74187450", size = 12416748, upload-time = "2026-04-14T11:07:43.246Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1e/aadaf31d6e4d20419ecedaf0b1c804e359ec23dcdb44c8d2bf6d8407080c/zensical-0.0.33-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f9813ac3256c28e2e2f1ba5c9fab1b4bca62bbe0e0f8e85ac22d33b068b1b08a", size = 12293372, upload-time = "2026-04-14T11:07:46.569Z" },
+ { url = "https://files.pythonhosted.org/packages/db/e5/838be8451ea8b2aecec39fbec3971060fc705e17f5741249740d9b6a6824/zensical-0.0.33-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3bad7ac71028769c5d1f3f84f448dbb7352db28d77095d1b40a8d1b0aa34ec30", size = 12659832, upload-time = "2026-04-14T11:07:50.754Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/5c/dd957d7c83efc13a70a6058d4190a3afcf29942aefb391120bca5466347d/zensical-0.0.33-cp310-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:06bb039daf044547c9400a52f9493b3cd486ba9baef3324fdcffd2e26e61105f", size = 12603847, upload-time = "2026-04-14T11:07:53.698Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/99/dd6ccc392ece1f34fb20ea339a01717badbbeb2fba1d4f3019a5028d0bcc/zensical-0.0.33-cp310-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:260238062b3139ece0edab93f4dbe7a12923453091f5aa580dfd73e799388076", size = 12956236, upload-time = "2026-04-14T11:07:56.728Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/76/e0a1b884eadf6afa7e2d56c90c268eec36836ac27e96ef250c0129e55417/zensical-0.0.33-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dff0f4afda7b8586bc4ab2a5684bce5b282232dd4e0cad3be4c73fedd264425", size = 12701944, upload-time = "2026-04-14T11:07:59.928Z" },
+ { url = "https://files.pythonhosted.org/packages/38/38/e1ff13461e406864fa2b23fc828822659a7dbac5c79398f724d17f088540/zensical-0.0.33-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:207b4d81b208d75b97dc7bd318804550b886a3e852ef67429ef0e6b9442839d1", size = 12835444, upload-time = "2026-04-14T11:08:02.998Z" },
+ { url = "https://files.pythonhosted.org/packages/41/04/7d24d52d6903fc5c511633afe8b5716fef19da09685327665cc127f61648/zensical-0.0.33-cp310-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:06d2f57f7bc8cc8fd904386020ea1365eebc411e8698a871e9525c885abca574", size = 12878419, upload-time = "2026-04-14T11:08:06.054Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/ec/87fc9e360c694ab006363c7834639eccafd0d26a487cd63dd609bd68f36a/zensical-0.0.33-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:c2851b82d83aa0b2ae4f8e99731cfeedeecebfa04e6b3fc4d375deca629fa240", size = 13022474, upload-time = "2026-04-14T11:08:09.007Z" },
+ { url = "https://files.pythonhosted.org/packages/10/b3/0bf174ab6ceedb31d9af462073b5339c894b2084a27d42cb9f0906050d76/zensical-0.0.33-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:90daaf512b0429d7b9147ad5e6085b455d24803eff18b508aed738ca65444683", size = 12975233, upload-time = "2026-04-14T11:08:12.535Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/27/7cc3c2d284698647f60f3b823e0101e619c87edf158d47ee11bf4bfb6228/zensical-0.0.33-cp310-abi3-win32.whl", hash = "sha256:2701820597fe19361a12371129927c58c19633dcaa5f6986d610dce58cecd8c4", size = 12012664, upload-time = "2026-04-14T11:08:14.977Z" },
+ { url = "https://files.pythonhosted.org/packages/25/0b/6be5c2fdaf9f1600577e7ba5e235d86b72a26f6af389efb146f978f76ac3/zensical-0.0.33-cp310-abi3-win_amd64.whl", hash = "sha256:a5a0911b4247708a55951b74c459f4d5faec5daaf287d23a2e1f0d96be1e647f", size = 12206255, upload-time = "2026-04-14T11:08:17.375Z" },
+]
+
[[package]]
name = "zeromq"
version = "4.3.5"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq#250faf500a3d101b91f4c85a4618fe1882c9cf61" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zeromq&rev=release-zeromq#173fe8e9a0b8cf666bac5363c3376e866a386568" }
[[package]]
name = "zstandard"
@@ -1702,4 +1642,4 @@ wheels = [
[[package]]
name = "zstd"
version = "1.5.6"
-source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd#6896f3e5ea22d632c5ea3bc6e5f3b773c144f43b" }
+source = { git = "https://github.com/commaai/dependencies.git?subdirectory=zstd&rev=release-zstd#c4b1fdec74010075965d68e2c743055c6ef18d48" }
diff --git a/zensical.toml b/zensical.toml
new file mode 100644
index 0000000000..7e5ca2c5db
--- /dev/null
+++ b/zensical.toml
@@ -0,0 +1,81 @@
+[project]
+site_name = "openpilot docs"
+site_url = "https://docs.comma.ai"
+repo_url = "https://github.com/commaai/openpilot/"
+
+docs_dir = "docs"
+site_dir = "docs_site/"
+
+extra_css = ["stylesheets/extra.css"]
+
+nav = [
+ { "What is openpilot?" = "index.md" },
+ { "How-to" = [
+ { "Turn the speed blue" = "how-to/turn-the-speed-blue.md" },
+ { "Connect to a comma 3X or four" = "how-to/connect-to-comma.md" },
+ { "Add support for a car" = "how-to/car-port.md" },
+ ] },
+ { "Concepts" = [
+ { "Logs" = "concepts/logs.md" },
+ { "Safety" = "concepts/safety.md" },
+ { "Glossary" = "concepts/glossary.md" },
+ ] },
+ { "Contributing" = [
+ { "Feedback" = "contributing/feedback.md" },
+ { "Roadmap" = "contributing/roadmap.md" },
+ { "Contributing Guide →" = "https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md" },
+ ] },
+ { "Links" = [
+ { "Blog →" = "https://blog.comma.ai" },
+ { "Bounties →" = "https://comma.ai/bounties" },
+ { "GitHub →" = "https://github.com/commaai" },
+ { "Discord →" = "https://discord.comma.ai" },
+ { "X →" = "https://x.com/comma_ai" },
+ ] },
+]
+
+[project.theme]
+logo = "assets/comma-logo.png"
+features = [
+ "navigation.expand",
+ "navigation.sections",
+ "navigation.instant",
+ "navigation.instant.prefetch",
+ "content.code.copy",
+ "content.action.edit",
+ "content.action.view",
+]
+
+[[project.extra.social]]
+icon = "fontawesome/brands/github"
+link = "https://github.com/commaai"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/discord"
+link = "https://discord.comma.ai"
+
+[[project.extra.social]]
+icon = "fontawesome/brands/x-twitter"
+link = "https://x.com/comma_ai"
+
+[project.markdown_extensions.attr_list]
+
+[project.markdown_extensions.admonition]
+
+[project.markdown_extensions.md_in_html]
+
+[project.markdown_extensions.pymdownx.highlight]
+anchor_linenums = true
+line_spans = "__span"
+pygments_lang_class = true
+
+[project.markdown_extensions.pymdownx.inlinehilite]
+
+[project.markdown_extensions.pymdownx.magiclink]
+
+[project.markdown_extensions.pymdownx.superfences]
+custom_fences = [{ name = "mermaid", class = "mermaid" }]
+
+[project.markdown_extensions.pymdownx.details]
+
+[project.markdown_extensions."ext.glossary"]